JPA(Jakarta Persistence API) lets us map objects to a relational database. Formerly it was known as Java Persistence API. JPA is a specification of data persistence in Java applications. In this blog, I'm going to write about basic configurations and entities.
Configure the Datasource
Spring boot offers auto-configuration for some in-memory embedded databases. Those are H2, HSQL and Derby. Those are good for development. But in a complex application where we need complex queries needed, these in-memory databases need more testing scope. This rises complexity. That's why I always avoid in-memory databases. But if you want to use those, we only need to include the dependency for those databases.
First, add the required dependencies. I've used the Spring initializer for code generation.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Now start the database. I've used docker and PostgreSQL for local development.
#!/bin/bash
docker network create pg_network1
docker run --name local-pg-1 \
--network pg_network1 \
-p 5432:5432 \
-e POSTGRES_USER=root \
-e POSTGRES_PASSWORD=root \
-d postgres
docker run --name local-pgadmin-1 \
--network pg_network1 \
-p 3000:80 \
-e PGADMIN_DEFAULT_EMAIL=admin@mail.com \
-e PGADMIN_DEFAULT_PASSWORD=root \
-d dpage/pgadmin4
Now, we have to configure the data source. In spring boot, we have to configure it in external configuration properties in application.properties or application.yaml or Profile-specific properties file.
spring.datasource.url=jdbc:postgresql://localhost/test
spring.datasource.username=root
spring.datasource.password=root
Note: At least we need to specify the URL otherwise spring boot will auto-configure an embedded database. The driver class will automatically be loaded from the database URL, otherwise, we can specify the property,
spring.datasource.driver-class-name=com.postgresql.jdbc.Driver
We may need more additional properties for development purposes only.
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
There is another important property ddl-auto. Using create, it will drop all the existing tables and create new ones.
spring.jpa.hibernate.ddl-auto=create
Spring boot supports many Connection pools algorithms. The HikariCP is the recommended one for its performance and concurrency. We will automatically get the HikariCP dependency as we've used the spring-boot-starter-data-jpa
dependency. The spring-boot-starter-data-jpa
also provides
Hibernate
Spring data jpa
Spring ORM
Note: If we want another JPA implementation provider, for example, MyBatis. We need another starter dependency. For MyBatis,
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.1'
JPA Entity classes
Entities are just POJO classes. An entity class represents a database table. A POJO class contains all required properties, a no-arg constructor(mandatory) and getters and setters of all the required properties. To turn a POJO class into an entity, we need the @Entity
annotation.
An entity class must have a primary key field.
An entity class must have the no-arg constructor.
An entity class must not be declared as final because some JPA implementations may create subclasses from it.
JPA implementations(e.g. Hibernate) create tables in databases using the entity class name. The entity name comes from the Entity class name.
@Entity
@NoArgsConstructor
@Getter
@Setter
public class AppUser {
@Id
private int id;
}
N.B: In PostgreSQL, the user is a reserved keyword. If we use this as a table name, we will get an exception.
Also, we can define the entity name explicitly like this,
@Entity(name = "m_user")
If we don't want the entity name and the table name to be the same then we have to use the @Table
annotation.
@Table(name = "tbl_user")
We can also define a schema in the @Table
annotation.
@Table(name = "tbl_key", schema="hello")
@Id
annotation
This annotation defines the primary key. There are 4 types of generators along with the non-generator type.
Non-generated: The user is responsible to provide the id. As a primary key must be unique, the user must provide a unique value. If we use the
@Id
then that is a default non-generator type. The type of the value can be the Java primitive or wrapper types.AUTO: The JPA implementation provider will generate the value based on the type of the primary key attribute. For numeric values, the generation strategy is the sequence for almost all cases. Few cases it selects the table generation strategy. For UUID values, it uses UUIDGenerator.
@Id @GeneratedValue(strategy = GenerationType.AUTO) private int id;
IDENTITY: In this case, the database is responsible to generate the Id. The database uses the auto-increment method for generating keys. This strategy disables batch operations because persistence providers have to rely on the database for the primary key values.
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id;
SEQUENCE: This is the recommended type. Both the JPA implementation provider and database are responsible to generate the values. If we don't provide additional information, the JPA implementation provider(e.g. Hibernate) uses its default sequence for the next value.
@Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private int id;
But we can use an additional database sequence. Hibernate will create the entity table first and then create the relevant sequence.
@Id @GeneratedValue( strategy = GenerationType.SEQUENCE, generator="user_generator" ) @SequenceGenerator( name = "user_generator", sequenceName = "user_seq", initialValue = 100, allocationSize = 20 ) private int id;
Here, the generator in
@GeneratedValue
and the name in@SequenceGenerator
are the same generator objects name. In@SequenceGenrator
, the name is the required field and others are optional.sequenceName is the database sequence name if we need a different name.
initialValue = 100 means, the sequence starts from 100.
allocationSize = 20 means, in this case, the first sequence is 100, the next is 120, then 140 and so on.
We can also keep this sequence in the different schema using the schema.
TABLE: Like SEQUENCE, this generator uses a table instead of a sequence for storing the keys. As we directly use a table, it is transaction based. It uses a pessimistic locking mechanism. The sequence will be calculated at a separate database transaction. Either it will open a new connection pool or suspend the current transaction and resume it after the sequence is generated. Either way, those operations come at a high cost.
@Id @GeneratedValue( strategy = GenerationType.TABLE, generator="user_generator" ) @TableGenerator( name = "user_generator", table = "user_seq", pkColumnName = "seq_id", valueColumnName = "seq_value" ) private int id;
Custom Generator
Also, we can use custom generators. We can do it by implementing the IdentifierGenerator interface.
public class EmployeeIdGenerator implements IdentifierGenerator, Configurable {
String prefix = "emp";
@Override
public Object generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
int max = session.createQuery("SELECT id FROM employee", Employee.class)
.stream()
.mapToInt(Employee::getId)
.max()
.orElse(0);
return prefix.concat("-"+max);
}
@Override
public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
prefix = params.getProperty("prefix");
}
}
In this example, we've collected the max primary key and added a string(emp-) at the beginning. In the configure method, we catch the prefix supplied from the entity class. Now we can use this generator like this.
@Id
@GeneratedValue(generator = "emp_generator")
@GenericGenerator(
name = "emp_generator",
strategy = "com.leeonscoding.JPAEntityExample.models.EmployeeIdGenerator",
parameters = @Parameter(name = "prefix", value = "emp")
)
private int id;
Composite Keys
In Hibernate, composite keys have several conditions
Must use @EmbeddedId or @IdClass annotations
Must implement equals() and hashCode() methods
@EmbeddedId example
@Embeddable
@Data
public class OrderPK {
private int orderId;
private int productId;
}
@NoArgsConstructor
@Getter
@Setter
@Entity
public class OrderEntry {
@EmbeddedId
private OrderPK id;
}
The same as the @EmbeddedId, but a different approach.
@Embeddable
@Data
public class SupportPK {
private int customerId;
private int ticketId;
}
@NoArgsConstructor
@Getter
@Setter
@Entity
@IdClass(SupportPK.class)
public class Support {
@Id
private int customerId;
@Id
private int ticketId;
}
Column annotation
We can define a column's name, length, nullability, uniqueness etc. If we don't specify this annotation, the property name will be the column's name. The others also have default values. For example,
length = 255
nullable = true
unique = false
@Column(name = "email", length = 25, nullable = false, unique = true)
private String email;
@Transient
annotation
If we don't want to persist any field in the database table then we should use this transient annotation.
@Entity
@NoArgsConstructor
@Getter
@Setter
public class AppUser {
@Id
private int id;
@Column(name = "email", length = 25, nullable = false, unique = true)
private String email;
private String firstName;
private String lastName;
// We can do it from firstName and lastName. No need to save in DB
@Transient
private String fullName;
}
Working with Date and times
The java.util.Date
and java.util.Calender
are different types from SQL date and time formats. We need @Temporal annotation for them.
@Temporal(TemporalType.DATE)
private Date createdDate;
@Temporal(TemporalType.TIME)
private Date createdTime;
@Temporal(TemporalType.TIMESTAMP)
private Date createdTimestamp;
@Temporal(TemporalType.DATE)
private Calendar createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Calendar createdTimestamp;
The calendar doesn't support the TIME type.
The Date and Calendar APIs have problems. Those problems are fixed with the new Java 8 Date and Time API in java.time
package.
The java.sql
package has the Date, Time and TimeStamp classes. Those are compatible with SQL Date and Time format as those are JDBC APIs. In those cases, we don't need the @Temporal
annotation. We can directly use those.
private Date createdDate;
private Time createdTime;
private Timestamp createdTimestamp;
These classes aren't recommended.
The recommended is the new Java 8 Date and Time APIs. Those classes also don't need the @Temporal
annotation. These can directly map to the SQL types.
LocalDate
mapped to DateLocalTime and OffsetTime mapped to Time
LocalDateTime, OffsetDateTime and ZonedDateTime mapped to Timestamp
private LocalDate createdDate;
private LocalTime createTime;
private OffsetTime pauseTime;
private LocalDateTime createdDateTime;
private OffsetDateTime buyDateTime;
private ZonedDateTime shippingTimestamp;
Enum type
We can persist the Java enum
enum UserType {
GUEST, REGISTERED
}
@Entity
@NoArgsConstructor
@Getter
@Setter
public class AppUser {
@Id
@GeneratedValue(
strategy = GenerationType.TABLE,
generator="user_generator"
)
@TableGenerator(
name = "user_generator",
table = "user_seq",
pkColumnName = "seq_id",
valueColumnName = "seq_value"
)
private int id;
@Column(name = "email", length = 25, nullable = false, unique = true)
private String email;
private String firstName;
private String lastName;
@Transient
private String fullName;
@Enumerated(value = EnumType.STRING)
private UserType userType;
}
If we don't use the @Enumerated annotation, the persistence provider will use the default enum's ordinal value. The ordinal is the constant's position and it starts from 0. Here, in this example, GUEST's ordinal is 0 and REGISTERED's ordinal is 1.
If we want to persist the constant's string then we need little configuration.
@Enumerated(value = EnumType.STRING)
private UserType userType;
I've put some of those codebases in this link(GitHub). Please check the code. You can run the spring boot project and watch the debugger console. Here is my output on my machine.
Hibernate:
drop table if exists app_user cascade
Hibernate:
drop table if exists blog_post cascade
Hibernate:
drop table if exists employee cascade
Hibernate:
drop table if exists hash_key cascade
Hibernate:
drop table if exists new_date_time_example cascade
Hibernate:
drop table if exists order_entry cascade
Hibernate:
drop table if exists support cascade
2023-07-23T23:21:44.615+06:00 WARN 13052 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Warning Code: 0, SQLState: 00000
2023-07-23T23:21:44.615+06:00 WARN 13052 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : table "support" does not exist, skipping
Hibernate:
drop table if exists temporal_example cascade
Hibernate:
drop table if exists user_role cascade
Hibernate:
drop table if exists user_seq cascade
Hibernate:
drop sequence if exists blog_post_seq
Hibernate:
drop sequence if exists key_seq
Hibernate:
create sequence blog_post_seq start with 1 increment by 50
Hibernate:
create sequence key_seq start with 100 increment by 20
Hibernate:
create table app_user (
id integer not null,
email varchar(25) not null unique,
first_name varchar(255),
last_name varchar(255),
user_type varchar(255) check (user_type in ('GUEST','REGISTERED')),
primary key (id)
)
Hibernate:
create table blog_post (
id integer not null,
content varchar(255),
title varchar(255),
primary key (id)
)
Hibernate:
create table employee (
id integer not null,
primary key (id)
)
Hibernate:
create table hash_key (
id integer not null,
primary key (id)
)
Hibernate:
create table new_date_time_example (
create_time time(6),
created_date date,
pause_time time(6),
buy_date_time timestamp(6) with time zone,
created_date_time timestamp(6),
id bigint not null,
shipping_timestamp timestamp(6) with time zone,
primary key (id)
)
Hibernate:
create table order_entry (
order_id integer not null,
product_id integer not null,
primary key (order_id, product_id)
)
Hibernate:
create table support (
customer_id integer not null,
ticket_id integer not null,
primary key (customer_id, ticket_id)
)
Hibernate:
create table temporal_example (
created_date date,
created_time time(6),
updated_date date,
created_timestamp timestamp(6),
id bigint not null,
updated_timestamp timestamp(6),
primary key (id)
)
Hibernate:
create table user_role (
id serial not null,
primary key (id)
)
Hibernate:
create table user_seq (
seq_value bigint,
seq_id varchar(255) not null,
primary key (seq_id)
)
Hibernate:
insert into user_seq(seq_id, seq_value) values ('app_user',0)
Conclusion
I hope you have enjoyed this blog. Please leave me a comment if you have any confusion. Let me know how I can improve. However, JPA is a vast area. In this post, I've tried to cover some of it. I'll try to cover other things in later posts. Till then happy coding.
References
Naveen sir from durgasoft