Hibernate @PrimaryKeyJoinColumn Annotation | One-to-One Mapping Example



In this tutorial, we will learn how to implement Hibernate one to one mapping with a shared primary key using @PrimaryKeyJoinColumn annotation.
Hibernate will ensure that it will use a common primary key value in both the tables. This way primary key of Employee can safely be assumed the primary key of Account also.

Employee JPA Entity

Let's create an Employee JPA entity and the account field is annotated with @PrimaryKeyJoinColumn annotation:
package net.javaguides.hibernate.entity.sharedPrimaryKey;

import java.io.Serializable;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;

@Entity(name = "SharedPrimaryKeyEmployeeEntity")
@Table(name = "Employee", uniqueConstraints = {
    @UniqueConstraint(columnNames = "ID"),
    @UniqueConstraint(columnNames = "EMAIL")
})
public class Employee implements Serializable {

    private static final long serialVersionUID = -1798070786993154676 L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID", unique = true, nullable = false)
    private Integer employeeId;

    @Column(name = "EMAIL", unique = true, nullable = false, length = 100)
    private String email;

    @Column(name = "FIRST_NAME", unique = false, nullable = false, length = 100)
    private String firstName;

    @Column(name = "LAST_NAME", unique = false, nullable = false, length = 100)
    private String lastName;

