11 | How to customize JpaRepository

EntityManager introduction

The Java Persistence API stipulates that operating database entities must be performed through EntityManager. As we saw earlier, the implementation class of all Repositories in JPA is SimpleJpaRepository, which calls methods in EntityManager when it actually operates entities.

We set a breakpoint in SimpleJpaRepository, so that we can easily see that EntityManger is the interface protocol of JPA, and its current class is SessionImpl in Hibernate, as shown in the following figure:

Drawing 0.png

So let’s see what methods EntityManager provides us.

What are the EntityManager methods?

Here are some important and commonly used methods. I will briefly mention the less commonly used methods. If you are interested, you can check them out by yourself.

Copy code

public interface EntityManager {
  //Used to bring the newly created Entity into the management of EntityManager. After this method is executed, the Entity object passed into the persist() method is converted into a persistent state.
  public void persist(Object entity);
  //Merge the free entity into the current persistence context, generally used for updates.
  public <T> T merge(T entity);
  //Delete the entity object, physically delete it
  public void remove(Object entity);
  //Synchronize the entities in the current persistence context to the database. Only when this method is executed, the above EntityManager operation will take effect in the DB;
  public void flush();
  //Query an entity object based on entity type and primary key;
  public <T> T find(Class<T> entityClass, Object primaryKey);
  //Create a Query object based on JPQL
  public Query createQuery(String qlString);
  //Use CriteriaUpdate to create an update query
  public Query createQuery(CriteriaUpdate updateQuery);
  //Use native sql statements to create queries, which can be query, update, delete, etc. sql
  public Query createNativeQuery(String sqlString);
  ...//I won’t list the other methods one by one. The usage is very simple. We only need to refer to how to use it in SimpleJpaRepository and how to use it;
}

In this lesson, we first know the syntax and usage of EntityManager. When we introduce Persistence Context in Lesson 21, we will explain in detail its impact on entity status and what each status means.

So now that you know these syntaxes, how do you use them?

How to use EntityManager?

Its use is very simple. As long as we can get the EntityManager, we can perform the operations inside it.

How to obtain EntityManager: through @PersistenceContext annotation.

Mark the @PersistenceContext annotation on the field of EntityManager type, so that the resulting EntityManager is the container-managed EntityManager. Since it is container-managed, we do not need and should not explicitly close the injected EntityManager instance.

The following is an example of this approach. We want to get the EntityManager in @PersistenceContext in the test class to see how the code should be written.

Copy code

@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class UserRepositoryTest {
    //Use this method to obtain entityManager
    @PersistenceContext
    private EntityManager entityManager;
    @Autowired
    private UserRepository userRepository;
    /**
     * Test entityManager usage
     *
     * @throws JsonProcessingException
     */
    @Test
    @Rollback(false)
    public void testEntityManager() throws JsonProcessingException {
        //Test to find a User object
        User user = entityManager.find(User.class,2L);
        Assertions.assertEquals(user.getAddresses(),"shanghai");

        //Let's change the deletion status of user
        user.setDeleted(true);
        //merger method
        entityManager.merge(user);
        //Update to the database
        entityManager.flush();

        //Create a JPQL through createQuery to query
        List<User> users = entityManager.createQuery("select u From User u where u.name=?1")
                .setParameter(1,"jack")
                .getResultList();
        Assertions.assertTrue(users.get(0).getDeleted());
    }
}

Through this test case, we can know that EntityManager is relatively easy to use. However, in actual work, I do not recommend operating EntityManager directly, because if you are not skilled in operation, some transaction exceptions will occur. Therefore, I still recommend that you operate through the Repositories provided by Spring Data JPA.
As a reminder, you can directly operate EntityManager when writing a framework. Remember not to use EntityManager in any business code, otherwise your code will be difficult to maintain in the end.

Now that we understand EntityManager, let’s take a look at what role @EnableJpaRepositories plays in custom Repository.

@EnableJpaRepositories detailed explanation

The following is a detailed introduction to the syntax of @EnableJpaRepositories and its default loading method.

@EnableJpaRepositories syntax

Let’s look at the code directly, as shown below:

Copy code

