23 February 2021

The code for this project can be found in this GitLab repository

Setting up the project

For the simplicity of this example, we’re not going to use a full-blown database. H2 is a SQL database written entirely in java that can easily be included in application jar files.

We are going to create a Maven project where we setup H2, Hibernate, Lombok and logging.

<dependencies>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>5.4.9.Final</version>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>1.4.200</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.18</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.7</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.7</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.7</version>
    </dependency>
</dependencies>

To allow log4j to properly log messages, we will create a file in src/main/resource/log4j2.xml with the following content:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %highlight{%-5level} %style{%logger{36}}{white} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="info"> <AppenderRef ref="Console"/> </Root>
    </Loggers>
</Configuration>

Data structures and relationships

We will model a chat application that has 3 types of entities: User, Message and Emote. All entities have a unique ID. We will generate this ID with UUID.randomUUID().toString().

User

A user has a name assicoated with him. We will also store if the user is active as a boolean flag.

Message

A message can be sent by one User and has a content (as a String). A message can have multiple emotes associated.

Emotes

An emote is a "reaction" a user can anonymously use on a message. There can be more than one emote per message and more than one messages where one emote is used.

Relationships

There is a many-to-one relationship between a user and his messages and a many-to-many relationship between a message and an emote. For this example we are going to use bidirectional links (both sides of the relationship reference each other)

To map messages with users, the Message table will contain a reference to the user that send that specific message. To map messages with emotes, we will need an joining table that keep the correspondence.

Here’s a table of the annotations we will have to use:

To\From User Message Emote

User

@ManyToOne @JoinColumn

Message

@OneToMany(mappedBy)

@ManyToMany(mappedBy)

Emote

@ManyToMany @JoinTable

Diagram

Data classes

User

We will use Lombok’s setters and getters to provide access to a user’s properties. 2 constructors are needed: The no argument constructor is needed by Hibernate to be able to construct objects at runtime trough reflection. The second constructor will be used to manually create a new object with a generated ID.

@Entity
@Accessors(chain = true)
@NoArgsConstructor
public class User {

    @Id @Getter
    private String id;

    @Getter @Setter
    private String name;

    @Getter @Setter
    private boolean active;

    @Getter
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private Set<Message> messages;

    public User(String id) {
        this.id = id;
        messages = new HashSet<>();
    }

    /* ... */
}

Message

When mapping a message to a user, we need to specify the name of the column where the user’s id will be stored. This name needs to be the same as specified in User’s @OneToMany mappedBy property.

To link messages and emotes, we are using a join table. We need to specify the name of this table and the name of it’s two columns that map the relationship. joinColumns specified the column name with ID’s we are mapping towards and inverseJoinColumns the column name with ID’s we are mapping from.

@Entity
@Accessors(chain = true)
@NoArgsConstructor
public class Message {

    @Id @Getter
    private String id;

    @Getter @Setter
    private String content;

    @Getter @Setter(AccessLevel.MODULE)
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "user")
    private User user;

    @Getter
    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "messages_emotes",
            joinColumns = @JoinColumn(name = "emote"),
            inverseJoinColumns = @JoinColumn(name = "messages"))
    private List<Emote> emotes;

    public Message(String id) {
        this.id = id;
        this.emotes = new ArrayList<>();
    }

    /* ... */
}

Emote

Emotes are only linked with messages, then annotate the @ManyToMany relationship. The other side of the relationship is already defined in the Message class, so we only need to include write the mappedBy property.

@Entity
@Accessors(chain = true)
@NoArgsConstructor
public class Emote {

    @Id @Getter
    private String id;

    @Getter @Setter
    private String representation;

    @Getter
    @ManyToMany(mappedBy = "emotes", cascade = CascadeType.ALL)
    private Set<Message> messages;

    public Emote(String id) {
        this.id = id;
        messages = new HashSet<>();
    }
}

Bidirectional relationships

