Como funciona o FetchMode no Spring Data JPA

91

Eu tenho uma relação entre três objetos de modelo em meu projeto (trechos de modelo e repositório no final do post.

Quando eu ligo, PlaceRepository.findByIdele dispara três consultas selecionadas:

("sql")

  1. SELECT * FROM place p where id = arg
  2. SELECT * FROM user u where u.id = place.user.id
  3. SELECT * FROM city c LEFT OUTER JOIN state s on c.woj_id = s.id where c.id = place.city.id

Esse é um comportamento bastante incomum (para mim). Pelo que eu posso dizer depois de ler a documentação do Hibernate, ele sempre deve usar consultas JOIN. Não há diferença nas consultas quando FetchType.LAZYalteradas para FetchType.EAGERna Placeclasse (consulta com SELECT adicional), o mesmo para a Cityclasse quando FetchType.LAZYalterado para FetchType.EAGER(consulta com JOIN).

Quando eu uso a CityRepository.findByIdsupressão de disparos, duas seleções:

  1. SELECT * FROM city c where id = arg
  2. SELECT * FROM state s where id = city.state.id

Meu objetivo é ter um comportamento igual em todas as situações (embora seja sempre JOIN ou SELECT, JOIN preferencial).

Definições de modelo:

Lugar, colocar:

@Entity
@Table(name = "place")
public class Place extends Identified {

    @Fetch(FetchMode.JOIN)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_user_author")
    private User author;

    @Fetch(FetchMode.JOIN)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "area_city_id")
    private City city;
    //getters and setters
}

Cidade:

@Entity
@Table(name = "area_city")
public class City extends Identified {

    @Fetch(FetchMode.JOIN)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "area_woj_id")
    private State state;
    //getters and setters
}

Repositórios:

PlaceRepository

public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom {
    Place findById(int id);
}

UserRepository:

public interface UserRepository extends JpaRepository<User, Long> {
        List<User> findAll();
    User findById(int id);
}

CityRepository:

public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom {    
    City findById(int id);
}
SirKometa
fonte
Dê uma olhada em 5 maneiras de inicializar relacionamentos preguiçosos: pensamentos-on-java.org/…
Grigory Kislin

Respostas:

109

Acho que Spring Data ignora o FetchMode. Eu sempre uso as anotações @NamedEntityGraphe @EntityGraphao trabalhar com Spring Data

@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
  attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {

  // default fetch mode is lazy.
  @ManyToMany
  List<GroupMember> members = new ArrayList<GroupMember>();

  
}

@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

  @EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
  GroupInfo getByGroupName(String name);

}

Verifique a documentação aqui

wesker317
fonte
1
Eu não pareço funcionar para mim. Quer dizer, funciona, mas ... Quando eu anoto o repositório com '@EntityGraph', ele não funciona sozinho (normalmente). Por exemplo: `Coloque findById (int id);` funciona, mas List<Place> findAll();termina com a Exceção org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!. Funciona quando eu adiciono manualmente @Query("select p from Place p"). Parece uma solução alternativa.
SirKometa
Talvez seja necessário trabalhar em findAll (), já que é um método existente na interface JpaRepository, enquanto o outro método "findById" é um método de consulta personalizado gerado em tempo de execução.
wesker317
Decidi marcar esta como a resposta adequada, pois é a melhor. Mas não é perfeito. Funciona na maioria dos cenários, mas até agora notei bugs em spring-data-jpa com EntityGraphs mais complexos. Obrigado :)
SirKometa
2
@EntityGraphé quase ununsable em cenários reais, uma vez que não pode ser especificado que tipo de Fetchnós queremos usar ( JOIN, SUBSELECT, SELECT, BATCH). Isso em combinação com a @OneToManyassociação e faz com que o Hibernate busque toda a tabela para a memória, mesmo se usarmos a consulta MaxResults.
Ondrej Bozek
1
Obrigado, gostaria de dizer que as consultas JPQL podem substituir a estratégia de busca padrão com a política de busca de seleção .
adrhc
51

