Spring Data JPA

Introduction-

We will learn here Spring Data JPA, perform CRUD operation, Clear concept of Relationship mapping like one-to-many, many-to-one with Example.

Spring Data JPA is a powerful framework that simplifies the process of working with relational databases in Java applications. It provides an easy-to-use API for working with JPA, allowing developers to focus on their business logic instead of worrying about the details of how JPA works.

With Spring Data JPA, you can easily define entities that map to database tables, and then use them to perform CRUD (create, read, update, delete) operations on those tables. In addition, Spring Data JPA provides support for more advanced features like querying and pagination, making it an excellent choice for both small and large projects.

In this blog, we will explore Spring Data JPA by building a sample application that uses it to manage users and packs. But, before let's see some key differences between Spring Data JPA and Hibernate.

Spring Data JPA Vs Hibernate

Spring Data JPA and Hibernate are both popular choices for implementing the JPA specification, which provides an API for working with relational databases in Java. While Hibernate is a standalone ORM (Object-Relational Mapping) framework, Spring Data JPA is a module of the larger Spring Data project, which aims to simplify common data access tasks.

Here are some key differences between Spring Data JPA and Hibernate:

  1. Ease of use: Spring Data JPA is designed to simplify common data access tasks and reduce boilerplate code, making it easier to use than Hibernate. Spring Data JPA provides a higher level of abstraction and supports query creation from method names, which eliminates the need to write SQL queries for many common operations.
  2. Integration with Spring ecosystem: Spring Data JPA is tightly integrated with the Spring framework and other Spring Data modules, which provides many benefits such as easy integration with Spring's dependency injection, transaction management, and caching features. Hibernate, on the other hand, can be used with any Java application, but may require more configuration and setup.
  3. Performance: Hibernate is generally considered to be faster than Spring Data JPA because it is a more lightweight solution. However, the performance difference may not be noticeable for small to medium-sized applications.
  4. Flexibility: Hibernate provides more flexibility and fine-grained control over the database schema and mapping, which can be beneficial for complex applications that require advanced database features. Spring Data JPA, on the other hand, provides more high-level abstractions and can be easier to use for simpler applications.

Setup

Before we get started, we need to set up a new Spring Boot project and add the necessary dependencies. To do this, follow these steps:

1. Create a new Spring Boot project using your preferred IDE.

2. Add the following dependencies to your project's build.gradle or pom.xml file:

Gradle:


dependencies {

    // Spring Boot

    implementation 'org.springframework.boot:spring-boot-starter'

    

    // Spring Data JPA

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

}

Maven:


<dependencies>

    <!-- Spring Boot -->

    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter</artifactId>

    </dependency>

    

    <!-- Spring Data JPA -->

    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-data-jpa</artifactId>

    </dependency>    

</dependencies>

3.Add the following properties in the application.properties file


#Database Configuration

#specifies the JDBC URL to connect to the database.
spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase

#specifies the database username and password
spring.datasource.username=dbuser
spring.datasource.password=dbpass

#specifies the JDBC driver class name to use.
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

#Hibernate Configuration

#specifies how Hibernate should update the database schema (in this case, it will automatically update the schema).
spring.jpa.hibernate.ddl-auto=update

#logs the SQL statements generated by Hibernate to the console.
spring.jpa.show-sql=true

#specifies the SQL dialect to use (in this case, MySQL 5 InnoDB dialect).
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

4. Create a new package called codeKatha.jpaExample.

5. Create a new file called JpaExampleApplication.java in the codeKatha.jpaExample package and add the following code:


package codeKatha.jpaExample;


package codeKatha.jpaExample;


import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;



@SpringBootApplication

public class JpaExampleApplication {



    public static void main(String[] args) {

        SpringApplication.run(JpaExampleApplication.class, args);

    }

}

Now we are ready to start building our application!

Types of association mapping

In Spring Data JPA, we can define three types of associations between entities:

One-to-One: In this type of association, each record in one table is associated with exactly one record in another table, and vice versa. For example, if we have a User and a Profile table, each user can have only one profile and each profile can belong to only one user.

One-to-Many: In this type of association, each record in one table is associated with multiple records in another table, but each record in the second table is associated with only one record in the first table. For example, if we have a User and a Post table, each user can have multiple posts, but each post can belong to only one user.

Many-to-Many: In this type of association, each record in one table can be associated with multiple records in another table, and vice versa. For example, if we have a User and a Pack table, each user can have multiple packs, and each pack can belong to multiple users.