To ensure that relationships are correctly specified, we must update both sides of the link. This introduces complexity in application code. To combat, updates will only be accepted from one side and they will be propagated on both sides.

Only allow users to add or remove messages:

/* ... */
public class User {

    /* ... */

    public User addMessage(Message message) {
        messages.add(message);
        message.setUser(this);
        return this;
    }

    public User removeMessage(Message message) {
        messages.remove(message);
        message.setUser(null);
        return this;
    }
}

Only allow messages to manage emotes:

/* ... */
public class Message {

    /* ... */

    public Message addEmote(Emote emote) {
        emotes.add(emote);
        emote.getMessages().add(this);
        return this;
    }

    public Message removeEmote(Emote emote) {
        emotes.remove(emote);
        emote.getMessages().remove(this);
        return this;
    }
}

Data repository

We will need a class that will store out instances. We’re going to create methods for inserting, reading, updating and deleting data.

To make changes to a database, we must use transcations. A transcation is a set of changes to be done to the database. In this example, the transcation implementation is a thin and simple wrapper over Hibernate’s Transcation class. It is not necessarily needed to start a transcation if all we intend to do is to query the database and not modify any data.

WARN: We are not alloed to modify a managed entity outside a transaction!

Hibernate keeps most of the data inside the database, but it also keeps a pool of cached objects in memory. By default, it does not clear this cache automatically, so it can lead to memory leaks. To combat, we will create a method clearCachedEntitied to be called by the application code after an operation ends. The entities are cleared when a transcation ends (or more accurate, we will make it do so), but query operations alone can cause memory leaks if we don’t clear the cache manually.

H2 comes with a built-in database explorer as a web interface that runs in a browser. We can turn on or off this feature with the showExplorer parameter.

We will need to keep a reference to the Hibernate session, against which we will execute the operations. A reference to a Transcation object is also kept. This field is null when there is no transcation active.

public class ChatData {

    /* data */
    private final Session session;
    private Transaction transaction;

    public ChatData(String dbPath, boolean showExplorer);

    /* save new entities */
    public void saveEntity(Object obj);

    /* find any entities */
    public <T> Stream<T> findAll(Class<T> clazz);
    public <T> Optional<T> findById(Class<T> clazz, String id);

    /* find specific entities */
    public Stream<User> findUsersByName(String name);
    public Stream<Message> findMessagesWithWord(String word);
    public Optional<Emote> findEmoteByRepresentation(String repr);

    /* delete entities */
    public <T> Optional<T> deleteEntiy(Class<T> clazz, String id);

    /* transcations */
    public void openTransaction();
    public void closeTransaction();

    /* clear cached entities */
    public void clearCachedEntities();

    /* shutdown the database */
    public void close();
}

Connecting and disconnecting to the database

We will create a Session object, given a path. Here we have the possibility to open a H2 Explorer window. Keep in mind that this needs to be done separately, in another thread.

When disconnecting, ensure that a possibly ongoing transcation is commited to the database.

public ChatData(String dbPath, boolean openExplorer)  {
    Configuration cfg = new Configuration()
            .addAnnotatedClass(User.class)
            .addAnnotatedClass(Message.class)
            .addAnnotatedClass(Emote.class);
    ServiceRegistry sevReg = new StandardServiceRegistryBuilder()
            .applySettings(hibernatePropsH2("jdbc:h2:" + dbPath))
            .build();
    SessionFactory sessionFactory = cfg.buildSessionFactory(sevReg);
    session = sessionFactory.openSession();
    if (openExplorer) {
        new Thread(() -> {
            try {
                org.h2.tools.Server.startWebServer(sevReg.getService(ConnectionProvider.class).getConnection());
            } catch (SQLException exception) {
                exception.printStackTrace();
            }
        }).start();
    }
}

