Spring Data - Overriding default methods for some

2020-06-04 02:20发布

问题:

I am just staring with spring-data and spring-data-rest and I really want to take advantage of what these tools have to offer. For the most case the base functionality is perfect for my use case however there are some cases where I need to customize the underlying functionality quite a bit, and selectively assign some repositories to inherit the customized functionality I am after.

To explain the problem a bit better, in spring-data there are 2 possible interfaces which you can inherit functionality from, CrudRepository or PagingAndSortingRepository. I want to add a third called lets say PesimisticRepository

All the PesimisticRepository does is to handle the notion of a deleted @Entity differently. A deleted entity is one where its deleted property is NOT NULL. This means that an @Entity which can be handled by a PesimisticRepository has to have a deleted property.

All this is possible, I have actually implemented this a couple of years ago. (You can check it out here in case you are interested)

My current attempt using spring-data is the following:

An extension of the PagingAndSortingRepository

package com.existanze.xxx.datastore.repositories;

import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;

import java.io.Serializable;


@NoRepositoryBean
public interface PesimisticRepository<T,ID extends Serializable> extends PagingAndSortingRepository<T,ID> {
}

For which I provide a default implementation extending JPARepository

package com.existanze.xxx.datastore.repositories;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.io.Serializable;
import java.util.Date;


public class JpaPesimisticRepository<T,ID extends Serializable> extends SimpleJpaRepository<T,ID> implements PesimisticRepository<T,ID> {


    private final EntityManager entityManager;

    public JpaPesimisticRepository(Class<T> domainClass, EntityManager em) {
        super(domainClass, em);
        this.entityManager = em;
    }

    @Override
    @Transactional
    public Page<T> findAll(Specification<T> spec, Pageable pageable) {

        CriteriaBuilder cb = this.entityManager.getCriteriaBuilder();
        CriteriaQuery<T> criteriaQuery = cb.createQuery(getDomainClass());
        Root<T> from = criteriaQuery.from(this.getDomainClass());
        Predicate deleted = cb.equal(from.get("deleted"), cb.nullLiteral(Date.class));
        criteriaQuery.select(from).where(deleted);
        TypedQuery<T> query = this.entityManager.createQuery(criteriaQuery);
        return pageable == null ? new PageImpl<T>(query.getResultList()) : readPage(query, pageable, spec);

    }

}

And then for any bean for which I wish to handle the deletion using the pessimistic method I define it as such

package com.existanze.xxx.datastore.repositories;

import com.existanze.xxx.domain.Phone;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;


@RepositoryRestResource
public interface PhoneRepository extends PesimisticRepository<Phone,Integer> {



}

It is important to explain why I wish to override these methods instead of providing custom ones, like findAllButDeleted. The reason is because I also want the pessimistic delete to trickle down to spring-data-rest. So that the HTTP endpoints generated will not need any form of customization.

This seems to work only for the findAll method. However for the rest of the methods the current exception is thrown.

$ curl http://localhost:8881/phones/23

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/>
<title>Error 500 </title>
</head>
<body>
<h2>HTTP ERROR: 500</h2>
<p>Problem accessing /phones/23. Reason:
<pre>    org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessApiUsageException: object is not an instance of declaring class; nested exception is java.lang.IllegalArgumentException: object is not an instance of declaring class</pre></p>
<hr /><i><small>Powered by Jetty://</small></i>
</body>
</html>

Furthermore, I have read the documentation which allows you to change the default JpaRepository for all Repositories, but again I need to do this on a per repository basis.

I hope I have been descriptive enough. Please let me know in the comments section if there is something that needs better explanation.

回答1:

You can create a custom repository, something like this:

package com.brunocesar.custom.repository.support;

import java.io.Serializable;

import javax.persistence.EntityManager;

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

import com.brunocesar.custom.entity.CustomAbstractEntity;

@NoRepositoryBean
public interface CustomGenericRepository<E extends CustomAbstractEntity, PK extends Serializable> extends
        JpaRepository<E, PK>, JpaSpecificationExecutor<E> {

    EntityManager getEntityManager();

}

package com.brunocesar.custom.repository.support.impl;

import java.io.Serializable;
import java.util.Calendar;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import com.brunocesar.custom.entity.CustomAbstractEntity;
import com.brunocesar.custom.repository.support.CustomGenericRepository;