public @interface EnableJpaRepositories {
   String[] value() default {};
   String[] basePackages() default {};
   Class<?>[] basePackageClasses() default {};
   Filter[] includeFilters() default {};
   Filter[] excludeFilters() default {};
   String repositoryImplementationPostfix() default "Impl";
   String namedQueriesLocation() default "";
   Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
   Class<?> repositoryFactoryBeanClass() default JpaRepositoryFactoryBean.class;
   Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;
   String entityManagerFactoryRef() default "entityManagerFactory";
   String transactionManagerRef() default "transactionManager";
   boolean considerNestedRepositories() default false;
   boolean enableDefaultTransactions() default true;
}

Let me explain the 10 methods in detail below:

1) value is equal to basePackage

Used to configure scanning of the package and sub-packages where Repositories are located.

Can be configured as a single string.

Copy code

@EnableJpaRepositories(basePackages = "com.example")

It can also be configured in the form of a string array, that is, multiple situations.

Copy code

@EnableJpaRepositories(basePackages = {"com.sample.repository1", "com.sample.repository2"})

By default, the @SpringBootApplication annotation appears in the directory and its subdirectories.

2) basePackageClasses

Specify the package where the Repository class is located, which can replace the use of basePackage.

It can also be a single character. The following example shows that all Repositories under the Package where BookRepository.class is located will be scanned and registered.

Copy code

@EnableJpaRepositories(basePackageClasses = BookRepository.class)

It can also contain multiple characters. The following example represents that all Repositories under the package where ShopRepository.class and OrganizationRepository.class are located will be scanned.

Copy code

@EnableJpaRepositories(basePackageClasses = {ShopRepository.class, OrganizationRepository.class})

3) includeFilters

Specify the included filter, which uses ComponentScan’s filter. You can specify the filter type.

The following example shows that only classes with Repository annotations are scanned.

Copy code

@EnableJpaRepositories( includeFilters={@ComponentScan.Filter(type=FilterType.ANNOTATION, value=Repository.class)})

4) excludeFilters

Specifies not to include a filter, which is also a class in the filter that uses ComponentScan.

The following example shows that classes annotated with @Service and @Controller do not need to be scanned in, which can speed up the startup of the application when our project becomes larger.

Copy code

@EnableJpaRepositories(excludeFilters={@ComponentScan.Filter(type=FilterType.ANNOTATION, value=Service.class),@ComponentScan.Filter(type=FilterType.ANNOTATION, value=Controller.class)})

5) repositoryImplementationPostfix

When we customize Repository, what is the suffix of the implementation class of the agreed interface Repository? The default is Impl. I will explain the example in detail below.

6) namedQueriesLocation

The location where named SQL is stored, the default is META-INF/jpa-named-queries.properties

Examples are as follows:

Copy code

Todo.findBySearchTermNamedFile=SELECT t FROM Table t WHERE LOWER(t.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) ORDER BY t.title ASC

As long as you know this, I recommend not to use it, because although it is very powerful, when we use such a complicated method, you need to think about whether there is a simpler method.

7) queryLookupStrategy

The search strategy for building conditional queries includes three methods: CREATE, USE_DECLARED_QUERY, and CREATE_IF_NOT_FOUND.

As we introduced in previous lessons:

  • CREATE: Automatically build query methods according to the interface name, which is the Defining Query Methods we mentioned earlier;
  • USE_DECLARED_QUERY: Use @Query to query;
  • CREATE_IF_NOT_FOUND: If there is @Query annotation, use this as the standard; if it does not work, then use Defining Query Methods; this is the default and basically does not need to be modified, we just know it.

8) repositoryFactoryBeanClass

Specify the factory class that produces Repository, the default is JpaRepositoryFactoryBean. The main function of JpaRepositoryFactoryBean is to help us generate implementation classes for all Repository interfaces in the form of dynamic proxy. For example, when we pass a breakpoint and see that the implementation class of UserRepository is a SimpleJpaRepository proxy object, it is this factory class that does it. Generally, we rarely change the mechanism for generating proxies.

9) entityManagerFactoryRef

Used to specify which factory class is used to create and produce EntityManager. The default is the Bean with name=”entityManagerFactory”. Generally used for multi-data configuration.

10) Class repositoryBaseClass()

Used to specify what the implementation class of our custom Repository is. The default is DefaultRepositoryBaseClass, which means there is no specified implementation base class of Repository.

11) String transactionManagerRef() default “transactionManager”

Used to specify the default transaction processing class. The default is transactionManager, which is generally used for multiple data sources.

