Como posso tornar preguiçosa uma relação JPA OneToOne

212

Nesta aplicação que estamos desenvolvendo, percebemos que a visualização era particularmente lenta. Criei um perfil da visualização e notei que havia uma consulta executada pelo hibernate que levava 10 segundos, mesmo que houvesse apenas dois objetos no banco de dados para buscar. Todos OneToManye as ManyToManyrelações eram preguiçosas, e esse não era o problema. Ao inspecionar o SQL real sendo executado, notei que havia mais de 80 junções na consulta.

Inspecionando ainda mais o problema, notei que o problema foi causado pela hierarquia profunda OneToOnee pelas ManyToOnerelações entre as classes de entidade. Então, pensei, vou fazê-los buscar preguiçosos, o que deve resolver o problema. Mas anotar @OneToOne(fetch=FetchType.LAZY)ou @ManyToOne(fetch=FetchType.LAZY)não parece funcionar. Ou recebo uma exceção ou eles não são realmente substituídos por um objeto proxy e, portanto, são preguiçosos.

Alguma idéia de como eu vou fazer isso funcionar? Note que eu não uso o persistence.xmlpara definir relações ou detalhes de configuração, tudo é feito no código java.

Kim L
fonte

Respostas:

218

Primeiro, alguns esclarecimentos à resposta do KLE :

  1. A associação individual irrestrita (anulável) é a única que não pode ser proxy sem a instrumentação de bytecode. A razão para isso é que a entidade proprietária DEVE saber se a propriedade da associação deve conter um objeto proxy ou NULL e não pode determinar isso observando as colunas da tabela base, porque normalmente um a um é mapeado através de PK compartilhada. precisa ser buscado de qualquer maneira, tornando o proxy inútil. Aqui está uma explicação mais detalhada .

  2. associações muitos-para-um (e um-para-muitos, obviamente) não sofrem com esse problema. A entidade proprietária pode facilmente verificar seu próprio FK (e, no caso de um para muitos, o proxy de coleção vazio é criado inicialmente e preenchido sob demanda), para que a associação seja preguiçosa.

  3. Substituir um para um por um para muitos nunca é uma boa ideia. Você pode substituí-lo por muitos exclusivos para muitos, mas existem outras opções (possivelmente melhores).

Rob H. tem um ponto válido, no entanto, talvez você não consiga implementá-lo, dependendo do seu modelo (por exemplo, se sua associação um a um é anulável).

Agora, no que diz respeito à pergunta original:

A) @ManyToOne(fetch=FetchType.LAZY)deve funcionar muito bem. Tem certeza de que não está sendo substituído na própria consulta? É possível especificar join fetchno HQL e / ou definir explicitamente o modo de busca via API de critérios, o que teria precedência sobre a anotação de classe. Se esse não for o caso e você ainda estiver com problemas, publique suas classes, consulte o SQL resultante para obter uma conversa mais direta.

B) @OneToOneé mais complicado. Se definitivamente não for anulável, siga a sugestão de Rob H. e especifique-a da seguinte forma:

@OneToOne(optional = false, fetch = FetchType.LAZY)

Caso contrário, se você puder alterar seu banco de dados (adicionar uma coluna de chave estrangeira à tabela do proprietário), faça-o e mapeie-o como "unido":

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name="other_entity_fk")
public OtherEntity getOther()

e em OtherEntity:

@OneToOne(mappedBy = "other")
public OwnerEntity getOwner()

Se você não pode fazer isso (e não pode viver com a busca ansiosa), a instrumentação bytecode é sua única opção. Eu tenho que concordar com CPerkins , no entanto - se você tem 80 !!! se une devido a associações OneToOne, você tem problemas maiores que este :-)

ChssPly76
fonte
Talvez exista outra opção, mas eu não a testei pessoalmente: no lado não restrito, use a one-to-onecom uma fórmula como select other_entity.id from other_entity where id = other_entity.id. Obviamente, isso não é ideal para performances de consulta.
Frédéric
1
opcional = false, não funciona para mim. @OneToOne (busca = FetchType.LAZY, mappedBy = "fundSeries", opcional = false) private FundSeriesDetailEntity fundSeriesDetail;
Oleg Kuts
21

Para que o carregamento lento funcione em mapeamentos um-para-um anuláveis, você precisa permitir que o hibernate compile a instrumentação de tempo e adicione @LazyToOne(value = LazyToOneOption.NO_PROXY)a à relação um-para-um.