We can also define two types of mapping for associations:

Unidirectional Mapping: In this type of mapping, only one of the entities has a reference to the other entity. For example, if we have a User entity and a Pack entity, the User entity can have a reference to the Pack entity, but the Pack entity does not have a reference to the User entity.

Bidirectional Mapping: In this type of mapping, both entities have a reference to each other. For example, if we have a User entity and a Pack entity, the User entity can have a reference to the Pack entity, and the Pack entity can have a reference to the User entity.

Spring Data JPA provides several annotations to define these associations:
  • @OneToOne: This annotation is used to define a one-to-one association between two entities. It can be used on either side of the association to specify the relationship.
  • @OneToMany: This annotation is used to define a one-to-many association between two entities. It is used on the side of the association that has the many instances.
  • @ManyToOne: This annotation is used to define a many-to-one association between two entities. It is used on the side of the association that has the single instance.
  • @ManyToMany: This annotation is used to define a many-to-many association between two entities. It is used on both sides of the association to specify the relationship.
  • @JoinTable: This annotation is used to specify the join table for a many-to-many association.
  • @JoinColumn: This annotation is used to specify the column that is used to join two entities in a one-to-one or many-to-one association.
  • @MappedSuperclass: This annotation is used to define a superclass that is mapped to the database but is not itself an entity. It can be used to define common fields and behavior that are inherited by entities.
These annotations can be used in conjunction with each other to define complex relationships between entities in Spring Data JPA.

Creating Entities

The first step in using Spring Data JPA is to create entities that map to database tables. In our example application, we will create two entities: User and Pack. So, let's create users and packs tables first followed by User and Pack entities.


CREATE TABLE users (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  username VARCHAR(255) NOT NULL,
  email VARCHAR(255) NOT NULL,
  PRIMARY KEY (id)
);

CREATE TABLE packs (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  name VARCHAR(255) NOT NULL,
  description TEXT,
  user_id BIGINT UNSIGNED NOT NULL,
  PRIMARY KEY (id),
  FOREIGN KEY (user_id) REFERENCES users(id)
);

These SQL statements create two tables, one for users and one for packs. The users table has columns for id (auto-generated primary key), username, and email. The packs table has columns for id (auto-generated primary key), name, description, and user_id, which is a foreign key referencing the id column in the users table.


package codeKatha.jpaExample.entity;

import javax.persistence.*;

@Entity

@Table(name = "users")

public class User {



    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;



    @Column(name = "username")

    private String username;



    @Column(name = "email")

    private String email;



    // Getters and setters

}


package codeKatha.jpaExample.entity;

import javax.persistence.*;

@Entity

@Table(name = "packs")

public class Pack {



    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;



    @Column(name = "name")

    private String name;



    @Column(name = "description")

    private String description;



    // Getters and setters

}

Here we have defined two entities using the @Entity annotation. We have also used the @Table annotation to specify the name of the table that each entity should be mapped to.

Both entities have an id field that is annotated with @Id to indicate that it is the primary key for the table. We have also used the @GeneratedValue annotation to indicate that the value of the id field should be automatically generated by the database.

We have also defined some additional fields for each entity. For User, we have defined username and email, while for Pack, we have defined name and description.

Defining Relationships

Now that we have defined our entities, we can define relationships between them. In our example application, we will define a one-to-many relationship between User and Pack, where a user can have multiple packs associated with them.

User entity mapping

To define this relationship, we will add a field to the User entity that references a collection of Pack entities:


package codeKatha.jpaExample.entity;

import javax.persistence.*;

import java.util.List;

@Entity

@Table(name = "users")

public class User {



    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;



    @Column(name = "username")

    private String username;



    @Column(name = "email")

    private String email;



    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)

    private List<Pack> packs;



    // Getters and setters

}

Here we have added a field called packs that is annotated with @OneToMany to indicate that there can be multiple Pack entities associated with a single User entity.

We have also used the mappedBy attribute to specify that this relationship is mapped by the user field in the Pack entity.

Finally, we have added the cascade attribute to specify that any changes made to a User entity should also be cascaded to its associated Pack entities.

Let's understand in more detail:

The @OneToMany annotation in JPA is used to define a one-to-many relationship between two entities. In this case, the User entity has a one-to-many relationship with the Pack entity.

The mappedBy attribute is used to specify the property on the target entity (Pack) that owns the relationship. In this case, the User entity has a list of Pack entities, and the mappedBy attribute is set to "user", which means that the user property on the Pack entity owns the relationship.

