Spring Data Jpa

Introduction

  • What is Spring Data JPA and why is it useful? Spring Data JPA is a library that helps you easily implement the data access layer of your application. It is based on the Java Persistence API (JPA), which is a Java specification for accessing, persisting, and managing data between Java objects/classes and a relational database. Spring Data JPA simplifies the development of the data access layer by providing a set of interfaces and classes that you can extend or use directly in your application. It eliminates the need for writing boilerplate code and allows you to focus on writing the business logic of your application.

  • Setting up a project with Spring Data JPA To use Spring Data JPA in your project, you need to add the spring-boot-starter-data-jpa dependency to your project's classpath. This dependency brings in the necessary libraries for using Spring Data JPA. You also need to configure a data source for your application and specify the JPA provider (e.g., Hibernate) that you want to use.

  • Understanding the EntityManager and EntityManagerFactory The EntityManager and EntityManagerFactory are central interfaces in the JPA specification. The EntityManager is responsible for managing the persistence of entities (i.e., Java objects that are mapped to database tables). It provides methods for performing CRUD operations on entities, as well as for executing queries and handling transactions. The EntityManagerFactory is responsible for creating EntityManager instances.

Creating Entities

Annotating Java objects as JPA entities To map a Java object to a database table, you need to annotate the class with the @Entity annotation and specify the name of the table it should be mapped to. You also need to annotate the fields of the class with the @Column annotation to specify the name of the corresponding database column. For example:

@Entity
@Table(name = "employees")
public class Employee {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(name = "first_name")
  private String firstName;

  @Column(name = "last_name")
  private String lastName;

  // getters and setters omitted for brevity
}
  • Mapping entity attributes to database columns By default, the name of the database column is the same as the name of the attribute in the entity class. However, you can use the name attribute of the @Column annotation to specify a different name for the column. You can also use the nullable, length, and unique attributes to specify constraints on the column.

  • Generating primary keys with the @GeneratedValue annotation The @GeneratedValue annotation is used to specify how the primary key of an entity should be generated. You can specify a generation strategy such as AUTO, IDENTITY, SEQUENCE, or TABLE.

  • Using enums and embeddable objects in entities You can use the @Enumerated annotation to map an enum attribute to a database column. The @Embeddable and @Embedded annotations can be used to create an embeddable object that can be used as a component in an entity class.

Repositories

What are repositories and why are they useful? Repositories are used to create a separation between the data access layer and the business logic layer of an application. They provide a set of methods for working with entities in a database. These methods include basic CRUD operations as well as more advanced operations such as pagination and sorting. By using repositories, you can centralize the data access logic in your application and reuse it across different parts of the application.

  • Creating a repository interface To create a repository, you need to create an interface that extends the JpaRepository interface and specifies the entity class and the type of the primary key of the entity. For example:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
  // custom methods go here
}
  • Extending the JpaRepository interface The JpaRepository interface provides a set of methods for performing basic CRUD operations on entities, such as save, findOne, findAll, delete, and count. You can use these methods directly in your repository interface without having to implement them yourself.
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

  @Query("SELECT e FROM Employee e WHERE e.firstName = :firstName")
  List<Employee> findByFirstName(@Param("firstName") String firstName);

  List<Employee> findByLastName(String lastName);

  @Modifying
  @Query("UPDATE Employee e SET e.firstName = :firstName WHERE e.id = :id")
  void updateFirstName(@Param("id") Long id, @Param("firstName") String firstName);
}
  • Using the @Query annotation to execute custom JPQL queries The @Query annotation can be used to specify a custom JPQL query for a repository method. You can use named parameters in the query and bind them to method parameters using the @Param annotation.

  • Using the @Modifying annotation for update and delete queries If you want to execute an update or delete query using a repository method, you need to annotate the method with the @Modifying annotation. This tells Spring Data JPA to execute the query as an update or delete rather than a select.

Transactions

  • Understanding the concept of transactions A transaction is a unit of work that is performed on a database. It consists of one or more SQL statements that are executed together as a single unit. If any of the statements fail, the whole transaction is rolled back and the database is restored to its previous state. Transactions are used to ensure the integrity of the data in the database.

  • Configuring transactions with the @Transactional annotation You can use the @Transactional annotation to specify that a method should be executed within a transaction. By default, the transaction is propagated from the caller to the callee, and a new transaction is created if none exists. You can use the propagation attribute of the @Transactional annotation to customize the propagation behavior.

  • Propagation levels and isolation levels The propagation level of a transaction specifies how a new transaction is created when a method with the @Transactional annotation is called. The isolation level of a transaction determines how the changes made by the transaction are isolated from other transactions.

    Some of the most common propagation levels are:

  • REQUIRED: If a transaction is already in progress, the method will join the existing transaction. If no transaction is in progress, a new transaction will be created.

  • REQUIRES_NEW: A new transaction is always created, even if a transaction is already in progress. The existing transaction is suspended until the new transaction is completed.

  • Some of the most common isolation levels are:

    • READ_COMMITTED: This is the default isolation level. It allows other transactions to read data that has been committed by the current transaction, but it prevents them from reading data that is still being modified by the current transaction.

    • REPEATABLE_READ: This isolation level prevents other transactions from reading data that has been modified by the current transaction, even if the changes have not been committed yet.

    • Handling transaction exceptions If an exception is thrown during the execution of a transaction, the transaction will be rolled back by default. You can customize this behavior by using the @Transactional annotation's rollbackFor and noRollbackFor attributes. The rollbackFor attribute specifies the exceptions that should cause the transaction to be rolled back, while the noRollbackFor attribute specifies the exceptions that should not cause the transaction to be rolled back.