Exemplo de mapeamento:

@OneToOne(fetch = FetchType.LAZY)  
@JoinColumn(name="other_entity_fk")
@LazyToOne(value = LazyToOneOption.NO_PROXY)
public OtherEntity getOther()

Extensão de arquivo Ant Build do exemplo (para executar a instrumentação de tempo de compilação do Hibernate):

<property name="src" value="/your/src/directory"/><!-- path of the source files --> 
<property name="libs" value="/your/libs/directory"/><!-- path of your libraries --> 
<property name="destination" value="/your/build/directory"/><!-- path of your build directory --> 

<fileset id="applibs" dir="${libs}"> 
  <include name="hibernate3.jar" /> 
  <!-- include any other libraries you'll need here --> 
</fileset> 

<target name="compile"> 
  <javac srcdir="${src}" destdir="${destination}" debug="yes"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </javac> 
</target> 

<target name="instrument" depends="compile"> 
  <taskdef name="instrument" classname="org.hibernate.tool.instrument.javassist.InstrumentTask"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </taskdef> 

  <instrument verbose="true"> 
    <fileset dir="${destination}"> 
      <!-- substitute the package where you keep your domain objs --> 
      <include name="/com/mycompany/domainobjects/*.class"/> 
    </fileset> 
  </instrument> 
</target>
Kdeveloper
fonte
3
Por que LazyToOneOption.NO_PROXYe não LazyToOneOption.PROXY?
Telmo Marques
Isso não responde ao "porquê", mas esse fato também é afirmado aqui (no final da seção "Mapeamento típico"): vladmihalcea.com/…
DanielM
12

A idéia básica por trás dos XToOnes no Hibernate é que eles não são preguiçosos na maioria dos casos.

Uma razão é que, quando o Hibernate precisa decidir colocar um proxy (com o ID) ou um valor nulo,
ele precisa procurar na outra tabela para participar. O custo de acessar a outra tabela no banco de dados é significativo; portanto, é possível buscar os dados dessa tabela naquele momento (comportamento não preguiçoso), em vez de buscá-lo em uma solicitação posterior que exigiria um segundo acesso ao mesma mesa.

Editado: para detalhes, consulte a resposta de ChssPly76 . Este é menos preciso e detalhado, não tem nada a oferecer. Obrigado ChssPly76.

KLE
fonte
Há várias coisas errada aqui - eu forneci outra resposta abaixo com uma explicação (muita coisa, não vai caber em um comentário)
ChssPly76
8

Aqui está algo que está funcionando para mim (sem instrumentação):

Em vez de usar @OneToOnenos dois lados, eu uso @OneToManyna parte inversa do relacionamento (aquele com mappedBy). Isso torna a propriedade uma coleção ( Listno exemplo abaixo), mas eu a traduzo em um item no getter, tornando-a transparente para os clientes.

Essa configuração funciona preguiçosamente, ou seja, as seleções são feitas apenas quando getPrevious()ou getNext()são chamadas - e apenas uma seleção para cada chamada.

A estrutura da tabela:

CREATE TABLE `TB_ISSUE` (
    `ID`            INT(9) NOT NULL AUTO_INCREMENT,
    `NAME`          VARCHAR(255) NULL,
    `PREVIOUS`      DECIMAL(9,2) NULL
    CONSTRAINT `PK_ISSUE` PRIMARY KEY (`ID`)
);
ALTER TABLE `TB_ISSUE` ADD CONSTRAINT `FK_ISSUE_ISSUE_PREVIOUS`
                 FOREIGN KEY (`PREVIOUS`) REFERENCES `TB_ISSUE` (`ID`);

A classe:

@Entity
@Table(name = "TB_ISSUE") 
public class Issue {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Integer id;

    @Column
    private String name;

    @OneToOne(fetch=FetchType.LAZY)  // one to one, as expected
    @JoinColumn(name="previous")
    private Issue previous;

    // use @OneToMany instead of @OneToOne to "fake" the lazy loading
    @OneToMany(mappedBy="previous", fetch=FetchType.LAZY)
    // notice the type isnt Issue, but a collection (that will have 0 or 1 items)
    private List<Issue> next;

    public Integer getId() { return id; }
    public String getName() { return name; }