@Transactional(readOnly = true)
public class CustomGenericRepositoryImpl<E extends CustomAbstractEntity, PK extends Serializable> extends
        SimpleJpaRepository<E, PK> implements CustomGenericRepository<E, PK> {

    private final EntityManager entityManager;
    private final JpaEntityInformation<E, ?> entityInformation;

    public CustomGenericRepositoryImpl(final JpaEntityInformation<E, ?> entityInformation,
            final EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.entityManager = entityManager;
        this.entityInformation = entityInformation;
    }

    @Override
    @Transactional
    public void delete(final E entity) {
        Assert.notNull(entity, "Entity object must not be null!");
        entity.setChangeDate(Calendar.getInstance().getTime());
        entity.setDeleted(true);
    }

    @Override
    public List<E> findAll() {
        return super.findAll(this.isRemoved());
    }

    @Override
    public E findOne(final PK pk) {
        return this.findOne(this.isRemovedByID(pk));
    }

    private Specification<E> isRemoved() {
        return new Specification<E>() {

            @Override
            public Predicate toPredicate(final Root<E> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
                return cb.isFalse(root.<Boolean> get("deleted"));
            }

        };
    }

    private Specification<E> isRemovedByID(final PK pk) {
        return new Specification<E>() {

            @Override
            public Predicate toPredicate(Root<E> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                final Predicate id = cb.equal(root.get("id"), pk);
                final Predicate hidden = cb.isFalse(root.<Boolean> get("deleted"));
                return cb.and(id, hidden);
            }

        };
    }

    @Override
    public EntityManager getEntityManager() {
        return this.entityManager;
    }

    protected JpaEntityInformation<E, ?> getEntityInformation() {
        return this.entityInformation;
    }

}

You will need too a custom factory bean to setup your custom repositories. Look like this:

package com.brunocesar.custom.repository.support.factory;

import java.io.Serializable;

import javax.persistence.EntityManager;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;

import com.brunocesar.custom.repository.support.impl.CustomGenericRepositoryImpl;

public class CustomGenericRepositoryFactoryBean<T extends JpaRepository<S, ID>, S, ID extends Serializable> extends
        JpaRepositoryFactoryBean<T, S, ID> {

    @Override
    protected RepositoryFactorySupport createRepositoryFactory(final EntityManager entityManager) {
        return new RepositoryFactory(entityManager);
    }

    private static class RepositoryFactory extends JpaRepositoryFactory {

        public RepositoryFactory(final EntityManager entityManager) {
            super(entityManager);
        }

        @Override
        @SuppressWarnings({"unchecked", "rawtypes"})
        protected <T, ID extends Serializable> SimpleJpaRepository<?, ?> getTargetRepository(
                final RepositoryMetadata metadata, final EntityManager entityManager) {
            final JpaEntityInformation<?, Serializable> entityInformation = this.getEntityInformation(metadata
                    .getDomainType());
            return new CustomGenericRepositoryImpl(entityInformation, entityManager);
        }

        @Override
        protected Class<?> getRepositoryBaseClass(final RepositoryMetadata metadata) {
            return CustomGenericRepositoryImpl.class;
        }

    }

}

And finally, the application context configuration.

Common repositories configuration look like this:

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
        p:dataSource-ref="dataSource" p:jpaProperties-ref="jpaProperties" p:jpaVendorAdapter-ref="jpaVendorAdapter"/>

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" p:entityManagerFactory-ref="entityManagerFactory" />

<jpa:repositories base-package="com.brunocesar.repository"
    transaction-manager-ref="transactionManager" entity-manager-factory-ref="entityManagerFactory" />

And custom, like this (you can or not use separeted EMF and transaction manager):

<bean id="entityManagerFactoryCustom" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
    p:dataSource-ref="dataSource" p:jpaProperties-ref="jpaProperties" p:jpaVendorAdapter-ref="jpaVendorAdapter"/>

<bean id="transactionManagerCustom" class="org.springframework.orm.jpa.JpaTransactionManager" p:entityManagerFactory-ref="entityManagerFactoryCustom" />

<jpa:repositories base-package="com.brunocesar.custom.repository,com.brunocesar.custom.repository.support"
    factory-class="com.brunocesar.custom.repository.support.factory.CustomGenericRepositoryFactoryBean"
    transaction-manager-ref="transactionManagerCustom" entity-manager-factory-ref="entityManagerFactoryCustom" />

Example 1, using JpaRepository:

package com.brunocesar.repository;

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

import com.brunocesar.entity.CommonEntity;

@Repository
public interface CommonRepository extends JpaRepository<CommonEntity, Long> {

}

Example 2, using custom repository:

package com.brunocesar.custom.repository;

import org.springframework.stereotype.Repository;

import com.brunocesar.custom.entity.CustomEntity;
import com.brunocesar.custom.repository.support.CustomGenericRepository;

@Repository
public interface CustomRepository extends CustomGenericRepository<CustomEntity, Long> {

}

This is part of what I usually do. If you need, I can create a basic application as an example.