Criteria Api

The Java Persistence Criteria API (JPA Criteria API) is a powerful tool for building dynamic, flexible queries in Java. Here is an example of how you can use the JPA Criteria API to build a dynamic query in a repository:

public interface EmployeeRepository extends JpaRepository<Employee, Long>, JpaSpecificationExecutor<Employee> {
  // custom methods go here
}

To use the JPA Criteria API, you need to implement the JpaSpecificationExecutor interface in your repository. This interface provides methods for executing Specification-based queries, such as findOne, findAll, and count.

public interface EmployeeRepository extends JpaRepository<Employee, Long>, JpaSpecificationExecutor<Employee> {
  default List<Employee> findByCriteria(String firstName, String lastName, String department) {
    return findAll((root, query, builder) -> {
      List<Predicate> predicates = new ArrayList<>();
      if (firstName != null) {
        predicates.add(builder.equal(root.get("firstName"), firstName));
      }
      if (lastName != null) {
        predicates.add(builder.equal(root.get("lastName"), lastName));
      }
      if (department != null) {
        predicates.add(builder.equal(root.get("department"), department));
      }
      return builder.and(predicates.toArray(new Predicate[0]));
    });
  }
} 

default List<Employee> findByCriteria(String firstName, String lastName, String department, String sortField, String sortDirection) {
  return findAll((root, query, builder) -> {
    // build predicates here
    if (sortField != null && sortDirection != null) {
      if (sortDirection.equalsIgnoreCase("ASC")) {
        query.orderBy(builder.asc(root.get(sortField)));
      } else {
        query.orderBy(builder.desc(root.get(sortField)));
      }
    }
    return builder.and(predicates.toArray(new Predicate[0]));
  });
}

In this example, the findByCriteria method builds a dynamic query by creating a Specification object using a lambda expression. The Specification object represents the criteria of the query, which consists of a list of predicates that are combined using the and operator.

Cashing

First, you need to add the following dependencies to your pom.xml file:

<!-- Spring Boot Starter Cache -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- Spring Boot Starter Data JPA -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Next, you need to enable caching in your Spring Boot application. You can do this by adding the @EnableCaching annotation to your main application class:

@SpringBootApplication
@EnableCaching
public class MyApp {
  public static void main(String[] args) {
    SpringApplication.run(MyApp.class, args);
  }
}

Then, you can use caching in your Spring services by adding the @Cacheable annotation to methods that are expensive or time-consuming:

@Service
public class EmployeeService {

  @Autowired
  private EmployeeRepository employeeRepository;

  @Cacheable("employees")
  public List<Employee> getEmployeesByDepartment(String department) {
    System.out.println("Fetching employees by department: " + department);
    return employeeRepository.findByDepartment(department);
  }
}

In this example, the getEmployeesByDepartment method fetches a list of employees from the database using the EmployeeRepository. The @Cacheable("employees") annotation indicates that the results of this method should be cached using the key "employees". When this method is called, the cache will be checked to see if there is already a cached result for the given key. If there is, the cached result will be returned. If not, the method will be executed and the result will be cached for future calls.

That's it! With these changes, the results of the getEmployeesByDepartment method will be cached, and subsequent calls to this method with the same department parameter will return the cached result instead of executing the method again.

If you're interested in learning more about caching in Spring, here are a few additional things to consider:

  • Cache eviction: By default, Spring caches do not have any eviction policy, meaning that cached values will never expire or be removed from the cache. You can configure an eviction policy using the @CacheEvict annotation, which allows you to specify a key or keys to be removed from the cache.

  • Cacheable parameters: You can use the key and condition parameters of the @Cacheable annotation to customize the cache key and the conditions under which the method should be cached. For example, you can use a parameter value as part of the cache key, or you can specify a condition based on the method parameters.

  • Caching providers: Spring supports a variety of caching providers, including Ehcache, Hazelcast, and Redis. You can configure your caching provider using the spring.cache.type property in your application.properties or application.yml file.

@Service
public class EmployeeService {

  @Autowired
  private EmployeeRepository employeeRepository;

  @Cacheable(value = "employees", key = "#department")
  public List<Employee> getEmployeesByDepartment(String department) {
    System.out.println("Fetching employees by department: " + department);
    return employeeRepository.findByDepartment(department);
  }

  @CacheEvict(value = "employees", key = "#department")
  public void clearCacheByDepartment(String department) {
    System.out.println("Clearing cache for department: " + department);
  }
}

In this example, the getEmployeesByDepartment method uses the @Cacheable annotation to cache the results of the method call with a cache key of department. If the method is called with the same department parameter, the cached results will be returned instead of executing the method again.