Em primeiro lugar, @Fetch(FetchMode.JOIN)e @ManyToOne(fetch = FetchType.LAZY)são antagônicos, um instruindo um Fetch EAGER, enquanto o outro sugerindo um Fetch LAZY.

A busca rápida raramente é uma boa escolha e, para um comportamento previsível, é melhor usar a JOIN FETCHdiretiva de tempo de consulta :

public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom {

    @Query(value = "SELECT p FROM Place p LEFT JOIN FETCH p.author LEFT JOIN FETCH p.city c LEFT JOIN FETCH c.state where p.id = :id")
    Place findById(@Param("id") int id);
}

public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom { 
    @Query(value = "SELECT c FROM City c LEFT JOIN FETCH c.state where c.id = :id")   
    City findById(@Param("id") int id);
}
Vlad Mihalcea
fonte
3
HÁ maneira de obter o mesmo resultado com Criteria API e Spring Data Especificações?
svlada
2
Não é a parte de busca, que requer perfis de busca JPA.
Vlad Mihalcea
Vlad Mihalcea, você poderia compartilhar o link com um exemplo de como fazer isso usando os critérios (especificação) do Spring Data JPA? Por favor
Yan Khonski de
Não tenho nenhum exemplo, mas você certamente pode encontrar um nos tutoriais do Spring Data JPA.
Vlad Mihalcea
se estiver usando o tempo de consulta ... você ainda precisará definir @OneToMany ... etc na entidade?
Eric Huang,
19

Spring-jpa cria a consulta usando o gerenciador de entidades, e o Hibernate irá ignorar o modo de busca se a consulta foi construída pelo gerenciador de entidades.

A seguir está a solução que usei:

  1. Implementar um repositório personalizado que herda de SimpleJpaRepository

  2. Substitua o método getQuery(Specification<T> spec, Sort sort):

    @Override
    protected TypedQuery<T> getQuery(Specification<T> spec, Sort sort) { 
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> query = builder.createQuery(getDomainClass());
    
        Root<T> root = applySpecificationToCriteria(spec, query);
        query.select(root);
    
        applyFetchMode(root);
    
        if (sort != null) {
            query.orderBy(toOrders(sort, root, builder));
        }
    
        return applyRepositoryMethodMetadata(entityManager.createQuery(query));
    }

    No meio do método, adicione applyFetchMode(root);para aplicar o modo de busca, para fazer o Hibernate criar a consulta com a junção correta.

    (Infelizmente, precisamos copiar todo o método e métodos privados relacionados da classe base porque não havia outro ponto de extensão.)

  3. Implementar applyFetchMode:

    private void applyFetchMode(Root<T> root) {
        for (Field field : getDomainClass().getDeclaredFields()) {
    
            Fetch fetch = field.getAnnotation(Fetch.class);
    
            if (fetch != null && fetch.value() == FetchMode.JOIN) {
                root.fetch(field.getName(), JoinType.LEFT);
            }
        }
    }
dream83619
fonte
Infelizmente, isso não funciona para consultas geradas usando o nome do método do repositório.
Ondrej Bozek
você poderia adicionar todas as declarações de importação? obrigado.
granadaCoder
3

" FetchType.LAZY" só será acionado para a tabela primária. Se em seu código você chamar qualquer outro método que tenha uma dependência de tabela pai, ele disparará uma consulta para obter as informações dessa tabela. (FIRES MULTIPLE SELECT)

" FetchType.EAGER" criará junção de todas as tabelas, incluindo tabelas pai relevantes diretamente. (USOS JOIN)

Quando usar: Suponha que você precise obrigatoriamente usar o informartion da tabela pai dependente e escolha FetchType.EAGER. Se você só precisa de informações para determinados registros, use FetchType.LAZY.

Lembre-se, FetchType.LAZYprecisa de uma fábrica de sessão de banco de dados ativa no local em seu código onde você escolher recuperar as informações da tabela pai.

