Como buscar associações FetchType.LAZY com JPA e Hibernate em um Spring Controller

146

Eu tenho uma classe Person:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToMany(fetch = FetchType.LAZY)
    private List<Role> roles;
    // etc
}

Com uma relação muitos-para-muitos que é preguiçosa.

No meu controlador eu tenho

@Controller
@RequestMapping("/person")
public class PersonController {
    @Autowired
    PersonRepository personRepository;

    @RequestMapping("/get")
    public @ResponseBody Person getPerson() {
        Person person = personRepository.findOne(1L);
        return person;
    }
}

E o PersonRepository é apenas esse código, escrito de acordo com este guia

public interface PersonRepository extends JpaRepository<Person, Long> {
}

No entanto, neste controlador, eu realmente preciso dos dados preguiçosos. Como posso acionar o carregamento?

Tentar acessá-lo falhará com

falhou ao inicializar preguiçosamente uma coleção de funções: no.dusken.momus.model.Person.roles, não foi possível inicializar o proxy - nenhuma Sessão

ou outras exceções, dependendo do que eu tente.

Minha descrição xml , caso necessário.

Obrigado.

Matsemann
fonte
Você pode escrever um método, que criará uma consulta para buscar um Personobjeto, com algum parâmetro? Nele Query, inclua a fetchcláusula e carregue Rolestambém para a pessoa.
22813 SudoRahul

Respostas:

206

Você precisará fazer uma chamada explícita na coleção lenta para inicializá-la (a prática comum é chamar .size()para esse fim). No Hibernate, existe um método dedicado para isso ( Hibernate.initialize()), mas o JPA não tem equivalente. É claro que você precisará garantir que a chamada seja feita, quando a sessão ainda estiver disponível, então anote seu método do controlador @Transactional. Uma alternativa é criar uma camada de Serviço intermediária entre o Controlador e o Repositório que possa expor métodos que inicializam coleções preguiçosas.

Atualizar:

Observe que a solução acima é fácil, mas resulta em duas consultas distintas no banco de dados (uma para o usuário e outra para suas funções). Se você deseja obter melhor desempenho, inclua o seguinte método na interface do repositório do Spring Data JPA:

public interface PersonRepository extends JpaRepository<Person, Long> {

    @Query("SELECT p FROM Person p JOIN FETCH p.roles WHERE p.id = (:id)")
    public Person findByIdAndFetchRolesEagerly(@Param("id") Long id);

}

Esse método usará a cláusula de junção de busca do JPQL para carregar ansiosamente a associação de funções em uma única viagem de ida e volta ao banco de dados e, portanto, reduzirá a penalidade de desempenho incorrida pelas duas consultas distintas na solução acima.

zagyi
fonte
3
Observe que essa é uma solução fácil, mas resulta em duas consultas distintas no banco de dados (uma para o usuário e outra para suas funções). Se você deseja obter melhor desempenho, tente escrever um método dedicado que busque ansiosamente o usuário e suas funções associadas em uma única etapa usando o JPQL ou a API de Critérios, conforme sugerido por outros.
zagyi
Agora pedi um exemplo para a resposta de José, tenho que admitir que não entendo completamente.
Matsemann 12/03/13
Verifique uma possível solução para o método de consulta desejado na minha resposta atualizada.
zagyi
7
É interessante notar que, se você simplesmente joinsem o fetch, o conjunto será retornado com initialized = false; portanto, ainda emitindo uma segunda consulta assim que o conjunto for acessado. fetché essencial para garantir que o relacionamento esteja completamente carregado e evitar a segunda consulta.
FGreg
Parece que o problema de fazer as duas coisas e buscar e ingressar é que os critérios de predicação da associação são ignorados e você acaba obtendo tudo na lista ou mapa. Se você quiser tudo, use uma busca, se quiser algo específico, uma junção, mas a, como foi dito, a junção estará vazia. Isso anula o objetivo de usar o carregamento .LAZY.
precisa saber é o seguinte
37

Embora essa seja uma postagem antiga, considere usar @NamedEntityGraph (Javax Persistence) e @EntityGraph (Spring Data JPA). A combinação funciona.

Exemplo

@Entity
@Table(name = "Employee", schema = "dbo", catalog = "ARCHO")
@NamedEntityGraph(name = "employeeAuthorities",
            attributeNodes = @NamedAttributeNode("employeeGroups"))
public class EmployeeEntity implements Serializable, UserDetails {
// your props
}

e depois o repo da primavera como abaixo

@RepositoryRestResource(collectionResourceRel = "Employee", path = "Employee")
public interface IEmployeeRepository extends PagingAndSortingRepository<EmployeeEntity, String>           {

    @EntityGraph(value = "employeeAuthorities", type = EntityGraphType.LOAD)
    EmployeeEntity getByUsername(String userName);

}
rakpan
fonte
1
Observe que isso @NamedEntityGraphfaz parte da API JPA 2.1, que não é implementada no Hibernate antes da versão 4.3.0.
NaXa
2
@EntityGraph(attributePaths = "employeeGroups")pode ser usado diretamente em um Spring Data Repository para anotar um método sem a necessidade de um @NamedEntityGraphno seu código @Entity - less, fácil de entender quando você abre o repositório.
Desislav Kamenov
13

Você tem algumas opções

  • Escreva um método no repositório que retorne uma entidade inicializada como o RJ sugeriu.