The clearCacheByDepartment method uses the @CacheEvict annotation to clear the cache for a specific department. When this method is called, the cached results for the specified department will be removed from the cache. This can be useful if you have data that changes frequently and you want to ensure that the cache is always up-to-date.

Here's an example of using the condition parameter of the @Cacheable annotation to cache the results of a method only if a specific condition is met:

@Service
public class EmployeeService {

  @Autowired
  private EmployeeRepository employeeRepository;

  @Cacheable(value = "employees", key = "#department", condition = "#useCache == true")
  public List<Employee> getEmployeesByDepartment(String department, boolean useCache) {
    System.out.println("Fetching employees by department: " + department);

    if (!useCache) {
      System.out.println("Cache disabled, retrieving data from database");
      return employeeRepository.findByDepartment(department);
    }

    return employeeRepository.findByDepartment(department);
  }
}

In this example, the getEmployeesByDepartment method uses the condition parameter of the @Cacheable annotation to specify that the method should only be cached if the useCache parameter is set to true. If useCache is false, the method will always retrieve the data from the database instead of using the cached results.

Note that the condition parameter takes a SpEL (Spring Expression Language) expression that must evaluate to a boolean value. In this case, the expression #useCache == true checks whether the useCache parameter is true.

Entity Relation

  1. One-to-One Relationship

In a one-to-one relationship, each record in the first table is associated with exactly one record in the second table, and vice versa. Here is an example of a one-to-one relationship between a Person entity and a Passport entity:

@Entity
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToOne(mappedBy = "person", cascade = CascadeType.ALL)
    private Passport passport;

    // constructors, getters and setters
}

@Entity
public class Passport {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String number;

    @OneToOne
    @JoinColumn(name = "person_id", referencedColumnName = "id")
    private Person person;

    // constructors, getters and setters
}

In this example, the Person entity has a passport field that is annotated with @OneToOne, which indicates that there is a one-to-one relationship between a Person and a Passport. The mappedBy attribute of the @OneToOne annotation specifies the name of the field in the Passport entity that owns the relationship. The cascade attribute of the @OneToOne annotation specifies that changes to the Person entity should be propagated to the Passport entity.

The Passport entity also has a person field that is annotated with @OneToOne and @JoinColumn, which indicates that there is a one-to-one relationship between a Passport and a Person. The name attribute of the @JoinColumn annotation specifies the name of the foreign key column in the Passport table that references the id column in the Person table

  1. Many-to-One/One-to-Many Relationship

    In a many-to-one relationship, each record in the first table can be associated with one record in the second table, but each record in the second table can be associated with multiple records in the first table. Here is an example of a many-to-one relationship between a Book entity and an Author entity:

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToOne
    @JoinColumn(name = "author_id", referencedColumnName = "id")
    private Author author;

    // constructors, getters and setters
}

@Entity
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
    private Set<Book> books = new HashSet<>();

    // constructors, getters and setters
}

In this example, the Book entity has an author field that is annotated with @ManyToOne and @JoinColumn, which indicates that there is a many-to-one relationship between a Book and an Author. The name attribute of the @JoinColumn annotation specifies the name of the foreign key column in the Book table that references the id column in the Author table.

The Author entity has a books field that is annotated with @OneToMany, which indicates that there is a one-to-many relationship between an Author and a `

Many-to-Many

In a many-to-many relationship, each record in the first table can be associated with one or more records in the second table, and vice versa. Here is an example of a many-to-many relationship between a Student entity and a Course entity:

@Entity
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "student_course",
            joinColumns = {@JoinColumn(name = "student_id")},
            inverseJoinColumns = {@JoinColumn(name = "course_id")})
    private Set<Course> courses = new HashSet<>();

    // constructors, getters and setters
}

@Entity
public class Course {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();

    // constructors, getters and setters
}

Ebedded

In JPA, an embedded object is an object that is stored as a part of another entity, rather than as a separate entity in its own right. The @Embeddable annotation is used to indicate that a class is an embedded object. Here's an example of how to use an embedded object in a JPA entity:

@Entity
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @Embedded
    private Address address;

    // constructors, getters and setters
}

@Embeddable
public class Address {

    private String street;
    private String city;
    private String state;
    private String zip;

    // constructors, getters and setters
}

In this example, we have a Person entity with an address field, which is an instance of the Address class. The Address class is marked with the @Embeddable annotation to indicate that it is an embedded object.

When we persist a Person entity, JPA will automatically persist the Address object as a part of the Person entity. We can access the Address object from the Person entity using the getAddress() method.

Here's an example of how to create and persist a Person entity with an Address object:

Person person = new Person();
person.setName("John Smith");

Address address = new Address();
address.setStreet("123 Main St.");
address.setCity("Anytown");
address.setState("CA");
address.setZip("12345");

person.setAddress(address);

entityManager.persist(person);

In this example, we create a Person object and an Address object, set the Address object as the value of the Person object's address field, and then persist the Person object using the entity manager.

Note that we don't have to explicitly persist the Address object separately, since it is embedded in the Person object and will be automatically persisted along with it.