Spring Boot - JPA

Let’s learn how to use Java Persistence API(JPA) to map objects to relational databases using an example project.

Maven dependency

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>

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

spring-boot-starter-data-jpa contains the following dependencies:

  • Hibernate: One of the most popular JPA implementations.
  • Spring ORMs: Core ORM support from the Spring Framework.
  • Spring Data JPA: Makes it easy to implement JPA-based repositories.

Datasource

application.properties - configure data source and related settings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# data source
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:testdb
# spring.datasource.username=sa
# spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

# Console access: http://localhost:8080/h2-console
spring.h2.console.enabled=true

spring.jpa.hibernate.ddl-auto=none

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
spring.jpa.properties.hibernate.format_sql=true

schema.sql - H2 SQL Script to create Database

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TABLE `user` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`enabled` boolean,
`created` date,
PRIMARY KEY (`id`)
);

CREATE TABLE `role` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);

CREATE TABLE `user_role` (
`user_id` bigint(11) NOT NULL,
`role_id` bigint(11) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES user(`id`),
FOREIGN KEY (`role_id`) REFERENCES role(`id`)
);

data.sql

1
2
3
4
5
6
7
insert into user(`username`, `password`, `enabled`, `created`) values( 'user', 'pass', true, '2019-01-02');

insert into role(`name`) values('user');
insert into role(`name`) values('admin');

insert into user_role(`user_id`, `role_id`) values(1, 1);
insert into user_role(`user_id`, `role_id`) values(1, 2);

Entity

Traditionally, JPA entities are specified in persistence.xml file. With Spring Boot, you can define entity with annotations. classes annotated with @Entity, @Embeddable or @MappedSuperclass are used as entity.

User Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

@Entity
@Table(name = "user")
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;

private String username;

private String password;

private boolean enabled;

private LocalDate created;

@ManyToMany(cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
@JoinTable(
name="user_role",
joinColumns = {@JoinColumn(name="user_id")},
inverseJoinColumns = {@JoinColumn(name="role_id")}
)
Set<Role> roles = new HashSet<>();

@Override
public String toString() {
return new ToStringBuilder(this).append(username).append(enabled).toString();
}

@Override
public int hashCode() {
return new HashCodeBuilder().append(username).append(enabled).toHashCode();
}

@Override
public boolean equals(Object obj) {
if (obj == null) { return false; }
if (obj == this) { return true; }
if (obj.getClass() != getClass()) {
return false;
}
User rhs = (User) obj;
return new EqualsBuilder()
.append(id, rhs.id)
.append(username, rhs.username)
.append(enabled, rhs.enabled)
.isEquals();
}
}

Role Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Entity
@Table(name = "role")
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;

private String name;

@ManyToMany(mappedBy="roles", fetch = FetchType.LAZY)
@JsonIgnore
private Set<User> users = new HashSet<>();

@Override
public String toString() {
return new ToStringBuilder(this).append(name).toString();
}

@Override
public int hashCode() {
return new HashCodeBuilder().append(name).toHashCode();
}

@Override
public boolean equals(Object obj) {
if (obj == null) { return false; }
if (obj == this) { return true; }
if (obj.getClass() != getClass()) {
return false;
}
Role rhs = (Role) obj;
return new EqualsBuilder()
.append(id, rhs.id)
.append(name, rhs.name)
.isEquals();
}
}

Here we define a Many to Many relationship suing @ManyToMany, @JoinTable and @JoinColumn annotations. User entity owns the relationship be cuase it has @JoinTable annotation.

The Role entity has mappedBy attribute to indicate the relationship is mapped by User entity’s roles collection.

To learn more about creating One-to-One, One-to-Many and Many-to-Many relationship in JPA. see the following post:

Solving JSON recursive dependency

When using Jackson to serialize JPA Entities with one-to-many or many-to-many relationship, you can easily get infinite recursion problem because they are bidirection relation. To avoid the error, add @JsonIdentityInfo annotation to tell Jackson the iendity info.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
@Table(name = "user")
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class User

//......
}

@Entity
@Table(name = "role")
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Role {
//......
}