private static Properties hibernatePropsH2(String h2url) {
    Properties hProps = new Properties();
    hProps.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
    hProps.setProperty("hibernate.connection.driver_class", "org.h2.Driver");
    hProps.setProperty("hibernate.connection.url", h2url);
    hProps.setProperty("hibernate.connection.username", "sa");
    hProps.setProperty("hibernate.connection.password", "");
    hProps.setProperty("hibernate.hbm2ddl.auto", "update");
    hProps.setProperty("hibernate.show_sql", "false");
    return hProps;
}

public void close() {
    closeTransaction();
    session.close();
}

Transcation management

Only create a new transcation if none is active, and only commit if there is a transaction.

public void openTransaction() {
    if (transaction == null) {
        transaction = session.beginTransaction();
    }
}

public void closeTransaction() {
    if (transaction != null) {
        transaction.commit();
        transaction = null;
    }
    clearCachedEntities();
}

public void clearCachedEntities() {
    session.clear();
}

Saving and deleting entities

When deleting, we search the database for a given ID and delete the entry.

The application code is responsible for severing relationships with other entities before a delete operation, otherwise all linked objects will be deleted too!
public void saveEntity(Object obj) {
    if (obj instanceof Emote || obj instanceof Message || obj instanceof User) {
        autoTransaction(() -> session.save(obj));
    }
}

public <T> Optional<T> deleteEntiy(Class<T> clazz, String id) {
    T ent = session.find(clazz, id);
    if (ent != null) {
        autoTransaction(() -> session.delete(ent));
        return Optional.of(ent);
    } else return Optional.empty();
}

private void autoTransaction(Runnable action) {
    if (transaction == null) {
        openTransaction();
        action.run();
        closeTransaction();
    } else action.run();
}

Finding entities

We will create two methods for accessing entities: by Id, and bulk acess.

public <T> Optional<T> findById(Class<T> clazz, String id) {
    return Optional.ofNullable(session.find(clazz, id));
}

public <T> Stream<T> findAll(Class<T> clazz) {
    CriteriaQuery<T> cq = session.getCriteriaBuilder().createQuery(clazz);
    return session.createQuery(cq.select(cq.from(clazz))).getResultStream();
}

Finding entities with query

Sometimes it’s needed to find entities that respect a certain critaria that is specified by it’s fields. We could use findAll and iterate over the result, but it would be need to fetch the entire database into memory, at once. Instead, we’ll use an JPQL query.

A query object is first created with parameters (?1), then the parameter is set and the query is executed.

public Stream<User> findUsersByName(String name) {
    TypedQuery<User> query = session.createQuery("SELECT user FROM User AS user WHERE user.name = ?1");
    query.setParameter(1, name);
    return query.getResultStream();
}

public Stream<Message> findMessagesWithWord(String word) {
    TypedQuery<Message> query = session.createQuery("SELECT mess FROM Message AS mess WHERE mess.content LIKE ?1");
    query.setParameter(1, word);
    return query.getResultStream();
}

public Optional<Emote> findEmotebyRepresentation(String repr) {
    TypedQuery<Emote> query = session.createQuery("SELECT em FROM Emote AS em WHERE em.representation = ?1");
    query.setParameter(1, repr);
    return Optional.ofNullable(query.getSingleResult());
}

Usage example

After calling data.closeTransaction, all changes are persisted to the database.

Add some users

User john = new User(UUID.randomUUID().toString());
User foo = new User(UUID.randomUUID().toString());
User bar = new User(UUID.randomUUID().toString());
john.setName("John").setActive(true);
foo.setName("Foo").setActive(true);
bar.setName("Bar").setActive(true);

data.openTransaction();
data.saveEntity(john);
data.saveEntity(foo);
data.saveEntity(bar);
data.closeTransaction();

Add some emotes

data.openTransaction();
Stream.of(":happy:", ":sad:", ":confused:", ":meow:", ":laugh:", ":like:", ":dislike:")
        .map(r -> new Emote(UUID.randomUUID().toString()).setRepresentation(r))
        .forEach(data::saveEntity);
data.closeTransaction();

