JPA OneToMany Relationship

By | August 5, 2018

1. Introduction.

The OneToMany relationship is the most common JPA association and foreign key is controlled by the child-side directly including unidirectional and bidirectional association. This note presents how to configure bidirectional @OneToMany association

2. Bidirectional ManyToOne / OneToMany Relationships.

Assuming that:

    • Event entity references a single instance of User entity
    • User entity references a collection of Event entity

In database, table Event contains a foreign key to table User. The foreign key column has the same type as the primary key of table User.

A demo example is written by Spring Boot, JPA, Liquibase, HikariDataSource, PostgresSQL, and Projectlombok (no need to write another getter or equals method,..).

2.1 Implementation.

Create two domain entities: UserEntity and EventEntity
src/main/java/com/example/onetomany/domain/UserEntity.java

package com.example.onetomany.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Type;
import org.springframework.util.CollectionUtils;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

@Data
@Entity
@Table(name = "user_entity")
@Builder
@EqualsAndHashCode(of = {"id"})
@ToString(of = {"id","email", "firstName", "lastName"})
@NoArgsConstructor
@AllArgsConstructor
public class UserEntity implements Serializable {

    @Id
    @Type(type = "pg-uuid")
    @GenericGenerator(name = "uuid-gen", strategy = "uuid2")
    @GeneratedValue(generator = "uuid-gen")
    @Column(name = "user_id")
    private UUID id;

    private String email;
    private String firstName;
    private String lastName;


    @OneToMany(mappedBy = "user" , cascade = CascadeType.ALL)
    private Set<EventEntity> eventEntities = new HashSet<>();

    public void addEvent(EventEntity eventEntity) {
        if (CollectionUtils.isEmpty(eventEntities)) {
            eventEntities = new HashSet<>();
        }
        eventEntities.add(eventEntity);
        eventEntity.setUser(this);
    }

    public void removeEvent(EventEntity eventEntity) {
        eventEntities.remove(eventEntity);
        eventEntity.setUser(null);
    }

}

Check eventEntities is not null due to Caused by: java.lang.NullPointerException: null

src/main/java/com/example/onetomany/domain/EventEntity.java

package com.example.onetomany.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Type;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import java.io.Serializable;
import java.time.Instant;
import java.util.UUID;

@Data
@Entity
@Table(name = "event_entity")
@Builder
@EqualsAndHashCode(of = {"id"})
@ToString(of = {"id","description", "creationTime"})
@NoArgsConstructor
@AllArgsConstructor
public class EventEntity implements Serializable {

    @Id
    @Type(type = "pg-uuid")
    @GenericGenerator(name = "uuid-gen", strategy = "uuid2")
    @GeneratedValue(generator = "uuid-gen")
    @Column(name = "event_id")
    private UUID id;

    private String description;

    @Column(columnDefinition = "TIMESTAMP WITH TIME ZONE NOT NULL")
    private Instant creationTime;

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "user_id")
    private UserEntity user;

}

For security reasons, primary key should be unpredictable so that UUID is a good candidate, although performance issue (not indexing UUID) is slow in comparison with Long, Integer.

Add a liquibase file base on the domains above
resources/db-changelog.xml

<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">

    <changeSet id="create_user_entity" author="khiem">
        <createTable tableName="user_entity">
            <column name="user_id" type="UUID">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="email" type="VARCHAR(255)"/>
            <column name="first_name" type="VARCHAR(255)"/>
            <column name="last_name" type="VARCHAR(255)"/>
        </createTable>
    </changeSet>
    <changeSet id="create_event_entity" author="khiem">
        <createTable tableName="event_entity">
            <column name="event_id" type="UUID">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="description" type="VARCHAR(255)"/>
            <column name="creation_time" type="TIMESTAMP WITH TIME ZONE">
                <constraints nullable="false"/>
            </column>
            <column name="user_id" type="UUID" />
        </createTable>
    </changeSet>
    <changeSet author="khiem" id="addForeignKeyConstraint-user-event">
        <addForeignKeyConstraint baseColumnNames="user_id"
                                 baseTableName="event_entity"
                                 constraintName="fk_user_event"
                                 deferrable="false"
                                 initiallyDeferred="false"
                                 onDelete="NO ACTION"
                                 onUpdate="NO ACTION"
                                 referencedColumnNames="user_id" referencedTableName="user_entity"/>
    </changeSet>
</databaseChangeLog>

Simple userReporsitory to handle insert or update user data.
src/main/java/com/example/onetomany/repository/UserRepository.java

package com.example.onetomany.repository;

import com.example.onetomany.domain.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;
import java.util.UUID;

public interface UserRepository extends JpaRepository<UserEntity, UUID> {
    Optional<UserEntity> findByEmail(String email);
}

