Unidirectional many to one JPA entity mapping with Hibernate
This article gives a detailed overview of a unidirectional many to one JPA entity mapping. You’ll learn the benefits and potential issues associated with such an entity relation mapping. We’ll learn how to set it with JPA annotations and what are the resulting database tables. We’ll also dig directly into SQL statements produced as a result of a many to one mapping.
Code to this example is available right here on Github.
Benefits of many to one JPA mapping
Here’s an overview of many to one entity relation. Lets say we’re dealing with credit cards. Each credit card belongs to only one account and each account can have multiple credit cards attached. Additionally we’ll say that a credit card cannot exist without an account, otherwise it’s not usable. It makes the credit card to account relation mandatory.
With a traditional JDBC in order to fetch credit card’s account you’d need to construct the following query:
[code land=”sql”]
SELECT * FROM ACCOUNT WHERE ID=?
This would probably involve creating an additional method in a Dao class, dealing with a prepared statement and mapping query result to the class instance. Luckily Hibernate can deal with most of the trouble for us, but there are few subtle benefits related to mapping itself:
- With many to one mapping, we’re able to retrieve an account associated with each credit card without additional code
- We can decide to fetch the account eagerly or lazily
- We can decide if the relation is mandatory or not. Mandatory relation will result in Hibernate throwing an exception when we attempt to save a credit card with null account
Many to one JPA mapping risks
Whenever something is done automatically by the framework, we as developers, usually lose some control over it. Many to one mapping is definitely the least problematic of all. Hibernate creators repeatedly say that a whole application can easily be built with just this one type of mapping.
Fetch strategy
Each mapping type has a default fetch strategy. For many to one, it’s an EAGER strategy. This means that every time you ask Hibernate to fetch a CreditCard, it will create an SQL join to fetch the Account data as well.
The rule of thumb, advised fetch strategy is LAZY. With LAZY approach Account information is fetched only when it’s needed (CreditCard’s account field is accessed).
When setting up a many to one entity relation, you should be aware which fetch strategy you’ll need. Let’s assume that in our example we won’t be extensively accessing Account from CreditCard Level so we’ll stick to advised Lazy strategy.
Suboptimal queries
Extensive entity mapping is a great way to sub-optimal queries, N+1 selects or Carthesian product problems. Luckily many to one is the easiest and least harmful out of all the JPA entity relation mappings. Making this mapping bi-directional can easily result in N+1 selects if you’re not careful. It’s safe to say that minimizing the number of entity mappings is a safe way to good ORM performance.
Take a look at previous article, to understand how to detect query issues using logging.
Example many to one JPA mapping
Here’s the code depicting the Account and CreditCard example.
@Entity
@org.hibernate.annotations.Immutable
public class CreditCard {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String cardNumber;
private LocalDate expirationDate;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "ACCOUNT_ID", nullable = false)
private Account account;
//Package private constructors
//Getters
//Static factory method
}
@Entity
@Immutable
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String accountNumber;
private String accountOwnerName;
//Package private constructors
//Getters
//Static factory method
}
Since CreditCard cannot exist without its Account, it contains an account property. @ManyToOne annotation describes the relation between CreditCard and Account classes.
Account property is also annotated with a @JoinColumn annotation to specify the name of the foreign key column in CREDITCARD table.
The above code results in the following data definition statements
17:55:54.913 [main] DEBUG org.hibernate.SQL - create table Account (id bigint generated by default as identity, accountNumber varchar(255), accountOwnerName varchar(255), primary key (id))
17:55:54.929 [main] DEBUG org.hibernate.SQL - create table CreditCard (id bigint generated by default as identity, cardNumber varchar(255), expirationDate date, ACCOUNT_ID bigint not null, primary key (id))
17:55:54.929 [main] DEBUG org.hibernate.SQL - alter table CreditCard add constraint FK7xw8ejd7amnijxlwqrw8mreyi foreign key (ACCOUNT_ID) references Account
As a result, the following tables are created. Just as we specified using @JoinColumn annotation, CREDIT_CARD table carries a foreign key constraint to the ACCOUNT table.
SQL queries generated by Hibernate with many to one mapping
Here are the queries hibernate generates when persisting both Account and a CreditCard.
@Test
public void whenPersistCreditCard_thenIdAssigned() {
Account account = Account.newInstance("12345", "James Bone");
CreditCard creditCard = CreditCard.newInstance("123-456", LocalDate.now(), account);
accountDao.persist(account);
creditCardDao.persist(creditCard);
Assert.assertNotNull(creditCard.getId());
}
18:24:43.939 [main] DEBUG com.allaroundjava.dao.BaseDao - Persisting class com.allaroundjava.model.Account
18:24:44.025 [main] DEBUG org.hibernate.SQL - insert into Account (id, accountNumber, accountOwnerName) values (null, ?, ?)
18:24:44.025 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - [12345]
18:24:44.025 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - [James Bone]
18:24:44.042 [main] DEBUG com.allaroundjava.dao.BaseDao - Persisting class com.allaroundjava.model.CreditCard
18:24:44.042 [main] DEBUG org.hibernate.SQL - insert into CreditCard (id, ACCOUNT_ID, cardNumber, expirationDate) values (null, ?, ?, ?)
18:24:44.042 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - [1]
18:24:44.042 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - [123-456]
18:24:44.042 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [DATE] - [2019-01-08]
And here’s how Hibernate handles fetching CreditCard and then lazily loading Account.
@Test
public void whenRetrieveById_thenCorrectObjectReceived() {
Account account = Account.newInstance("6789", "Henry Towel");
CreditCard creditCard = CreditCard.newInstance("987-7654", LocalDate.now(), account);
accountDao.persist(account);
creditCardDao.persist(creditCard);
Optional creditCardOptional = creditCardDao.getById(creditCard.getId());
Assert.assertTrue(creditCardOptional.isPresent());
Account fetchedAccount = creditCardOptional.get().getAccount();
Assert.assertEquals(account.getAccountNumber(), fetchedAccount.getAccountNumber());
}
18:31:02.717 [main] DEBUG com.allaroundjava.dao.BaseDao - Persisting class com.allaroundjava.model.Account
18:31:02.799 [main] DEBUG org.hibernate.SQL - insert into Account (id, accountNumber, accountOwnerName) values (null, ?, ?)
18:31:02.816 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - [6789]
18:31:02.816 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - [Henry Towel]
18:31:02.817 [main] DEBUG com.allaroundjava.dao.BaseDao - Persisting class com.allaroundjava.model.CreditCard
18:31:02.817 [main] DEBUG org.hibernate.SQL - insert into CreditCard (id, ACCOUNT_ID, cardNumber, expirationDate) values (null, ?, ?, ?)
18:31:02.817 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - [1]
18:31:02.817 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [2] as [VARCHAR] - [987-7654]
18:31:02.817 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [3] as [DATE] - [2019-01-08]
18:31:02.817 [main] DEBUG com.allaroundjava.dao.BaseDao - Fetching class com.allaroundjava.dao.CreditCardDao with id 1 from database
18:31:02.844 [main] DEBUG org.hibernate.SQL - select creditcard0_.id as id1_1_0_, creditcard0_.ACCOUNT_ID as ACCOUNT_4_1_0_, creditcard0_.cardNumber as cardNumb2_1_0_, creditcard0_.expirationDate as expirati3_1_0_ from CreditCard creditcard0_ where creditcard0_.id=?
18:31:02.845 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - [1]
18:31:02.850 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - extracted value ([ACCOUNT_4_1_0_] : [BIGINT]) - [1]
18:31:02.851 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - extracted value ([cardNumb2_1_0_] : [VARCHAR]) - [987-7654]
18:31:02.851 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - extracted value ([expirati3_1_0_] : [DATE]) - [2019-01-08]
18:31:02.857 [main] DEBUG org.hibernate.SQL - select account0_.id as id1_0_0_, account0_.accountNumber as accountN2_0_0_, account0_.accountOwnerName as accountO3_0_0_ from Account account0_ where account0_.id=?
18:31:02.858 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - [1]
18:31:02.859 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - extracted value ([accountN2_0_0_] : [VARCHAR]) - [6789]
18:31:02.859 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - extracted value ([accountO3_0_0_] : [VARCHAR]) - [Henry Towel]
Please bear in mind that the select query for Account is fired when executing fetchedAccount.getAccountNumber(). Before that, no interaction with the database happens in relation to Account. If the fetch type was EAGER in CreditCard, the Account information would be fetched with a join clause in the single query.
- Take a look at an extension to this article describing a Bi-directional Many To One mapping.
- Explore how to configure a One To One mapping.
- Here’s how to extract Hibernate queries to log file.