Set users as inactive

Try to set users John and Booo as inactive

data.openTransaction();
Stream.of("John", "Booo").forEach(uname -> {
    Optional<User> user = data.findUsersByName(uname).findAny();
    if (user.isPresent()) {
        user.get().setActive(false);
        System.out.println("User " + uname + " is now inactive");
    } else {
        System.out.println("No such user: " + uname);
    }
});
data.closeTransaction();

Console output:

User John is now inactive
No such user: Booo

Send some messages and emotes

Note that we do not need to call data.saveEntity on the message objects because we link them to users during a transcation. Hibernate will ensure that all linked objects will be persisted to the database and will become managed, recusrively.

Message fooMsg = new Message(UUID.randomUUID().toString());
Message barMsg = new Message(UUID.randomUUID().toString());
fooMsg.setContent("Hey how are you?");
barMsg.setContent("Fine, you?");

data.openTransaction();
data.findUsersByName("Foo").findAny().ifPresent(u -> u.addMessage(fooMsg));
data.findUsersByName("Bar").findAny().ifPresent(u -> u.addMessage(barMsg));
data.closeTransaction();

Normally, we’d have to use message ID’s but since they are not deterministically assigned in this example, we’ll add emotes to the first message found from a user.

Emote happy = data.findEmoteByRepresentation(":happy:").orElse(null);
Emote like = data.findEmoteByRepresentation(":like:").orElse(null);
Emote meow = data.findEmoteByRepresentation(":meow:").orElse(null);

data.openTransaction();
data.findUsersByName("Foo").findAny().flatMap(u -> u.getMessages()
        .stream().findAny()).ifPresent(m -> m.addEmote(happy));
data.findUsersByName("Bar").findAny().flatMap(u -> u.getMessages()
        .stream().findAny()).ifPresent(m -> m.addEmote(like).addEmote(meow));
data.closeTransaction();

Printing messages

Since we’re not modifying anything, we don’t need a transcation. Still, it’s good practice to clear Hibernate’s the entity cache after, to avoid memory leaks.

String printStr = data.findAll(Message.class)
        .map(m -> m.getUser().getName()
                + ": " + m.getContent()
                + "\n  => "
                + m.getEmotes().stream().map(Emote::getRepresentation).collect(Collectors.joining(", ")))
        .collect(Collectors.joining("\n"));
System.out.println(printStr);
data.clearCachedEntities();

Console output:

Foo: Hey how are you?
  => :happy:
Bar: Fine, you?
  => :like:, :meow:

Extras

SQL String maximum length

When defining a String field, it will be translated in varchar(255) in SQL. To have strings longer than 255, use the @Lob annotation like so:

@Lob
@Getter @Setter
private String longText;

List of non-entity types

It may be necessary to save an List<> of a type that is not @Entity.

In this case we must create a @Converter class that converts the array to a serializable type that can be placed into a database column.

For example, we’re converting a list of internet links (they do not contain the \n chacacter) to a newline-separated string that is stored in the database:

@Converter
public class LinkListConverter implements AttributeConverter<List<String>, String> {

    @Override
    public String convertToDatabaseColumn(List<String> stringList) {
        return stringList != null ? String.join("\n", stringList) : "";
    }

    @Override
    public List<String> convertToEntityAttribute(String string) {
        return string != null ? Arrays.asList(string.split("\n")) : emptyList();
    }
}
@Lob
@Getter @Setter
@Convert(converter = LinkListConverter.class)
private List<String> links;

JAR Packaging

To package the entire app into a JAR file, use the maven shade plugin:

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
        <source>15</source>
        <target>15</target>
    </configuration>
</plugin>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.2.4</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals> <goal>shade</goal> </goals>
            <configuration>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>xyz.lucaci32u4.jpasample.Main</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>


This website is open-source. The source code is available on my GitLab repository

© 2021 | Mixed with Bootstrap v5.0.0 | Baked with JBake v2.6.4