JPA: Como ter uma relação um-para-muitos do mesmo tipo de entidade

99

Existe uma classe de entidade "A". A classe A pode ter filhos do mesmo tipo "A". Além disso, "A" deve conter seu pai, se for uma criança.

Isso é possível? Em caso afirmativo, como devo mapear as relações na classe Entity? ["A" tem uma coluna de id.]

Sanjayav
fonte

Respostas:

170

Sim, isso é possível. Este é um caso especial de relação @ManyToOne/ bidirecional padrão @OneToMany. É especial porque a entidade em cada extremidade do relacionamento é a mesma. O caso geral é detalhado na Seção 2.10.2 da especificação JPA 2.0 .

Aqui está um exemplo prático. Primeiro, a classe de entidade A:

@Entity
public class A implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    @ManyToOne
    private A parent;
    @OneToMany(mappedBy="parent")
    private Collection<A> children;

    // Getters, Setters, serialVersionUID, etc...
}

Aqui está um main()método aproximado que persiste três dessas entidades:

public static void main(String[] args) {

    EntityManager em = ... // from EntityManagerFactory, injection, etc.

    em.getTransaction().begin();

    A parent   = new A();
    A son      = new A();
    A daughter = new A();

    son.setParent(parent);
    daughter.setParent(parent);
    parent.setChildren(Arrays.asList(son, daughter));

    em.persist(parent);
    em.persist(son);
    em.persist(daughter);

    em.getTransaction().commit();
}

Nesse caso, todas as três instâncias de entidade devem ser persistidas antes da confirmação da transação. Se eu falhar em persistir uma das entidades no gráfico de relacionamentos pai-filho, uma exceção é lançada commit(). No Eclipselink, isso é um RollbackExceptiondetalhamento da inconsistência.

Este comportamento é configurável através do cascadeatributo em A's @OneToManye @ManyToOneanotações. Por exemplo, se eu definir cascade=CascadeType.ALLem ambas as anotações, eu poderia persistir com segurança uma das entidades e ignorar as outras. Digamos que eu persistisse parentem minha transação. A implementação de JPA atravessa parenta childrenpropriedade porque está marcada com CascadeType.ALL. A implementação JPA encontra sone daughterali. Em seguida, persiste os dois filhos em meu nome, embora eu não tenha solicitado explicitamente.

Mais uma nota. É sempre responsabilidade do programador atualizar os dois lados de um relacionamento bidirecional. Em outras palavras, sempre que adiciono um filho a algum dos pais, devo atualizar a propriedade do pai da criança de acordo. Atualizar apenas um lado de um relacionamento bidirecional é um erro no JPA. Sempre atualize os dois lados do relacionamento. Isso está escrito de forma inequívoca na página 42 da especificação JPA 2.0:

Observe que é o aplicativo que tem a responsabilidade de manter a consistência dos relacionamentos de tempo de execução - por exemplo, para garantir que o "um" e os "muitos" lados de um relacionamento bidirecional sejam consistentes um com o outro quando o aplicativo atualiza o relacionamento em tempo de execução .

Dan LaRocque
fonte
Muito obrigado pela explicação detalhada! O exemplo vai direto ao ponto e funcionou na primeira execução.
sanjayav
@sunnyj Fico feliz em ajudar. Boa sorte com seus projetos).
Dan LaRocque
Ele encontrou esse problema antes ao criar uma entidade de categoria que possui subcategorias. Isso é útil!
Truong Ha
@DanLaRocque talvez eu esteja entendendo mal (ou tenha um erro de mapeamento de entidade), mas estou vendo um comportamento inesperado. Tenho uma relação um-para-muitos entre o usuário e o endereço. Quando um usuário existente adiciona um endereço, eu segui sua sugestão para atualizar o usuário e o endereço (e chamar 'salvar' em ambos). Mas isso resultou na inserção de uma linha duplicada na minha tabela de endereços. É porque eu configurei incorretamente meu CascadeType no campo de endereço do usuário?
Alex
@DanLaRocque é possível definir essa relação como unidirecional ??
Ali Arda Orhan
8

Para mim, o truque era usar o relacionamento muitos para muitos. Suponha que sua entidade A seja uma divisão que pode ter subdivisões. Então (pulando detalhes irrelevantes):