Por exemplo, para LAZY:

.. Place fetched from db from your dao loayer
.. only place table information retrieved
.. some code
.. getCity() method called... Here db request will be fired to get city table info

Referência adicional

Godwin
fonte
Curiosamente, essa resposta me colocou no caminho certo para usar, NamedEntityGraphjá que eu queria um gráfico de objeto não hidratado.
JJ Zabkar de
esta resposta merece mais votos positivos. É sucinto e me ajudou muito a entender por que eu estava vendo muitas consultas "acionadas magicamente" ... muito obrigado!
Clint Eastwood
3

O modo de busca só funcionará ao selecionar o objeto por id, ou seja, usando entityManager.find(). Como o Spring Data sempre criará uma consulta, a configuração do modo de busca não terá uso para você. Você pode usar consultas dedicadas com fetch joins ou usar gráficos de entidade.

Quando você deseja o melhor desempenho, deve selecionar apenas o subconjunto dos dados de que realmente precisa. Para fazer isso, geralmente é recomendado usar uma abordagem DTO para evitar a busca de dados desnecessários, mas isso geralmente resulta em uma grande quantidade de código clichê sujeito a erros, já que você precisa definir uma consulta dedicada que constrói seu modelo DTO por meio de um JPQL expressão do construtor.

As projeções do Spring Data podem ajudar aqui, mas em algum ponto você precisará de uma solução como Blaze-Persistence Entity Views, que torna isso muito fácil e tem muito mais recursos em sua capa que serão úteis! Você acabou de criar uma interface DTO por entidade onde os getters representam o subconjunto de dados de que você precisa. Uma solução para o seu problema pode ser assim

@EntityView(Identified.class)
public interface IdentifiedView {
    @IdMapping
    Integer getId();
}

@EntityView(Identified.class)
public interface UserView extends IdentifiedView {
    String getName();
}

@EntityView(Identified.class)
public interface StateView extends IdentifiedView {
    String getName();
}

@EntityView(Place.class)
public interface PlaceView extends IdentifiedView {
    UserView getAuthor();
    CityView getCity();
}

@EntityView(City.class)
public interface CityView extends IdentifiedView {
    StateView getState();
}

public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom {
    PlaceView findById(int id);
}

public interface UserRepository extends JpaRepository<User, Long> {
    List<UserView> findAllByOrderByIdAsc();
    UserView findById(int id);
}

public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom {    
    CityView findById(int id);
}

Isenção de responsabilidade, sou o autor de Blaze-Persistence, então posso ser tendencioso.

Christian Beikov
fonte
2

I elaborado em dream83619 resposta para torná-lo lidar com Hibernate aninhados @Fetchanotações. Usei o método recursivo para encontrar anotações em classes associadas aninhadas.

