Skip to content

UniqueData

Sammy Aknan edited this page Mar 14, 2025 · 4 revisions

Every data object will need to extend UniqueData. UniqueData objects have a unique id (a UUID). This must be the primary id column in the table. Since the id is generated on the application level, UUID v4 is used to prevent duplicate ids.

A class which extends UniqueData should not have members such as int, List<?>, or other traditional types. Members should only be PersistentValues, CachedValues, References and/or PersistentCollections. Only one instance of the object will exist on any application (using the id for uniqueness), but keep in mind that if there are multiple application instances running, each one of them will contain an instance of the object. When using wrappers such as PersistentValue, not only is the database kept in sync, but all other application instances as well. This may be violated when using extreme caution.

Creating/inserting data

Constructor signature

Every class that extends UniqueData must have a constructor with the following parameters: DataManager dataManager, UUID id. It is recommended to do minimal logic in this constructor. It is also recommended to make it private and use static methods called .create(...) or .prepare(...) instead of calling one of the constructors directly. The constructor will get called reflectively at runtime. If you are doing any logic in the constructor, you should check if the id is null. The id will be null if an only if the DataManager is creating a dummy instance of the class to extract some metadata about it. Dummy instances will not exist in the database and for all intensive purposes you can ignore them as a whole.

This constructor will make a call to UniqueData's constructor, which takes the following parameters: DataManager dataManager, String schema, String table, UUID id

Consider the following class, User:

public class User extends UniqueData {
    private final PersistentValue<String> name = PersistentValue.of(this, String.class, "name");

    private User(DataManager dataManager, UUID id) {
        super(dataManager, "public", "users", id);
    }

    public static User create(String name) {
        User user = new User(Application.getDataManager(), UUID.randomUUID());
        Application.getDataManager().insertAsync(user, user.name.initial(name));

        return user;
    }

    public static User prepare(BatchInsert batch, String name) {
        User user = new User(Application.getDataManager(), UUID.randomUUID());
        batch.add(user, user.name.initial(name));

        return user;
    }
}

The create method will create a new User and insert it asynchronously into the database. The method will return instantly and the User object is ready for use. The prepare method on the other hand does not insert the User into the database, it adds the operation to a batch which will be inserted all at once. A BatchInsert is useful for multiple reasons. See below for more information on BatchInserts.

BatchInserts

Using the User class from above and the Post class from below, consider the following code. Note that the following code is incorrect.

User user = User.create("Noah");
Post post = Post.create(user, "Bla bla bla");

In the create methods, DataManager#insertAsync is called which does an asynchronous insertion. This can be problematic when dealing with foreign keys. In our example, the database will throw an error if the Post gets inserted before the User. This is where a BatchInsert should be used. Although this example will technically work without using a BatchInsert, it is not recommended, since an asynchronous insertion is done. If DataManager#insert was used instead, then it would be acceptable, since that is a synchronous/blocking operation.

Here is the correct code for this example:

BatchInsert batch = Application.getDataManager().batchInsert();
User user = User.prepare(batch, "Noah");
Post post = Post.prepare(batch, user, "Bla bla bla");
batch.insertAsync();

The main benefit of using a BatchInsert is that everything is done in a single transaction. Note that the foreign key definition contains DEFERRABLE INITIALLY DEFERRED. This is to tell the database to enforce the foreign key after the transaction is committed. static-data is useful to avoid doing blocking operations on the main thread, so you should only use insert instead of insertAsync in very specific situations.

Additional notes:

  • After a .prepare(...) method is called but before BatchInsert#insert or BatchInsert#insertAsync is called, accessing the data's PersistentValues, References, or other data wrappers will most likely throw errors.

Post class:

public class Post extends UniqueData {
    private final Reference<User> user = Reference.of(this, User.class, "user_id");
    private final PersistentValue<String> content = PersistentValue.of(this, String.class, "content");

    private Post(DataManager dataManager, UUID id) {
        super(dataManager, "public", "posts", id);
    }
    
    public static Post create(User user, String content) {
        Post post = new Post(Application.getDataManager(), UUID.randomUUID());
        Application.getDataManager().insertAsync(post, post.user.initial(user), post.content.initial(name));

        return post;
    }

    public static Post prepare(BatchInsert batch, User user, String content) {
        Post post = new Post(Application.getDataManager(), UUID.randomUUID());
        batch.add(post, post.user.initial(user), post.content.initial(name));

        return post;
    }
}

Example table definitions:

CREATE TABLE IF NOT EXISTS public.users
(
    id                UUID        NOT NULL PRIMARY KEY,
    name              TEXT        NOT NULL
);

CREATE TABLE IF NOT EXISTS public.posts
(
    id                UUID        NOT NULL PRIMARY KEY,
    user_id           UUID        NOT NULL REFERENCES proxy.users (id) ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
    content           TEXT        NOT NULL
);

Clone this wiki locally