@JsonIdentityInfo is not the only way to solve this problem, you can also use

  • @JsonBackReference and @JsonManagedReference to handle the relationship
  • @JsonIgnore or @JsonIgnoreProperties annotation to ignore the serialization of one or more property.

Refer to this post to see how they are used to handle bidirectional relationship in Json: https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion

Used with Lombok

Like Json serialization, you can also get stackoverflow when using auto generated methods from Lombok.

It is very common to use lombok to generate a class’s getters, setters etc to reduce boilerplate code. However, the default implementation for toString, equals and hashCode method will cause recursion for a many-to-many relationship. To solve this problem, either stop using @Data annotation to generate code or explicitly define toString, hashCode and equals method.

The User and Role entity class both explicitely define toString, hashCode and equals method to avoid recusion.

JpaRepository

JpaRepository extends CrudRepository and PagingAndSortingRepository. CrudRepository provides methods for generic CRUD operations. PagingAndSortingRepository extends CrudRepository and provides additional methods to retrieve entities using pagination and sorting.

Class extends JpaRepository interface will have all the basic CRUB methods defined and implemented.

You can also add customize methods and queries to the interface. Spring Data will analyze the methods defined by the interfaces and tries to automatically generate queries from the method name.

Use the @Query annotation in Spring Data JPA to execute both JPQL and native SQL queries. For more on JPQL, see

For more information on various ways to query in JPA, see Spring Data JPA Query Methods

1
2
3
4
5
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("from User where created between :start and :end")
List<User> findByDateRange(@Param("start") LocalDate start, @Param("end") LocalDate end);
}

Using Jpa Entity

You can now autowire the JpaRepository to service class and use it.

1
2
3
4
5
6
7
8
9
10
@Service
public class UserService {
@Autowired
private UserRepository userRepository;

@Transactional
public User findByUsername(String username){
return userRepository.findByUsername(username).orElse(null);
}
}

About Lazy Loading

You can set the loading strategy by setting fetch attribute. The value can be fetchType.LAZY or fetchType.EAGER.

If you encounter LazyInitializationException when using lazy loading, add @Transactional to the service method because session is needed for lazy loading otherwise a lazyinitializationexception will be thrown.

1
2
3
4
5
Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.xinghua24.bookmark.entity.User.roles, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:606) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:585) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:149) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]

For more on Lazy loading, see Eager/Lazy Loading In Hibernate by baeldung

Transaction

If we use spring-boot-starter-jdbc or spring-boot-starter-data-jpa, Spring Boot will turn on @EnableTransactionManagement by default. You can use @Transaction to declare transaction at class level or method level. @Transaction by default rolls back on all transactions. You can use rollbackOn and dontRollbackOn attribute to customize the exceptions to rollback.

Service method with @Transactional annotation

1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserService {
// ...
@Transactional(rollbackOn = IllegalArgumentException.class)
public void testTransaction() {
User user = userRepo.findById(1L).orElse(null);
user.setUsername("Name shouldn't be changed!!!!");
userRepo.save(user);
throw new IllegalArgumentException("ILLEGAL OPERATION");
}
}

When this method is called, even if the repository saves the change, the exception will cause the transaction to roll back and undo the previous change.

Note that @Transaction should be apply to service class(with @Service annotation). Don’t apply this annotation to multiple layers of the application. Also, the method should be declare public for the transaction to work.

generate-ddl vs ddl-auto

JPA can generate ddl to set up database on startup. There are 2 properties you can set in configuration file.

  • spring.jpa.generate-ddl (boolean) switches the feature on and off and is vendor independent.
  • spring.jpa.hibernate.ddl-auto (enum) is a Hibernate feature that controls the behavior in a more fine-grained way. See below for more detail.

ddl-auto can take the following options:

  • none
  • validate - validate only, not changing the schema
  • update - update the schema
  • create - create the schema
  • create-drop - create the schema, drop at the end of the session

ddl-auto and generate-ddl should not be used in production.

Reference

Furthur research on Spring Data, see Baeldung’s Spring Data Topic https://www.baeldung.com/category/persistence/spring-persistence/spring-data/

Source Code: https://github.com/xinghua24/SpringBootExamples/tree/master/jpa