Должен ли я использовать методы Java 8 по умолчанию для внедренных вручную методов Spring Data repository?

При использовании нового выпуска Spring Data Evans приятно использовать некоторые из приятных вещей, которые поставляются с java 8. Одна из них - это реализация по умолчанию в интерфейсах. В репозитории ниже используется QueryDSL, чтобы сделать запрос безопасным.

Моя проблема в том, что раньше, когда я писал это, я использовал шаблон отдельного интерфейса UserRepositoryCustom для findByLogin, а затем еще один класс UserRepositoryImpl, и в этом классе я бы имел @PersistenceContext, чтобы получить текущий EntityManager.

Как получить EntityManager, когда у меня нет класса? Возможно ли это?

@Repository
public interface UserRepository extends JpaRepository<User, UUID> {

    final QUser qUser = QUser.user;

    // How do I get the entityManager since this is a interface, i cannot have any variables?
    //@PersistenceContext
    //EntityManager entityManager;

    public default Optional<User> findByLogin(String login) {
        JPAQuery query = new JPAQuery(entityManager);
        User user = query
                .from(qUser)
                .where(
                        qUser.deleter.isNull(),
                        qUser.locked.isFalse(),
                        qUser.login.equalsIgnoreCase(login)
                )
                .singleResult(qUser);

        return Optional.ofNullable(user);
    }
}

Ответы

Ответ 1

Способы по умолчанию должны использоваться только для делегирования вызовов другим методам репозитория. Методы по умолчанию - по определению - не могут получить доступ к какому-либо состоянию экземпляра (поскольку интерфейс не имеет). Они могут делегировать другим методам интерфейса или вызывать статические из других классов.

Собственно, использование пользовательской реализации, описанной в справочной документации является правильным подходом. Вот короткая версия для справки (в случае, если другие задаются вопросом тоже):

/**
 * Interface for methods you want to implement manually.
 */
interface UserRepositoryCustom {
  Optional<User> findByLogin(String login);
}

/**
 * Implementation of exactly these methods.
 */
class UserRepositoryImpl extends QueryDslRepositorySupport implements UserRepositoryCustom {

  private static final QUser USER = QUser.user;

  @Override
  public Optional<User> findByLogin(String login) {

    return Optional.ofNullable(
      from(USER).
      where(
        USER.deleter.isNull(),
        USER.locked.isFalse(), 
        USER.login.equalsIgnoreCase(login)).
      singleResult(USER));
  }
}

/**
 * The main repository interface extending the custom one so that the manually
 * implemented methods get "pulled" into the API.
 */
public interface UserRepository extends UserRepositoryCustom, 
  CrudRepository<User, Long> { … }

Имейте в виду, что соглашения об именах важны здесь (но при необходимости могут быть настроены). Расширяя QueryDslRepositorySupport, вы получаете доступ к методу from(…), чтобы вам не приходилось взаимодействовать с EntityManager самостоятельно.

В качестве альтернативы вы можете позволить UserRepository реализовать QueryDslPredicateExecutor и передать предикаты извне репозитория, но это позволит вам в конечном итоге с клиентами, которые должны работать с Querydsl (что может быть нежелательно), плюс вы не можете получить Optional тип обертки OOTB.

Ответ 2

Вы не получаете EntityManager в интерфейсе, хотя вы можете обойти его, выполнив поиск.

Но почему вы это делаете? Spring Данные JPA уже поддерживает возвращаемый тип Optional, поэтому вам не нужно его реализовывать. Spring Данные сделают это для вас.

public interface UserRepository extends JpaRepository<User, UUID> {

    Optional<User> findByLoginIgnoreCase(String login) {
}

Код выше должен быть всем, что вам нужно. Вы даже можете указать запрос с помощью @Query, если он вам понадобится.

Образец можно найти здесь.

Ответ 3

То, что я закончил, - это создание базы репозитория, которая имеет getEntityManager()

Но не все так просто, чтобы базовый класс работал с spring boot

// DomainRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;

import javax.persistence.EntityManager;
import java.io.Serializable;

@NoRepositoryBean
public interface DomainRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {

    EntityManager getEntityManager();

}

Тогда реализация

// DomainRepositoryImpl.java
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;

import javax.persistence.EntityManager;
import java.io.Serializable;

public class DomainRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements DomainRepository<T, ID> {

    private EntityManager entityManager;

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

    public EntityManager getEntityManager() {
        return entityManager;
    }
}

Но тогда spring должен знать, как создавать репозитории домена, поэтому нам нужно создать factory.

// DomainRepositoryFactoryBean.java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;

import javax.persistence.EntityManager;
import java.io.Serializable;

public class DomainRepositoryFactoryBean<R extends JpaRepository<T, I>, T, I extends Serializable> extends JpaRepositoryFactoryBean<R, T, I> {

    protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
        return new RepositoryBaseFactory(entityManager);
    }

    private static class RepositoryBaseFactory<T, I extends Serializable> extends JpaRepositoryFactory {

        private EntityManager entityManager;

        public RepositoryBaseFactory(EntityManager entityManager) {
            super(entityManager);

            this.entityManager = entityManager;
        }

        protected Object getTargetRepository(RepositoryMetadata metadata) {

            return new DomainRepositoryImpl<T, I>((Class<T>) metadata.getDomainType(), entityManager);
        }

        protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {

            // The RepositoryMetadata can be safely ignored, it is used by the JpaRepositoryFactory
            //to check for QueryDslJpaRepository which is out of scope.
            return DomainRepository.class;
        }
    }
}

И затем сообщить spring boot использовать этот factory при создании репозиториев

// DomainConfig.java
@Configuration
@EnableJpaRepositories(repositoryFactoryBeanClass = DomainRepositoryFactoryBean.class, basePackages = {"com.mysite.domain"})
@EnableTransactionManagement
public class DomainConfig {
}

а затем измените UserRepository, чтобы использовать его.

@Repository
public interface UserRepository extends DomainRepository<User, UUID> {
    public default Optional<User> findByLogin(String login) {
        JPAQuery query = new JPAQuery(getEntityManager());
        ...
    }
}