Thursday 29 December 2022

Hibernate 6: map lists using ElementCollection

Sometimes, you may want to deal with the embeddable data associated with an entity.

 

For example,

a.   An employee might have one or more communication addresses

b.   A person might have some alias/nick names.

 

In both the cases, the collection of addresses or collection of nick names do not make any sense outside of employee object. In this case, we can model, these nick names as embeddable collections inside an entity and no need to model these nick names as another entity.

 

Example

@Embeddable
public class NickName {

  private String name;

  .......
  .......
}

@Entity
@Table(name = "employee")
public class Employee {
  @Id
  @Column(name = "employee_id")
  private int id;

  private String name;

  @ElementCollection
  private List<NickName> nickNames;

  ..........
  ..........
}

 

Above snippet generates following DDL.

    create table employee (
       employee_id integer not null,
        name varchar(255),
        primary key (employee_id)
    )

    create table Employee_nickNames (
       Employee_employee_id integer not null,
        name varchar(255)
    )

    alter table if exists Employee_nickNames 
       add constraint FK5ahwp3hworqds1shqyvktsyg7 
       foreign key (Employee_employee_id) 
       references employee

 

Points to remember while using collection types

a. Two entities cannot share a reference to the same collection instance.

b. Collection-valued properties do not support null value semantics.

c. Collections cannot be nested, meaning Hibernate does not support mapping List<List<?>>

 

Find the below working application.

 

Step 1: Create new maven project ‘hibernate-map-lists’.

 

Step 2: Update pom.xml with maven dependencies.

 

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.sample.app</groupId>
	<artifactId>hibernate-map-lists</artifactId>
	<version>1</version>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

		<java.version>15</java.version>
		<maven.compiler.source>${java.version}</maven.compiler.source>
		<maven.compiler.target>${java.version}</maven.compiler.target>

	</properties>

	<dependencies>
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<version>42.4.1</version>
		</dependency>

		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-core</artifactId>
			<version>6.1.2.Final</version>
		</dependency>


	</dependencies>
</project>

 

Step 3: Define NickName class.

 

NickName.java

package com.sample.app.dto;

import jakarta.persistence.Embeddable;

@Embeddable
public class NickName {

	private String name;
	
	public NickName() {}

	public NickName(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String toString() {
		return "NickName [name=" + name + "]";
	}

}

 

Step 4: Define entity class.

 

Employee.java

 

package com.sample.app.entity;

import java.util.List;

import com.sample.app.dto.NickName;

import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "employee")
public class Employee {
	@Id
	@Column(name = "employee_id")
	private int id;

	private String name;

	@ElementCollection
	private List<NickName> nickNames;

	public Employee() {
	}

	public Employee(int id, String name, List<NickName> nickNames) {
		this.id = id;
		this.name = name;
		this.nickNames = nickNames;
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public List<NickName> getNickNames() {
		return nickNames;
	}

	public void setNickNames(List<NickName> nickNames) {
		this.nickNames = nickNames;
	}

	@Override
	public String toString() {
		return "Employee [id=" + id + ", name=" + name + ", nickNames=" + nickNames + "]";
	}

}

 

Step 5: Create hibernate.cfg.xml file under src/main/resources folder.

 

hibernate.cfg.xml

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>

	<session-factory>

		<!-- Database Connection settings -->
		<property name="connection.driver_class">org.postgresql.Driver</property>
		<property name="connection.url">jdbc:postgresql://127.0.0.1:5432/test</property>
		<property name="connection.username">postgres</property>
		<property name="connection.password">postgres</property>

		<!-- Enable the logging of all the generated SQL statements to the console -->
		<property name="show_sql">true</property>

		<!-- Format the generated SQL statement to make it more readable, -->
		<property name="format_sql">true</property>

		<!-- Hibernate will put comments inside all generated SQL statements to 
			hint what’s the generated SQL trying to do -->
		<property name="use_sql_comments">false</property>

		<!-- This property makes Hibernate generate the appropriate SQL for the 
			chosen database. -->
		<property name="dialect">org.hibernate.dialect.PostgreSQLDialect</property>

		<!-- Drop and re-create the database schema on startup -->
		<property name="hbm2ddl.auto">create</property>

		<!-- mappings for annotated classes -->
		<mapping class="com.sample.app.entity.Employee" />

	</session-factory>

</hibernate-configuration>

Step 6: Define main application class.

 

App.java

package com.sample.app;

import java.util.Arrays;
import java.util.List;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;

import com.sample.app.dto.NickName;
import com.sample.app.entity.Employee;

import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;

public class App {
	private static SessionFactory sessionFactory = buildSessionFactory();