Portanto, você deve implementar o repositório customizado e o getQuery(spec, domainClass, sort)método de substituição . Infelizmente, você também deve copiar todos os métodos privados referenciados :(.

Aqui está o código, os métodos privados copiados são omitidos.
EDIT: Adicionados métodos privados restantes.

@NoRepositoryBean
public class EntityGraphRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> {

    private final EntityManager em;
    protected JpaEntityInformation<T, ?> entityInformation;

    public EntityGraphRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.em = entityManager;
        this.entityInformation = entityInformation;
    }

    @Override
    protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<S> query = builder.createQuery(domainClass);

        Root<S> root = applySpecificationToCriteria(spec, domainClass, query);

        query.select(root);
        applyFetchMode(root);

        if (sort != null) {
            query.orderBy(toOrders(sort, root, builder));
        }

        return applyRepositoryMethodMetadata(em.createQuery(query));
    }

    private Map<String, Join<?, ?>> joinCache;

    private void applyFetchMode(Root<? extends T> root) {
        joinCache = new HashMap<>();
        applyFetchMode(root, getDomainClass(), "");
    }

    private void applyFetchMode(FetchParent<?, ?> root, Class<?> clazz, String path) {
        for (Field field : clazz.getDeclaredFields()) {
            Fetch fetch = field.getAnnotation(Fetch.class);

            if (fetch != null && fetch.value() == FetchMode.JOIN) {
                FetchParent<?, ?> descent = root.fetch(field.getName(), JoinType.LEFT);
                String fieldPath = path + "." + field.getName();
                joinCache.put(path, (Join) descent);

                applyFetchMode(descent, field.getType(), fieldPath);
            }
        }
    }

    /**
     * Applies the given {@link Specification} to the given {@link CriteriaQuery}.
     *
     * @param spec can be {@literal null}.
     * @param domainClass must not be {@literal null}.
     * @param query must not be {@literal null}.
     * @return
     */
    private <S, U extends T> Root<U> applySpecificationToCriteria(Specification<U> spec, Class<U> domainClass,
        CriteriaQuery<S> query) {

        Assert.notNull(query);
        Assert.notNull(domainClass);
        Root<U> root = query.from(domainClass);

        if (spec == null) {
            return root;
        }

        CriteriaBuilder builder = em.getCriteriaBuilder();
        Predicate predicate = spec.toPredicate(root, query, builder);

        if (predicate != null) {
            query.where(predicate);
        }

        return root;
    }

    private <S> TypedQuery<S> applyRepositoryMethodMetadata(TypedQuery<S> query) {
        if (getRepositoryMethodMetadata() == null) {
            return query;
        }

        LockModeType type = getRepositoryMethodMetadata().getLockModeType();
        TypedQuery<S> toReturn = type == null ? query : query.setLockMode(type);

        applyQueryHints(toReturn);

        return toReturn;
    }

    private void applyQueryHints(Query query) {
        for (Map.Entry<String, Object> hint : getQueryHints().entrySet()) {
            query.setHint(hint.getKey(), hint.getValue());
        }
    }

    public Class<T> getEntityType() {
        return entityInformation.getJavaType();
    }

    public EntityManager getEm() {
        return em;
    }
}
Ondrej Bozek
fonte
Estou tentando sua solução, mas tenho uma variável de metadados privada em um dos métodos de cópia que está causando problemas. Você pode compartilhar o código final?
Homer1980ar
A busca recursiva não funciona. Se eu tiver OneToMany, ele passará java.util.List para a próxima iteração
antohoho
não testei bem ainda, mas acho que deveria ser algo assim ((Join) descent) .getJavaType () em vez de field.getType () quando chamada recursivamente applyFetchMode
antohoho
2

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
neste link:

se você estiver usando JPA no topo do Hibernate, não há como definir o FetchMode usado pelo Hibernate para JOIN; no entanto, se você estiver usando JPA no topo do Hibernate, não há como definir o FetchMode usado pelo Hibernate para JOIN.

A biblioteca Spring Data JPA fornece uma API de especificações de design orientada por domínio que permite controlar o comportamento da consulta gerada.

final long userId = 1;

final Specification<User> spec = new Specification<User>() {
   @Override
    public Predicate toPredicate(final Root<User> root, final 
     CriteriaQuery<?> query, final CriteriaBuilder cb) {
    query.distinct(true);
    root.fetch("permissions", JoinType.LEFT);
    return cb.equal(root.get("id"), userId);
 }
};

List<User> users = userRepository.findAll(spec);
kafkas
fonte
2

De acordo com Vlad Mihalcea (consulte https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ ):

As consultas JPQL podem substituir a estratégia de busca padrão. Se não declararmos explicitamente o que queremos buscar usando as diretivas de busca de junção interna ou esquerda, a política de busca de seleção padrão é aplicada.

Parece que a consulta JPQL pode substituir sua estratégia de busca declarada, então você terá que usar join fetchpara carregar avidamente alguma entidade referenciada ou simplesmente carregar por id com EntityManager (que obedecerá sua estratégia de busca, mas pode não ser uma solução para seu caso de uso )

adrhc
fonte