Intro to Spring Data JPA. The Entity class and its annotations

Intro to Spring Data JPA. The Entity class and its annotations

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

@EmbeddedId example

@Embeddable
@Data
public class OrderPK {
    private int orderId;
    private int productId;
}
@NoArgsConstructor
@Getter
@Setter
@Entity
public class OrderEntry {
    @EmbeddedId
    private OrderPK id;
}

@IdClass

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 Date

  • LocalTime 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