Persistence APIs

The Record class provides a set of flexible persistence APIs for saving objects to or deleting objects from a database. The APIs enable pre- and post-processing of data before and after an object is saved to the database or deleted from it. You can leverage the APIs to implement a variety of persistence-related behaviors, such as validating data and sending notifications.

Save

The persistence APIs include the following save methods, each with different functional characteristics. Note that some save methods, when invoked, automatically trigger a sequence of callback methods that you can implement for pre- and post-processing of data. For more information, see Save Lifecycle.

save
This method does a standard save to all configured databases. It triggers the save lifecycle.
saveImmediately
This method allows the save to execute outside the context of a transaction. It triggers the save lifecycle.
saveUniquely
This method circumvents a uniqueness constraint violation on an indexed value. If there is a violation, this method saves a new object with a duplicate value directly to the database. It does not trigger the save lifecycle.
saveUnsafely
This method can be called on a State object. It saves an object directly to the database without validation; that is, it does not trigger the save lifecycle.

Save Lifecycle

When either the save method or the saveImmediately method is called on a Record-derived object, Dari does not directly persist the object to the configured databases. Instead, Dari invokes a lifecycle of callback methods that starts with beforeSave and ends with afterSave. Invoking this lifecycle allows for pre- and post-save processing of objects, and conditional handling of uniqueness violations. For example, you can custom-validate objects before saving, send notifications after saving, and handle duplicate settings on fields that require unique values.

By default, the save callback methods do nothing. If you want processing to occur before or after the save operation executes on the database, you must implement the applicable callback methods.

Note

If you need to save or delete a separate object in any of the save callbacks, always use saveImmediately or deleteImmediately. If you need to save or delete a lot of objects, consider using a database transaction with beginIsolatedWrites.

The save lifecycle executes as follows:

1: beforeSave

Use beforeSave for simple state changes, not expensive database or API calls, as it is called frequently while editors are working; it is called only once, however, for saves initiated in code.

By leveraging the beforeSave method, data can be validated, modified or thrown out when content is saved. In the code snippet below, a hidden field called internalName is populated with a value constructed from the normalized version of name plus code.

public class Project extends Content
{
    @Indexed
    @Required
    private String name;

    @Required
    private String code;

    private Boolean inactive;

    @Indexed
    @ToolUi.Hidden
    private String internalName;

   // getters and setters
   ...

   @Override
   public void beforeSave() {
      this.internalName = StringUtils.toNormalized(name) + "-" + getCode();
   }
}
2: onValidate

Use for custom validation on an object. If a uniqueness constraint violation is detected, the onDuplicate method is called next in the lifecycle.

The onValidate callback allows you to handle data input errors in the Content Edit Form. You can use onValidate with Brightspot validation annotations. Although Brightspot responds to input errors with applicable messaging, you can provide additional error processing or messaging. In addition, you can use this callback for custom validations.

In the following example, a @Required annotation is set on the Activity fields. An onValidate callback is included to enhance the validation error messages for two of the fields that are required. In addition, a custom validation is performed for the project field.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Activity extends Content {

   @Required
   private Project project;

   @Required
   private Date activityDate;

   @Required
   private User activityUser;

   @Required
   private String description;

   // getters and setters
   ...

   protected void onValidate() {

      if (activityUser == null) {
          getState().addError(getState().getField("activityUser"), "Who are you?");
      }

      if (activityDate == null) {
          getState().addError(getState().getField("activityDate"), "Enter today's date");
      }

      if (project.getInactive()) {
          getState().addError(getState().getField("project"), "Please select a different project");
          throw new IllegalArgumentException(project.getName() + " can no longer be used!");
      }
   }
}

In the previous snippet—

  • Lines 20–26 check if the activityUser and activityDate fields have been set. If not, the Brightspot error message of “Required!” is extended with a custom message, as illustrated below. Note that onValidate does not check the description field, so a missing value results in only the Brightspot error message.
  • Lines 28–31 supply error messages if the Project object set on the project field is no longer active. An inactive project selection results in a message at the top of the content edit form and in the project field.
../../_images/onvalidate.png
3: beforeCommit
Use to make additional changes to the object prior to persisting it to the database.
4: Save operation to database
Dari persists the object as a record directly to the underlying database implementation.
5: onDuplicate

This method is invoked if the save operation fails because a uniqueness constraint violation is detected on an indexed field or method (@index(unique=true)). Use this method to handle the violation by setting a unique value and returning true. If the violation is eliminated, the lifecycle returns to the onValidate method.

Another option for handling a uniqueness constraint violation is to use the saveUniquely method. It forces a save on a new object with a duplicate value and circumvents the save lifecycle.

6: afterSave

Called after an object is successfully saved to the database, use for post-save processing, such as sending notifications.

For example, in the following code snippet, the afterSave method logs basic information about new User instances.

public class User extends Record
{
    ...

    // Executes after saving instance.
    public void afterSave()
    {
        State state = getState();
        Database db = state.getDatabase();
        System.out.print("Saved new User in database " + db.getName() + "\n"
           + "Id: " + this.getId() + "\n"
           + "User: " + this.getLabel());
    }
 }

Delete

The persistence APIs include the delete method for removing objects from a database. When delete is called on a Record-derived object, Dari does not directly remove the object from the database. Instead, Dari invokes a lifecycle of callback methods that allows for pre- and post-delete processing of data.

By default, the delete callback methods do nothing. If you want processing to occur before or after the delete operation executes on the database, you must implement the callback methods.

Note

If you need to save or delete a separate object in either of the delete callbacks, always use saveImmediately or deleteImmediately. If you need to save or delete a lot of objects, consider using a database transaction with beginIsolatedWrites.

The delete lifecycle executes as follows:

1: beforeDelete

Use for processing before the object is deleted from the database. This callback is often used to validate data before the record is deleted. For example, in the following code snippet, the beforeDelete method prevents deletion of a User instance with a particular user name.

public class User extends Record
{
    ...

    // Executes before deleting instance.
   public void beforeDelete()
   {

     if (this.getName().equals("Larry Johnson"))
     {
         State state = getState();
         Database db = state.getDatabase();
         String message = "User " + this.getLabel() + " cannot be deleted";
         throw new DatabaseException(state.getDatabase(), message);
     }
   }
}
2: Delete operation to database
Dari deletes the record directly from the underlying database implementation.
3: afterDelete
Use for processing after the object is deleted from the database.