    public Issue getPrevious() { return previous; }
    // in the getter, transform the collection into an Issue for the clients
    public Issue getNext() { return next.isEmpty() ? null : next.get(0); }

}
acdcjunior
fonte
7

Como expliquei neste artigo , a menos que você esteja usando o Bytecode Enhancement , não poderá buscar preguiçosamente a @OneToOneassociação do lado dos pais .

No entanto, na maioria das vezes, você nem precisa da associação do lado dos pais se usar @MapsIdno lado do cliente:

@Entity(name = "PostDetails")
@Table(name = "post_details")
public class PostDetails {

    @Id
    private Long id;

    @Column(name = "created_on")
    private Date createdOn;

    @Column(name = "created_by")
    private String createdBy;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Post post;

    public PostDetails() {}

    public PostDetails(String createdBy) {
        createdOn = new Date();
        this.createdBy = createdBy;
    }

    //Getters and setters omitted for brevity
}

Com @MapsId, a idpropriedade na tabela filha serve como Chave Primária e Chave Externa para a Chave Primária da tabela pai.

Portanto, se você tiver uma referência à Postentidade pai , poderá buscar facilmente a entidade filha usando o identificador da entidade pai:

PostDetails details = entityManager.find(
    PostDetails.class,
    post.getId()
);

Dessa forma, você não terá problemas de consulta N + 1 que podem ser causados ​​pela mappedBy @OneToOneassociação no lado pai.

Vlad Mihalcea
fonte
desta forma, não podemos mais executar operações em cascata de pai para filho: /
Hamdi
Para persistir, é apenas uma chamada persistente extra; para excluir, você pode usar cascata DDL.
Vlad Mihalcea
6

Nos mapeamentos XML nativos do Hibernate, você pode fazer isso declarando um mapeamento individual com o atributo restrito definido como true. Não tenho certeza do que é equivalente à anotação Hibernate / JPA, e uma rápida pesquisa no documento não forneceu resposta, mas espero que isso lhe dê uma pista para continuar.

Rob H
fonte
5
+1 para uma boa sugestão; infelizmente, nem sempre é aplicável, pois o modelo de domínio pode realmente requerer nulidade. A maneira correta de mapear isso por meio de anotações é@OneToOne(optional=false,fetch=FetchMode.LAZY)
ChssPly76 18/09/09
Eu tentei isso e não vi nenhuma melhoria de desempenho. Eu ainda vi muitas consultas na saída de hibernação via depurador.
precisa saber é o seguinte
3

Como já foi perfeitamente explicado por ChssPly76, os proxies do Hibernate não ajudam com associações um-a-um sem restrições (anuláveis), MAS, há um truque explicado aqui para evitar a configuração da instrumentação. A idéia é enganar o Hibernate que a classe de entidade que queremos usar já foi instrumentada: você a instrumenta manualmente no código-fonte. É fácil! Eu o implementei com o CGLib como provedor de bytecode e ele funciona (verifique se você configurou lazy = "no-proxy" e fetch = "select", não "join", no seu HBM).

Eu acho que essa é uma boa alternativa para a instrumentação real (quero dizer automática) quando você tem apenas uma relação anulável de um para um que deseja tornar preguiçosa. A principal desvantagem é que a solução depende do provedor de bytecode que você está usando, portanto, comente sua classe com precisão, pois poderá ser necessário alterar o fornecedor de bytecode no futuro; é claro, você também está modificando seu bean de modelo por uma razão técnica e isso não está correto.

Pino
fonte
1

Essa pergunta é bastante antiga, mas com o Hibernate 5.1.10, há uma nova solução melhor e mais confortável.

O carregamento lento funciona, exceto para o lado pai de uma associação @OneToOne. Isso ocorre porque o Hibernate não tem outra maneira de saber se atribui um nulo ou um proxy a essa variável. Mais detalhes você pode encontrar neste artigo

  • Você pode ativar o carregamento lento por aprimoramento de código de código
  • Ou, você pode simplesmente remover o lado pai e usar o lado do cliente com @MapsId, conforme explicado no artigo acima. Dessa forma, você descobrirá que realmente não precisa do lado do pai, pois o filho compartilha o mesmo ID com o pai, para que você possa facilmente buscá-lo conhecendo o ID do pai.
Toumi
fonte
0

Se a relação não deve ser bidirecional, um @ElementCollection pode ser mais fácil do que usar uma coleção lenta do One2Many.

Stefan
fonte