2.2 Test.

Using maven to package and deploy the application. In practice, it should be separate different environments such as development, staging, and production.

mvn clean package -DskipTests -Pdev

Then run it by command:

java -jar target/onetomany-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev

Try to insert a user and user’s event log:

if (!userRepository.findByEmail("test1@domain.com").isPresent()) {

            UserEntity userEntity = userRepository.save(UserEntity.builder()
                    .email("test1@domain.com")
                    .firstName("Richard")
                    .lastName("Thaler")
                    .build());

            userEntity.addEvent(EventEntity.builder()
                    .description("Login successfully")
                    .creationTime(Instant.now())
                    .build());

            userRepository.save(userEntity);

}

Check result in database. FK of EventEntity is the same value of PK of UserEntity

user_id                               | email           | first_name | last_name
46f532d4-f4b7-44d1-b273-271b800332a2    test1@domain.com  Richard      Thaler

event_id                            | description       | creation_time             | user_id
41e52671-6903-4bbf-b138-cb6297990b21  Login successfully 2018-08-06 03:48:41.747+07  46f532d4-f4b7-44d1-b273-271b800332a2

Retrieve all events

        Optional.ofNullable(userRepository.findByEmail("test1@domain.com")).ifPresent(
                t -> t.get().getEventEntities().forEach(System.out::println)
        );

Output: EventEntity(id=41e52671-6903-4bbf-b138-cb6297990b21, description=Login successfully, creationTime=2018-08-05T20:48:41.747Z)

Remove event logs from the user:

        UserEntity userEntity = userRepository.findByEmail("test1@domain.com")
                .orElseThrow(() -> new EntityNotFoundException());

        Set<EventEntity> eventEntities = userEntity.getEventEntities();

        if (!CollectionUtils.isEmpty(eventEntities)) {
            Iterator<EventEntity> iterator = eventEntities.iterator();
            while (iterator.hasNext()) {
                userEntity.removeEvent(iterator.next());
            }
        }
        userRepository.save(userEntity);

Check event log in database. Actually, PK of a event entity is deleted (no reference to UserEntity).

event_id                            | description       | creation_time             | user_id
41e52671-6903-4bbf-b138-cb6297990b21  Login successfully 2018-08-06 03:48:41.747+07   NULL

2.3 Example logs.

Some issues happened and fixed when running the example as following:
1. Method org.postgresql.jdbc.PgConnection.createClob() is not yet implemented

Caused by: java.sql.SQLFeatureNotSupportedException: Method org.postgresql.jdbc.PgConnection.createClob() is not yet implemented.

Fixed by adding: spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation: true to application.yml file

2. Spring boot app always shutdown immediately after starting

[restartedMain] com.example.onetomany.Application   : Started Application in 4.337 seconds (JVM running for 4.851)
[Thread-9] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@2e75d95d: startup date [Sun Aug 05 22:34:52 ICT 2018]; root of context hierarchy
[Thread-9] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
[Thread-9] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans
[Thread-9] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
[Thread-9] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
[Thread-9] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.223 s
[INFO] Finished at: 2018-08-05T22:34:56+07:00
[INFO] Final Memory: 26M/437M
[INFO] ------------------------------------------------------------------------

Fixed by adding a dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3. [WARNING] The requested profile “dev” could not be activated because it does not exist.
This is because of missing development profile. It should be defined in pom.xml. For example:

<profiles>
		<profile>
			<id>dev</id>
			<activation>
				<activeByDefault>true</activeByDefault>
			</activation>
			<dependencies>
				<dependency>
					<groupId>org.springframework.boot</groupId>
					<artifactId>spring-boot-starter-undertow</artifactId>
				</dependency>
				<dependency>
					<groupId>org.springframework.boot</groupId>
					<artifactId>spring-boot-devtools</artifactId>
					<optional>true</optional>
				</dependency>
			</dependencies>
			<build>
				<plugins>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-compiler-plugin</artifactId>
						<version>${maven-compiler-plugin.version}</version>
						<configuration>
							<source>${java.version}</source>
							<target>${java.version}</target>
						</configuration>
					</plugin>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-war-plugin</artifactId>
						<configuration>
						</configuration>
					</plugin>
				</plugins>
			</build>
			<properties>
				<!-- log configuration -->
				<logback.loglevel>DEBUG</logback.loglevel>
				<!-- default Spring profiles -->
				<spring.profiles.active>dev</spring.profiles.active>
			</properties>
		</profile>
	</profiles>

Code from github

References:

  • Oracle, JSR 338: JavaTM Persistence API, Version 2.1 Final Release.
  • Vlad Mihalcea, High-Performance Java Persistence, 2015-2016

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.