The cascade attribute is used to specify how changes to the parent entity (User) should be propagated to the child entities (Pack). In this case, the CascadeType.ALL option is used, which means that any change made to the User entity (e.g., creating, updating, or deleting) will be propagated to all associated Pack entities.

This means that if a User is deleted, all Pack entities associated with that User will also be deleted. Similarly, if a new Pack is added to a User's list of Pack entities, the Pack entity will be automatically persisted.

The List type is used to represent the collection of Pack entities associated with a User. This allows a User to have many Pack entities associated with it, and the @OneToMany annotation is used to define the relationship between the two entities.

Pack entity mapping

For the Pack entity, we need to add a field that references the User entity it is associated with:


package codeKatha.jpaExample.entity;

import javax.persistence.*;

@Entity

@Table(name = "packs")

public class Pack {



    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;



    @Column(name = "name")

    private String name;



    @Column(name = "description")

    private String description;



    @ManyToOne(fetch = FetchType.LAZY)

    @JoinColumn(name = "user_id")

    private User user;



    // Getters and setters

}

Here we have added a field called user that is annotated with @ManyToOne to indicate that there is a many-to-one relationship between Pack and User.

We have also used the fetch attribute to specify that this relationship should be lazily loaded, and the @JoinColumn annotation to specify the name of the foreign key column that is used to reference the User entity.

Let's understand in more detail:

The @ManyToOne annotation in JPA is used to define a many-to-one relationship between two entities. In this case, the Pack entity has a many-to-one relationship with the User entity.

The fetch attribute is used to specify how the associated User entity should be loaded from the database. In this case, FetchType.LAZY is used, which means that the User entity will be loaded from the database only when it is accessed for the first time. This can help improve performance by avoiding unnecessary database queries.

The @JoinColumn annotation is used to specify the foreign key column in the Pack table that references the primary key column in the User table. In this case, the user_id column in the Pack table references the id column in the User table.

The name attribute is used to specify the name of the foreign key column (user_id), while the referencedColumnName attribute could be used to specify the name of the primary key column in the User table, but since it is not specified, it defaults to the primary key column name of id.

The private User user; field represents the association between the Pack and User entities. This allows each Pack to be associated with a single User, and the @ManyToOne annotation is used to define the relationship between the two entities.

Using Spring Data JPA

Now that we have defined our entities and relationships, we can use Spring Data JPA to perform CRUD operations on them.

About JpaRepository

JpaRepository is an interface provided by Spring Data JPA that extends the CrudRepository interface. It provides additional methods for working with JPA entities, beyond the basic CRUD operations provided by CrudRepository.

CrudRepository is an interface provided by Spring Data JPA that provides the basic CRUD (Create, Read, Update, Delete) operations for working with JPA entities. The JpaRepository interface provides the additional methods.

1. findAll() - retrieves all entities of a given type from the database

2. findById(ID id) - retrieves an entity by its ID from the database

3. save(S entity) - saves an entity to the database (either inserting a new record or updating an existing one)

4. deleteById(ID id) - deletes an entity by its ID from the database

5. count() - counts the number of entities of a given type in the database

6. existsById(ID id) - checks whether an entity with a given ID exists in the database

These methods are implemented automatically by Spring Data JPA, based on the naming conventions of the method names. For example, the findById() method takes an ID parameter and returns an entity of the same type, where the ID corresponds to the primary key of the entity.

In addition to the above methods, JpaRepository also supports pagination and sorting of results using Page and Sort objects. For example, the findAll(Pageable pageable) method returns a Page object containing a subset of entities, based on the given Pageable object.

By extending the JpaRepository interface, custom repository interfaces can easily define new methods for working with JPA entities. These methods can then be implemented by Spring Data JPA, based on the method names and parameters, without any additional code. This allows developers to create efficient and easy-to-use data access interfaces for their JPA entities.

Get started

To get started, we need to define repositories that extend the JpaRepository interface provided by Spring Data JPA. Here is an example repository for the User entity:


package codeKatha.jpaExample.repository;

import codeKatha.jpaExample.entity.User;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {

}

And here is an example repository for the Pack entity:


package codeKatha.jpaExample.repository;

import codeKatha.jpaExample.entity.Pack;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PackRepository extends JpaRepository<Pack, Long> {

}

With these repositories in place, we can now use them to perform CRUD operations on our entities. Here are some examples:


package codeKatha.jpaExample.service;

import codeKatha.jpaExample.entity.User;

import codeKatha.jpaExample.entity.Pack;

import codeKatha.jpaExample.repository.UserRepository;

import codeKatha.jpaExample.repository.PackRepository;

import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service

@Transactional

public class UserService {

    private final UserRepository userRepository;

    private final PackRepository packRepository;



    public UserService(UserRepository userRepository, PackRepository packRepository) {

        this.userRepository = userRepository;

        this.packRepository = packRepository;

}

public List<User> getAllUsers() {

    return userRepository.findAll();

}



public User getUserById(Long id) {

    return userRepository.findById(id).orElse(null);

}



public User createUser(User user) {

    return userRepository.save(user);

}



public void deleteUser(Long id) {

    userRepository.deleteById(id);

}



public List<Pack> getPacksByUserId(Long userId) {

    User user = userRepository.findById(userId).orElse(null);

    if (user == null) {

        return null;

    }

    return user.getPacks();

}



public Pack createPackForUser(Long userId, Pack pack) {

    User user = userRepository.findById(userId).orElse(null);

    if (user == null) {

        return null;

    }

    pack.setUser(user);

    return packRepository.save(pack);

}



public void deletePack(Long packId) {

    packRepository.deleteById(packId);

}

}

Here we have defined a `UserService` class that uses the `UserRepository` and `PackRepository` to perform CRUD operations on our entities.

We have defined methods to get all users, get a user by ID, create a new user, and delete a user. We have also defined methods to get packs associated with a user, create a new pack for a user, and delete a pack.

For example, to create a new user and associate a pack with them, we could use the following code:


User user = new User();

user.setUsername("advait");

user.setEmail("advait@codeKatha.com");

user = userService.createUser(user);



Pack pack = new Pack();

pack.setName("My Pack");

pack.setDescription("This is my pack");

pack = userService.createPackForUser(user.getId(), pack);

Here we create a new User entity and save it using the createUser method of the UserService. We then create a new Pack entity and associate it with the user using the createPackForUser method of the UserService.

Spring Data JPA Custom Queries

Spring Data JPA provides several ways to write custom queries to retrieve data from the database. Here are some examples:

Query methods

Spring Data JPA provides the ability to generate queries based on the method name. For example, to find a user by their username, you can define a method in your UserRepository interface like this:


public interface UserRepository extends JpaRepository<User, Long> {

    User findByUsername(String username);

}

This will generate a query that retrieves the user with the specified username.

Named queries

You can define custom queries using the @NamedQuery annotation in your entity classes. For example, to find all users with a specific email address, you can define a named query like this:


@Entity
@Table(name = "users")
@NamedQuery(name = "User.findByEmail", query = "SELECT u FROM User u WHERE u.email = ?1")
public class User {
    //...
}

You can then use this named query in your repository interface like above 'Query Method' both gives same result:


public interface UserRepository extends JpaRepository<User, Long> {

    List<User> findByEmail(String email);

}

@Query annotation

You can also define custom queries using the @Query annotation in your repository interface. For example, to find all users who have a pack with a specific name, you can define a method like this:


public interface UserRepository extends JpaRepository<User, Long> {

    @Query("SELECT u FROM User u JOIN u.packs p WHERE p.name = :name")
    List<User> findByPackName(@Param("name") String name);

}

This will generate a query that joins the User and Pack tables and returns all users who have a pack with the specified name.

Criteria API

Spring Data JPA also provides a Criteria API that allows you to define queries programmatically. For example, to find all users with a specific email address, you can define a method like this:


public interface UserRepository extends JpaRepository<User, Long> {

    List<User> findByEmail(String email);

    default List<User> findByEmailUsingCriteria(String email) {
        CriteriaBuilder cb = entityManager().getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> root = query.from(User.class);
        query.select(root).where(cb.equal(root.get("email"), email));
        return entityManager().createQuery(query).getResultList();
    }

}

This method uses the CriteriaBuilder to create a query that selects all users where the email field is equal to the specified email address.

These are just a few examples of the ways you can write custom queries with Spring Data JPA. The best approach depends on your specific requirements and preferences.

Spring Data JPA Composite Key

What is Composite Key

A composite key is a primary key that consists of two or more columns in a database table. It is used to uniquely identify each row in the table by combining the values of the multiple columns. In the context of an object-relational mapping (ORM) framework like JPA, a composite key can be represented by a single class that holds the values of the multiple columns.
Suppose we have the following table for User and Pack:

Users
-----
id (PK)
username
email

Packs
-----
id (PK)
name
description
user_id (FK)


In this scenario, we can create a third table called user_pack to represent the many-to-many relationship between users and packs. The user_pack table will have two foreign keys, one for the users table and one for the packs table.

Composite key using Spring Data JPA

To define a composite key using Spring Data JPA, we can create a separate entity that represents the composite key and then use that entity as the primary key in the user_pack table.

Here's an example of how to define a composite key for the user_pack table in MySQL:


CREATE TABLE user_pack (
  user_id INT NOT NULL,
  pack_id INT NOT NULL,
  PRIMARY KEY (user_id, pack_id),
  FOREIGN KEY (user_id) REFERENCES user(id),
  FOREIGN KEY (pack_id) REFERENCES pack(id)
);

In this example, the composite primary key is made up of the user_id and pack_id columns. The user_id and pack_id columns are also foreign keys that reference the id columns in the users and packs tables, respectively.

To implement this in Spring Data JPA, we can create a separate entity to represent the composite key:

@Embeddable
public class UserPackId implements Serializable {

    private Long userId;
    private Long packId;

    // getters and setters
}

In this example, the UserPackId class represents the composite key for the user_pack table. It has two fields: userId and packId, which correspond to the user_id and pack_id columns in the table.

We can then create the UserPack entity to represent the user_pack table:


@Entity
@Table(name = "user_pack")
public class UserPack {

    @EmbeddedId
    private UserPackId id;

    @ManyToOne
    @MapsId("userId")
    private User user;

    @ManyToOne
    @MapsId("packId")
    private Pack pack;

    // getters and setters
}

In this example, the UserPack entity has an @EmbeddedId field called id, which represents the composite primary key. The @MapsId annotation is used to map the user field to the userId field in the UserPackId class and the pack field to the packId field in the UserPackId class.

Composite key annotations explanation

1. @Embeddable:
The @Embeddable annotation is used to indicate that the class is an embeddable class and can be embedded into an entity. An embeddable class is a value type class that is used to represent a composite key or a value type that is embedded within an entity. An embeddable class is typically used when we want to group multiple columns into a single object, so that we can use it as a single field in an entity.
In the given example, the UserPackId class is marked as @Embeddable because it is used to represent a composite key that consists of two columns: userId and packId.

2. @EmbeddedId:
The @EmbeddedId annotation is used to specify that the field is an embedded primary key field. It is used to specify that the primary key of an entity is an instance of an embeddable class.
In the given example, the UserPack entity has an @EmbeddedId field called id, which represents the composite primary key made up of the userId and packId columns.

3. @ManyToOne:
The @ManyToOne annotation is used to specify a many-to-one relationship between two entities. It is used to specify that an entity has a many-to-one relationship with another entity. It is typically used when we want to map a foreign key column in a table to an entity in another table.
In the given example, the UserPack entity has two @ManyToOne fields called user and pack, which represent the relationship between the UserPack entity and the User and Pack entities.

4. @MapsId:
The @MapsId annotation is used to specify that a field in an entity maps to the primary key of another entity. It is used to specify that the value of a field in an entity should be mapped to the primary key of another entity.
In the given example, the user field in the UserPack entity is annotated with @MapsId("userId"), which specifies that the value of the user field should be mapped to the userId field in the UserPackId class. Similarly, the pack field in the UserPack entity is annotated with @MapsId("packId"), which specifies that the value of the pack field should be mapped to the packId field in the UserPackId class.

If you need a detailed tutorial on a one-to-one, many-to-one, one-to-many, many-to-many, what is unidirectional mapping, what is bidirectional mapping with code example, just reach out through the contact us form- by clicking three lines on the Homepage.

Hope this blog tutorial has been helpful to you. If you have any remaining doubts or suggestions, please feel free to share them in the comments below. We would be glad to receive your feedback.

💛 You can Click Here to connect with us. We will give our best for you. 

Comments

Popular Posts on Code Katha

Java Interview Questions for 10 Years Experience

Sql Interview Questions for 10 Years Experience

Spring Boot Interview Questions for 10 Years Experience

Java interview questions - Must to know concepts

Visual Studio Code setup for Java and Spring with GitHub Copilot

Data Structures & Algorithms Tutorial with Coding Interview Questions

Spring AI with Ollama

Java interview questions for 5 years experience

Elasticsearch Java Spring Boot