The above is the basic syntax of @EnableJpaRepositories. There are many methods involved, so you can explore slowly. Let’s take a look at how it is loaded by default.

@EnableJpaRepositories default loading method

By default, it is the automatic loading mechanism of spring boot. JpaRepositoriesAutoConfiguration is loaded through the spring.factories file, as shown below:

Drawing 1.png

The @Import(JpaRepositoriesRegistrar.class) operation is performed in JpaRepositoriesAutoConfiguration, as shown below:

Drawing 2.png

And JpaRepositoriesRegistrar.class is configured with @EnableJpaRepositories, so that the default value has the following effect:

Drawing 3.png

In this way, the syntax and default loading method of @EnableJpaRepositories have been introduced. You can know that many of our customized needs can be completed through @EnableJpaRepositories. So how do you define your own Repository implementation class? Let’s see.

Customize Repository’s impl method

There are two ways to define your own Repository implementation.

The first method: define an independent Impl implementation class of Repository

Let’s illustrate it through an example. Suppose we want to implement a logical deletion function. Let’s see what should be done?

Step one: Define a CustomizedUserRepository interface.

This interface will be automatically scanned after @EnableJpaRepositories is turned on. The code is as follows:

Copy code

package com.example.jpa.example1.customized;
import com.example.jpa.example1.User;
public interface CustomizedUserRepository {
    User logicallyDelete(User user);
}

Step 2: Create a CustomizedUserRepositoryImpl implementation class.

And the implementation class ends with the Impl we said above, as follows:

Copy code

package com.example.jpa.example1.customized;
import com.example.jpa.example1.User;
import javax.persistence.EntityManager;
public class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
    private EntityManager entityManager;
    public CustomizedUserRepositoryImpl(EntityManager entityManager) {
        this.entityManager = entityManager;
    }
    @Override
    public User logicallyDelete(User user) {
        user.setDeleted(true);
        return entityManager.merge(user);
    }
}

Among them, we also discovered the second injection method of EntityManager, which is to place it directly in the construction method and automatically inject it through Spring.

Step 3: When using UserRepository, just inherit our customized CustomizedUserRepository interface.

Copy code

public interface UserRepository extends JpaRepository<User,Long>, JpaSpecificationExecutor<User>, CustomizedUserRepository {
}

Step 4: Write a test case to test it.

Copy code

@Test
public void testCustomizedUserRepository() {
    //Find out a User object
    User user = userRepository.findById(2L).get();
    //Call our logical deletion method to delete
    userRepository.logicallyDelete(user);
    //Let's check it again to see if the value has changed.
    List<User> users = userRepository.findAll();
    Assertions.assertEquals(users.get(0).getDeleted(),Boolean.TRUE);
}

Finally, call the logicallyDelete method we customized just now, run the test case, and the result is completely passed. So what is the implementation principle of this method? Let’s analyze it through debug.

Principle analysis

We talked about Class repositoryFactoryBeanClass() default JpaRepositoryFactoryBean.class above. The dynamic proxy creation factory of repository is: JpaRepositoryFactoryBean, which will help us produce the implementation class of repository. So let’s take a look at the source code of JpaRepositoryFactoryBean directly and analyze its principle.

Drawing 4.png

Set a breakpoint and you will find that each Repository will build a JpaRepositoryFactory. After the JpaRepositoryFactory is loaded, the afterPropertiesSet() method will be executed to find the Fragment of the UserRepository (that is, our customized CustomizedUserRepositoryImpl), as shown below:

Drawing 5.png

Let’s take a look at all the methods in RepositoryFactory, as shown below. At first glance, the dynamic proxy generates the implementation class of Repository. Let’s enter this method and set a breakpoint to continue observing.

Drawing 6.png

Then we can see through breakpoints that fragments are placed in composition, and finally placed in advice, and finally the proxy class of our repository is generated. At this time we open the repository and take a closer look at the values inside.

Drawing 7.png

You can see the interfaces in the repository, which are the interface definitions in the userRepository we just tested.

Drawing 8.png

We can see that the sixth item in the advisors is the implementation class of our custom interface. From here we can draw the conclusion: Spring scans the interfaces and implementation classes of all repositories, and through the AOP aspects and dynamic proxy methods, we can know What is the implementation class of our custom interface.