	private static <T> List<T> loadAllData(Class<T> clazz, Session session) {
		final CriteriaBuilder builder = session.getCriteriaBuilder();
		final CriteriaQuery<T> criteria = builder.createQuery(clazz);
		criteria.from(clazz);
		return session.createQuery(criteria).getResultList();
	}

	private static SessionFactory buildSessionFactory() {
		try {
			if (sessionFactory == null) {
				StandardServiceRegistry standardRegistry = new StandardServiceRegistryBuilder()
						.configure("hibernate.cfg.xml").build();

				Metadata metaData = new MetadataSources(standardRegistry).getMetadataBuilder().build();

				sessionFactory = metaData.getSessionFactoryBuilder().build();
			}
			return sessionFactory;
		} catch (Throwable ex) {
			throw new ExceptionInInitializerError(ex);
		}
	}

	public static void main(String args[]) {

		try (Session session = sessionFactory.openSession()) {

			session.beginTransaction();
			Employee emp = new Employee(1, "Krishna",
					Arrays.asList(new NickName("Vasu dev"), new NickName("Rishikesha"), new NickName("Keshava"),
							new NickName("Gopala"), new NickName("Nandakumara")));
			session.persist(emp);
			session.flush();
			session.getTransaction().commit();

			List<Employee> employees = loadAllData(Employee.class, session);
			for (Employee emp1 : employees) {
				System.out.println(emp1);
			}

		}

	}
}

Total project structure looks like below.



Run App.java, you will see below messages in the console.

Sep 13, 2022 9:43:24 AM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate ORM core version 6.1.2.Final
Sep 13, 2022 9:43:24 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
WARN: HHH10001002: Using built-in connection pool (not intended for production use)
Sep 13, 2022 9:43:24 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001005: Loaded JDBC driver class: org.postgresql.Driver
Sep 13, 2022 9:43:24 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001012: Connecting with JDBC URL [jdbc:postgresql://127.0.0.1:5432/test]
Sep 13, 2022 9:43:24 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001001: Connection properties: {password=****, user=postgres}
Sep 13, 2022 9:43:24 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001003: Autocommit mode: false
Sep 13, 2022 9:43:24 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PooledConnections <init>
INFO: HHH10001115: Connection pool size: 20 (min=1)
Sep 13, 2022 9:43:24 AM org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl logSelectedDialect
INFO: HHH000400: Using dialect: org.hibernate.dialect.PostgreSQLDialect
Hibernate: 
    
    alter table if exists Employee_nickNames 
       drop constraint if exists FK5ahwp3hworqds1shqyvktsyg7
Sep 13, 2022 9:43:25 AM org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection
INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@37854b34] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode.
Sep 13, 2022 9:43:25 AM org.hibernate.engine.jdbc.spi.SqlExceptionHelper$StandardWarningHandler logWarning
WARN: SQL Warning Code: 0, SQLState: 00000
Sep 13, 2022 9:43:25 AM org.hibernate.engine.jdbc.spi.SqlExceptionHelper$StandardWarningHandler logWarning
WARN: relation "employee_nicknames" does not exist, skipping
Hibernate: 
    
    drop table if exists employee cascade
Sep 13, 2022 9:43:25 AM org.hibernate.engine.jdbc.spi.SqlExceptionHelper$StandardWarningHandler logWarning
WARN: SQL Warning Code: 0, SQLState: 00000
Sep 13, 2022 9:43:25 AM org.hibernate.engine.jdbc.spi.SqlExceptionHelper$StandardWarningHandler logWarning
WARN: table "employee" does not exist, skipping
Hibernate: 
    
    drop table if exists Employee_nickNames cascade
Sep 13, 2022 9:43:25 AM org.hibernate.engine.jdbc.spi.SqlExceptionHelper$StandardWarningHandler logWarning
WARN: SQL Warning Code: 0, SQLState: 00000
Sep 13, 2022 9:43:25 AM org.hibernate.engine.jdbc.spi.SqlExceptionHelper$StandardWarningHandler logWarning
WARN: table "employee_nicknames" does not exist, skipping
Hibernate: 
    
    create table employee (
       employee_id integer not null,
        name varchar(255),
        primary key (employee_id)
    )
Sep 13, 2022 9:43:25 AM org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection
INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@5a8c93] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode.
Hibernate: 
    
    create table Employee_nickNames (
       Employee_employee_id integer not null,
        name varchar(255)
    )
Hibernate: 
    
    alter table if exists Employee_nickNames 
       add constraint FK5ahwp3hworqds1shqyvktsyg7 
       foreign key (Employee_employee_id) 
       references employee