@Entity
@Table(name = "DIVISION")
@EntityListeners( { HierarchyListener.class })
public class Division implements IHierarchyElement {

  private Long id;

  @Id
  @Column(name = "DIV_ID")
  public Long getId() {
        return id;
  }
  ...
  private Division parent;
  private List<Division> subDivisions = new ArrayList<Division>();
  ...
  @ManyToOne
  @JoinColumn(name = "DIV_PARENT_ID")
  public Division getParent() {
        return parent;
  }

  @ManyToMany
  @JoinTable(name = "DIVISION", joinColumns = { @JoinColumn(name = "DIV_PARENT_ID") }, inverseJoinColumns = { @JoinColumn(name = "DIV_ID") })
  public List<Division> getSubDivisions() {
        return subDivisions;
  }
...
}

Como eu tinha uma lógica de negócios extensa em torno da estrutura hierárquica e o JPA (baseado no modelo relacional) é muito fraco para suportá-lo, introduzi a interface IHierarchyElemente o listener de entidade HierarchyListener:

public interface IHierarchyElement {

    public String getNodeId();

    public IHierarchyElement getParent();

    public Short getLevel();

    public void setLevel(Short level);

    public IHierarchyElement getTop();

    public void setTop(IHierarchyElement top);

    public String getTreePath();

    public void setTreePath(String theTreePath);
}


public class HierarchyListener {

    @PrePersist
    @PreUpdate
    public void setHierarchyAttributes(IHierarchyElement entity) {
        final IHierarchyElement parent = entity.getParent();

        // set level
        if (parent == null) {
            entity.setLevel((short) 0);
        } else {
            if (parent.getLevel() == null) {
                throw new PersistenceException("Parent entity must have level defined");
            }
            if (parent.getLevel() == Short.MAX_VALUE) {
                throw new PersistenceException("Maximum number of hierarchy levels reached - please restrict use of parent/level relationship for "
                        + entity.getClass());
            }
            entity.setLevel(Short.valueOf((short) (parent.getLevel().intValue() + 1)));
        }

        // set top
        if (parent == null) {
            entity.setTop(entity);
        } else {
            if (parent.getTop() == null) {
                throw new PersistenceException("Parent entity must have top defined");
            }
            entity.setTop(parent.getTop());
        }

        // set tree path
        try {
            if (parent != null) {
                String parentTreePath = StringUtils.isNotBlank(parent.getTreePath()) ? parent.getTreePath() : "";
                entity.setTreePath(parentTreePath + parent.getNodeId() + ".");
            } else {
                entity.setTreePath(null);
            }
        } catch (UnsupportedOperationException uoe) {
            LOGGER.warn(uoe);
        }
    }

}
topchef
fonte
1
Por que não usar o mais simples @OneToMany (mappedBy = "DIV_PARENT_ID") em vez de @ManyToMany (...) com atributos de autorreferência? A redigitação de nomes de tabelas e colunas como esse viola o DRY. Talvez haja uma razão para isso, mas não vejo. Além disso, o exemplo EntityListener é simples, mas não portátil, presumindo que Topseja um relacionamento. Página 93 da especificação JPA 2.0, Ouvintes de entidade e métodos de retorno de chamada: "Em geral, o método de ciclo de vida de um aplicativo portátil não deve invocar operações EntityManager ou Consulta, acessar outras instâncias de entidade ou modificar relacionamentos". Certo? Avise-me se estiver fora.
Dan LaRocque
Minha solução tem 3 anos usando JPA 1.0. Eu o adaptei inalterado do código de produção. Tenho certeza de que poderia tirar alguns nomes de colunas, mas não era esse o ponto. Sua resposta é exata e simples, não sei por que usei muitos para muitos naquela época - mas funciona e estou confiante de que havia uma solução mais complexa por um motivo. Porém, terei que revisitar isso agora.
topchef de
Sim, top é uma auto-referência, portanto, um relacionamento. Estritamente falando, eu não o modifico - apenas inicializo. Além disso, é unidirecional, então não há dependência ao contrário, não faz referência a outras entidades além de si mesmo. De acordo com sua especificação de cotação, tem "em geral", o que significa que não é uma definição estrita. Eu acredito que neste caso o risco de portabilidade é muito baixo.
topchef de