Customized interfaces and implementation classes for different repositories require us to extend them manually, which is more suitable for different business scenarios that have their own repository implementations. Another way is to directly change the implementation class of the dynamic proxy, let’s see next.

Second method: Define the default Repository implementation class through @EnableJpaRepositories

When facing complex businesses, it is inevitable to customize some public methods or override some default implementations. For example: In many cases, online data is not allowed to be deleted, so at this time we need to override the deletion method in SimpleJpaRepository, replace it with update, and perform logical deletion instead of physical deletion. So let’s see what we should do next?

Step 1: As we mentioned above, use @EnableJpaRepositories to specify repositoryBaseClass. The code is as follows:

Copy code

@SpringBootApplication
@EnableWebMvc
@EnableJpaRepositories(repositoryImplementationPostfix = "Impl",repositoryBaseClass = CustomerBaseRepository.class)
public class JpaApplication {
   public static void main(String[] args) {
      SpringApplication.run(JpaApplication.class, args);
   }
}

It can be seen that when starting the project, we specify that the base class of our repositoryBaseClass is CustomerBaseRepository through @EnableJpaRepositories.

Step 2: Create CustomerBaseRepository and inherit SimpleJpaRepository.

After inheriting SimpleJpaRepository, we can directly override the delete method. The code is as follows:

Copy code

package com.example.jpa.example1.customized;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
@Transactional(readOnly = true)
public class CustomerBaseRepository<T extends BaseEntity,ID> extends SimpleJpaRepository<T,ID> {
    private final JpaEntityInformation<T, ?> entityInformation;
    private final EntityManager em;
    public CustomerBaseRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.entityInformation = entityInformation;
        this.em = entityManager;
    }
    public CustomerBaseRepository(Class<T> domainClass, EntityManager em) {
        super(domainClass, em);
        entityInformation = null;
        this.em = em;
    }
    //Overwrite the delete method, implement logical deletion, and replace it with the update method
    @Transactional
    @Override
    public void delete(T entity) {
        entity.setDeleted(Boolean.TRUE);
        em.merge(entity);
    }
}

It should be noted that you need to override the constructor of the parent class, receive the EntityManager, and assign it to the private variables in your own class.

Step 3: Write a test case to test it.

Copy code

@Test
public void testCustomizedBaseRepository() {
    User user = userRepository.findById(2L).get();
    userRepository.logicallyDelete(user);
    userRepository.delete(user);
    List<User> users = userRepository.findAll();
    Assertions.assertEquals(users.get(0).getDeleted(),Boolean.TRUE);
}

You can find that after we execute “delete”, the User in the database is still there, but deleted has become a deleted state. So why is this? Let’s analyze the principle.

Principle analysis

Or open the parent class method in RepositoryFactory, which will load our custom implementation class based on the repositoryBaseClass we configured in @EnableJpaRepositories. The key methods are as follows:

Drawing 9.png

Let’s also look at the breakpoints of the method just now, as follows:

Drawing 10.png

You can see that information has become the base class we extended, and the implementation class of the final generated repository has also been replaced by CustomerBaseRepository.

We have finished talking about the custom method, so in what practical scenarios will it be used? Let’s take a look.

What are the actual application scenarios?

In actual work, what scenarios would use custom Repository?

  1. First of all, it must be when we are making a framework and solving some common problems, such as logical deletion, as shown in our example above.
  2. In actual production, there are often such scenarios: the UUID query method is exposed to the outside world, but the Long type ID is exposed internally. At this time, we can customize a underlying implementation method of FindByIdOrUUID, and we can choose to use it in the customized Implemented in the Respository interface.
  3. Defining Query Methods and @Query cannot satisfy our queries, but when we want to use its method semantics, we can consider implementing different Respository implementation classes to satisfy our complex queries in different business scenarios. I have seen some teams use it like this, but I personally feel that it is generally not used. If you use it, it means that your code must have room for optimization, and the code should not be too complicated.

We talked about logical deletion above, and there is another way to do it using @SQLDelete. The usage is as follows:

Copy code

@SQLDelete(sql = "UPDATE user SET deleted = true where deleted =false and id = ?")
public class User implements Serializable {
....
}

This can be done without us customizing the Respository. The advantage of this method is flexibility, but the disadvantage is that we need to configure it on the entities one by one. You can freely choose the method according to the actual scenario.