    @OneToOne(cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn
    private Account account;

    public Integer getEmployeeId() {
        return employeeId;
    }

    public void setEmployeeId(Integer employeeId) {
        this.employeeId = employeeId;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Account getAccount() {
        return account;
    }

    public void setAccount(Account account) {
        this.account = account;
    }
}

Account JPA Entity

On the Account JPA entity side, it will remain dependent on the owner entity (Employee) for the mapping.
package net.javaguides.hibernate.entity.sharedPrimaryKey;

import java.io.Serializable;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;

import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;

@Entity(name = "SharedPrimaryKeyAccountEntity")
@Table(name = "ACCOUNT", uniqueConstraints = {
    @UniqueConstraint(columnNames = "ID")
})
public class Account implements Serializable {

    private static final long serialVersionUID = -6790693372846798580 L;

    @Id
    @Column(name = "ID", unique = true, nullable = false)
    @GeneratedValue(generator = "gen")
    @GenericGenerator(name = "gen", strategy = "foreign", parameters = @Parameter(name = "property", value = "employee"))
    private Integer accountId;

    @Column(name = "ACC_NUMBER", unique = true, nullable = false, length = 100)
    private String accountNumber;

    @OneToOne(mappedBy = "account", cascade = CascadeType.ALL)
    private Employee employee;

    public Employee getEmployee() {
        return employee;
    }

    public void setEmployee(Employee employee) {
        this.employee = employee;
    }

    public Integer getAccountId() {
        return accountId;
    }

    public void setAccountId(Integer accountId) {
        this.accountId = accountId;
    }

    public String getAccountNumber() {
        return accountNumber;
    }

    public void setAccountNumber(String accountNumber) {
        this.accountNumber = accountNumber;
    }
}

Create a Hibernate configuration file - hibernate.cfg.xml

The configuration file contains information about the database and mapping file. Conventionally, its name should be hibernate.cfg.xml
Let's create an XML file named hibernate.cfg.xml under the resources folder and write the following code in it.
<!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>
        <!-- JDBC Database connection settings -->
        <property name="connection.driver_class">com.mysql.cj.jdbc.Driver</property>
        <property name="connection.url">jdbc:mysql://localhost:3306/java_demo?useSSL=false</property>
        <property name="connection.username">root</property>
        <property name="connection.password">root</property>
        <!-- JDBC connection pool settings ... using built-in test pool -->
        <property name="connection.pool_size">1</property>
        <!-- Select our SQL dialect -->
        <property name="dialect">org.hibernate.dialect.MySQL5Dialect</property>
        <!-- Echo the SQL to stdout -->
        <property name="show_sql">true</property>
        <!-- Set the current session context -->
        <property name="current_session_context_class">thread</property>
        <!-- Drop and re-create the database schema on startup -->
        <property name="hbm2ddl.auto">create-drop</property>
        <!-- dbcp connection pool configuration -->
        <property name="hibernate.dbcp.initialSize">5</property>
        <property name="hibernate.dbcp.maxTotal">20</property>
        <property name="hibernate.dbcp.maxIdle">10</property>
        <property name="hibernate.dbcp.minIdle">5</property>
        <property name="hibernate.dbcp.maxWaitMillis">-1</property>
        <!--  <mapping class="net.javaguides.hibernate.entity.foreignKeyAsso.Employee"/><mapping class="net.javaguides.hibernate.entity.foreignKeyAsso.Account"/> -->
        <mapping class="net.javaguides.hibernate.entity.sharedPrimaryKey.Employee"/>
        <mapping class="net.javaguides.hibernate.entity.sharedPrimaryKey.Account"/>
        <!--         <mapping class="net.javaguides.hibernate.entity.joinTable.Employee"/><mapping class="net.javaguides.hibernate.entity.joinTable.Account"/> -->
    </session-factory>
</hibernate-configuration>

Hibernate Utility Class - HibernateUtil.java

Create a helper class to bootstrap hibernate SessionFactory. In most Hibernate applications, the SessionFactory should be instantiated once during application initialization. The single instance should then be used by all code in a particular process, and any Session should be created using this single SessionFactory.
The SessionFactory is thread-safe and can be shared and Session is a single-threaded object. 
Let's create a HibernateUtil class to configure singleton SessionFactory and use it throughout the application. The bootstrapping API is quite flexible, but in most cases, it makes the most sense to think of it as a 3 step process:
  • Build the StandardServiceRegistry
  • Build the Metadata
  • Use those 2 to build the SessionFactory
package net.javaguides.hibernate.util;

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;

public class HibernateUtil {
    private static StandardServiceRegistry registry;
    private static SessionFactory sessionFactory;

    public static SessionFactory getSessionFactory() {
        if (sessionFactory == null) {
            try {
                // Create registry
                registry = new StandardServiceRegistryBuilder().configure().build();

                // Create MetadataSources
                MetadataSources sources = new MetadataSources(registry);

                // Create Metadata
                Metadata metadata = sources.getMetadataBuilder().build();

                // Create SessionFactory
                sessionFactory = metadata.getSessionFactoryBuilder().build();

            } catch (Exception e) {
                e.printStackTrace();
                if (registry != null) {
                    StandardServiceRegistryBuilder.destroy(registry);
                }
            }
        }
        return sessionFactory;
    }

    public static void shutdown() {
        if (registry != null) {
            StandardServiceRegistryBuilder.destroy(registry);
        }
    }
}

Create the Test class and Run an Application

package net.javaguides.hibernate.test;

import org.hibernate.Session;


import net.javaguides.hibernate.entity.sharedPrimaryKey.Account;
import net.javaguides.hibernate.entity.sharedPrimaryKey.Employee;
import net.javaguides.hibernate.util.HibernateUtil;

public class TestSharedPrimaryKey {

    public static void main(String[] args) {
        Session session = HibernateUtil.getSessionFactory().openSession();
        session.beginTransaction();

        Account account = new Account();
        account.setAccountNumber("123-345-65454");

        //Add new Employee object
        Employee emp = new Employee();
        emp.setEmail("demo-user@mail.com");
        emp.setFirstName("demo");
        emp.setLastName("user");

        emp.setAccount(account);
        account.setEmployee(emp);
        //Save Employee
        session.save(emp);


        session.getTransaction().commit();
        HibernateUtil.shutdown();
    }
}

Output

The above application will generate the following output:
Dec 27, 2019 7:14:29 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {5.3.7.Final}
Dec 27, 2019 7:14:29 PM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
Dec 27, 2019 7:14:29 PM org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
Dec 27, 2019 7:14:30 PM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
WARN: HHH10001002: Using Hibernate built-in connection pool (not for production use!)
Dec 27, 2019 7:14:30 PM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001005: using driver [com.mysql.cj.jdbc.Driver] at URL [jdbc:mysql://localhost:3306/java_demo?useSSL=false]
Dec 27, 2019 7:14:30 PM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001001: Connection properties: {user=root, password=****}
Dec 27, 2019 7:14:30 PM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001003: Autocommit mode: false
Dec 27, 2019 7:14:30 PM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PooledConnections <init>
INFO: HHH000115: Hibernate connection pool size: 1 (min=1)
Dec 27, 2019 7:14:30 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQL5Dialect
Hibernate: drop table if exists ACCOUNT
Dec 27, 2019 7:14:31 PM org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection
INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@10b3df93] 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: drop table if exists Employee
Hibernate: create table ACCOUNT (ID integer not null, ACC_NUMBER varchar(100) not null, primary key (ID)) engine=MyISAM
Dec 27, 2019 7:14:31 PM org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection
INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@12f3afb5] 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 (ID integer not null auto_increment, EMAIL varchar(100) not null, FIRST_NAME varchar(100) not null, LAST_NAME varchar(100) not null, primary key (ID)) engine=MyISAM
Hibernate: alter table ACCOUNT add constraint UK_nsa1j7vica4ow9xhhkudukb3j unique (ACC_NUMBER)
Hibernate: alter table Employee add constraint UK_ardf0f11mfa6tujs3hflthwdv unique (EMAIL)
Dec 27, 2019 7:14:32 PM org.hibernate.tool.schema.internal.SchemaCreatorImpl applyImportSources
INFO: HHH000476: Executing import script 'org.hibernate.tool.schema.internal.exec.ScriptSourceInputNonExistentImpl@7bd69e82'
Hibernate: insert into Employee (EMAIL, FIRST_NAME, LAST_NAME) values (?, ?, ?)
Hibernate: insert into ACCOUNT (ACC_NUMBER, ID) values (?, ?)
Dec 27, 2019 7:14:32 PM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:mysql://localhost:3306/java_demo?useSSL=false]

Comments