Sep 13, 2022 9:43:25 AM org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator initiateService
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: 
    insert 
    into
        employee
        (name, employee_id) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        Employee_nickNames
        (Employee_employee_id, name) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        Employee_nickNames
        (Employee_employee_id, name) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        Employee_nickNames
        (Employee_employee_id, name) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        Employee_nickNames
        (Employee_employee_id, name) 
    values
        (?, ?)
Hibernate: 
    insert 
    into
        Employee_nickNames
        (Employee_employee_id, name) 
    values
        (?, ?)
Hibernate: 
    select
        e1_0.employee_id,
        e1_0.name 
    from
        employee e1_0
Employee [id=1, name=Krishna, nickNames=[NickName [name=Vasu dev], NickName [name=Rishikesha], NickName [name=Keshava], NickName [name=Gopala], NickName [name=Nandakumara]]]

 

Query database to confirm the DDL.

test=# \d+ employee;
                                                   Table "public.employee"
   Column    |          Type          | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
-------------+------------------------+-----------+----------+---------+----------+-------------+--------------+-------------
 employee_id | integer                |           | not null |         | plain    |             |              | 
 name        | character varying(255) |           |          |         | extended |             |              | 
Indexes:
    "employee_pkey" PRIMARY KEY, btree (employee_id)
Referenced by:
    TABLE "employee_nicknames" CONSTRAINT "fk5ahwp3hworqds1shqyvktsyg7" FOREIGN KEY (employee_employee_id) REFERENCES employee(employee_id)
Access method: heap

test=# 
test=# 
test=# 
test=# 
test=# \d+ employee_nicknames
                                                  Table "public.employee_nicknames"
        Column        |          Type          | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
----------------------+------------------------+-----------+----------+---------+----------+-------------+--------------+-------------
 employee_employee_id | integer                |           | not null |         | plain    |             |              | 
 name                 | character varying(255) |           |          |         | extended |             |              | 
Foreign-key constraints:
    "fk5ahwp3hworqds1shqyvktsyg7" FOREIGN KEY (employee_employee_id) REFERENCES employee(employee_id)
Access method: heap

As you see the definition of ‘employee_nicknames’ table, the ordering of a list is by default not maintained. To maintain the order, you should explicitly specify the jakarta.persistence.OrderColumn annotation.

@ElementCollection
@OrderColumn
private List<NickName> nickNames;

 

Above snippet add an additional 'nicknames_order' column in employee_nicknames table.

test=# \d+ employee_nicknames
                                                  Table "public.employee_nicknames"
        Column        |          Type          | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
----------------------+------------------------+-----------+----------+---------+----------+-------------+--------------+-------------
 employee_employee_id | integer                |           | not null |         | plain    |             |              | 
 name                 | character varying(255) |           |          |         | extended |             |              | 
 nicknames_order      | integer                |           | not null |         | plain    |             |              | 
Indexes:
    "employee_nicknames_pkey" PRIMARY KEY, btree (employee_employee_id, nicknames_order)
Foreign-key constraints:
    "fk5ahwp3hworqds1shqyvktsyg7" FOREIGN KEY (employee_employee_id) REFERENCES employee(employee_id)
Access method: heap

test=# 
test=# 
test=# 
test=# 
test=# SELECT * FROM employee_nicknames;
 employee_employee_id |    name     | nicknames_order 
----------------------+-------------+-----------------
                    1 | Vasu dev    |               0
                    1 | Rishikesha  |               1
                    1 | Keshava     |               2
                    1 | Gopala      |               3
                    1 | Nandakumara |               4
(5 rows)

 

Hibernate stores the order value (nicknames_order) based on the position of element in the list. The element at nickNames [2] is stored with nicknames_order=2 and so on.

 

The default column name for the order is derived from the entity property name, by suffixing _order. Ex: nicknames_order. You can customize this name explicitly by passing the name.

 

@ElementCollection
@OrderColumn(name = "nick_names_sequence")
private List<NickName> nickNames;

Now, a column named nick_names_sequence will be used.

test=# \d+ employee_nicknames
                                                  Table "public.employee_nicknames"
        Column        |          Type          | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
----------------------+------------------------+-----------+----------+---------+----------+-------------+--------------+-------------
 employee_employee_id | integer                |           | not null |         | plain    |             |              | 
 name                 | character varying(255) |           |          |         | extended |             |              | 
 nick_names_sequence  | integer                |           | not null |         | plain    |             |              | 
Indexes:
    "employee_nicknames_pkey" PRIMARY KEY, btree (employee_employee_id, nick_names_sequence)
Foreign-key constraints:
    "fk5ahwp3hworqds1shqyvktsyg7" FOREIGN KEY (employee_employee_id) REFERENCES employee(employee_id)
Access method: heap

You can download this application from this link.


 

Previous                                                    Next                                                    Home

No comments:

Post a Comment