Mais trabalho, melhor desempenho.

  • Use OpenEntityManagerInViewFilter para manter a sessão aberta para toda a solicitação.

Menos trabalho, geralmente aceitável em ambientes da web.

  • Use uma classe auxiliar para inicializar entidades quando necessário.

Menos trabalho, útil quando o OEMIV não está disponível, por exemplo, em um aplicativo Swing, mas também pode ser útil em implementações de repositório para inicializar qualquer entidade de uma só vez.

Para a última opção, escrevi uma classe de utilitário, JpaUtils para iniciar entidades em algum deph.

Por exemplo:

@Transactional
public class RepositoryHelper {

    @PersistenceContext
    private EntityManager em;

    public void intialize(Object entity, int depth) {
        JpaUtils.initialize(em, entity, depth);
    }
}
Jose Luis Martin
fonte
Como todos os meus pedidos são simples chamadas REST sem renderização, etc., a transação é basicamente o meu pedido inteiro. Obrigado pela sua contribuição.
Matsemann 12/03/13
Como eu faço o primeiro? Eu sei como escrever uma consulta, mas não como fazer o que você diz. Você poderia mostrar um exemplo? Seria muito útil.
Matsemann 12/03/2013
zagyi forneceu um exemplo em sua resposta, obrigado por me indicar a direção certa de qualquer maneira.
Matsemann 31/03
Não sei como sua turma seria chamada! não completou solução outros resíduos tempo
Shady Sherif
Use OpenEntityManagerInViewFilter para manter a sessão aberta para toda a solicitação. - Má ideia. Eu faria uma solicitação adicional para buscar todas as coleções para minhas entidades.
Yan Khonski
6

Acho que você precisa do OpenSessionInViewFilter para manter sua sessão aberta durante a renderização de exibição (mas não é uma boa prática).

Michail Nikolaev
fonte
1
Como não estou usando JSP nem nada, apenas criando uma API REST, o @Transactional fará por mim. Mas será útil em outros momentos. Obrigado.
Matsemann
@Matsemann Eu sei que é tarde agora ... mas você pode fazer uso de OpenSessionInViewFilter mesmo em um controlador, bem como a sessão que funcionará até uma resposta é compilado ...
Vishwas Shashidhar
@ Matsemann Thanks! A anotação transacional fez o truque para mim! fyi: Até funciona se você apenas anotar a superclasse de uma classe de descanso.
que você
3

Dados da Primavera JpaRepository

Os dados do Spring JpaRepositorydefinem os dois métodos a seguir:

  • getOne, que retorna um proxy de entidade adequado para definir uma associação @ManyToOneou @OneToOnepai ao persistir em uma entidade filho .
  • findById, que retorna a entidade POJO após executar a instrução SELECT que carrega a entidade da tabela associada

No entanto, no seu caso, você não chamar tanto getOneou findById:

Person person = personRepository.findOne(1L);

Então, suponho que o findOnemétodo é um método que você definiu no PersonRepository. No entanto, o findOnemétodo não é muito útil no seu caso. Como você precisa buscar a coleção Personjunto com o is roles, é melhor usar um findOneWithRolesmétodo.

Métodos personalizados de dados de primavera

Você pode definir uma PersonRepositoryCustominterface, da seguinte maneira:

public interface PersonRepository
    extends JpaRepository<Person, Long>, PersonRepositoryCustom { 

}

public interface PersonRepositoryCustom {
    Person findOneWithRoles(Long id);
}

E defina sua implementação assim:

public class PersonRepositoryImpl implements PersonRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public Person findOneWithRoles(Long id)() {
        return entityManager.createQuery("""
            select p 
            from Person p
            left join fetch p.roles
            where p.id = :id 
            """, Person.class)
        .setParameter("id", id)
        .getSingleResult();
    }
}

É isso aí!

Vlad Mihalcea
fonte
Existe uma razão pela qual você escreveu a consulta e não usou uma solução como a EntityGraph na resposta @rakpan? Isso não produziria o mesmo resultado?
Jeroen Vandevelde
A sobrecarga para usar um EntityGraph é maior que uma consulta JPQL. A longo prazo, é melhor escrever a consulta.
Vlad Mihalcea
Você pode elaborar as despesas gerais (de onde vem, é perceptível, ...)? Porque eu não entendo por que há uma sobrecarga maior se ambos geram a mesma consulta.
Jeroen Vandevelde
1
Como os planos do EntityGraphs não são armazenados em cache como o JPQL. Isso pode ser um impacto significativo no desempenho.
Vlad Mihalcea
1
Exatamente. Vou ter que escrever um artigo sobre isso quando tiver algum tempo.
Vlad Mihalcea
1

Você pode fazer o mesmo assim:

@Override
public FaqQuestions getFaqQuestionById(Long questionId) {
    session = sessionFactory.openSession();
    tx = session.beginTransaction();
    FaqQuestions faqQuestions = null;
    try {
        faqQuestions = (FaqQuestions) session.get(FaqQuestions.class,
                questionId);
        Hibernate.initialize(faqQuestions.getFaqAnswers());

        tx.commit();
        faqQuestions.getFaqAnswers().size();
    } finally {
        session.close();
    }
    return faqQuestions;
}

Basta usar faqQuestions.getFaqAnswers (). Size () em seu controlador e você obterá o tamanho da lista inicializada preguiçosamente, sem buscar a própria lista.

neel4soft
fonte