web-dev-qa-db-fra.com

Comment enregistrer correctement en cascade une relation bidirectionnelle un à un sur la clé primaire dans Hibernate 3.6

J'ai une relation d'entité bidirectionnelle un à un avec des clés partagées. Lorsque j'essaie d'enregistrer le propriétaire de l'association, j'obtiens une exception "null id généré" contre le côté propriétaire de la relation. J'utilise hibernate-entitymanager et j'utilise spring pour la gestion des transactions.

Entité propriétaire

@Entity
@Table(name = "lead")
public class Lead
{
    private Long leadId;

    private LeadAffiliate leadAffiliate;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Long getLeadId()
    {
        return leadId;
    }

    @OneToOne(cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn
    public LeadAffiliate getLeadAffiliate()
    {
        return leadAffiliate;
    }
}

Entité détenue

@Entity
@Table(name = "lead_affiliate")
public class LeadAffiliate
{
    private Long leadId;

    private Lead lead;

    @Id
    public Long getLeadId()
    {
        return leadId;
    }

    @MapsIdmappedBy = "leadAffiliate")
    @OneToOne(cascade = CascadeType.All)
    @PrimaryKeyJoinColumn
    @JoinColumn(name = "lead_id")
    public Lead getLead()
    {
        return lead;
    }
}

et le code ci-dessous est utilisé pour enregistrer l'entité:

LeadAffiliate aff = new LeadAffiliate();

aff.setLead(lead);
lead.setLeadAffiliate(aff);

em.persist(lead);

Tout cela fonctionne parfaitement dans hibernate 3.5.0-Final. Lorsque j'essaie de mettre à niveau vers 3.5.6-Final ou 3.6.0.Final, c'est quand je commence à obtenir l'erreur "null id généré pour LeadAffiliate":

javax.persistence.PersistenceException: org.hibernate.id.IdentifierGenerationException: null id generated for:class com.sellingsource.bizdev.entities.LeadAffiliate
    at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.Java:1214)
    at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.Java:1147)
    at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.Java:1153)
    at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.Java:678)
    at Sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at Sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.Java:39)
    at Sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.Java:25)
    at Java.lang.reflect.Method.invoke(Method.Java:597)
    at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.Java:365)
    at $Proxy152.persist(Unknown Source)
    at Sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at Sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.Java:39)
    at Sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.Java:25)
    at Java.lang.reflect.Method.invoke(Method.Java:597)
    at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.Java:240)
    at $Proxy120.persist(Unknown Source)
    at com.sellingsource.common.dao.JpaGenericDao.create(JpaGenericDao.Java:38)
    ... 64 more
Caused by: org.hibernate.id.IdentifierGenerationException: null id generated for:class com.sellingsource.bizdev.entities.LeadAffiliate
    at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.Java:123)
    at org.hibernate.ejb.event.EJB3PersistEventListener.saveWithGeneratedId(EJB3PersistEventListener.Java:69)
    at org.hibernate.event.def.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.Java:179)
    at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.Java:135)
    at org.hibernate.impl.SessionImpl.firePersist(SessionImpl.Java:799)
    at org.hibernate.impl.SessionImpl.persist(SessionImpl.Java:791)
    at org.hibernate.engine.EJB3CascadingAction$1.cascade(EJB3CascadingAction.Java:48)
    at org.hibernate.engine.Cascade.cascadeToOne(Cascade.Java:392)
    at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.Java:335)
    at org.hibernate.engine.Cascade.cascadeProperty(Cascade.Java:204)
    at org.hibernate.engine.Cascade.cascade(Cascade.Java:161)
    at org.hibernate.event.def.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.Java:450)
    at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.Java:282)
    at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.Java:203)
    at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.Java:129)
    at org.hibernate.ejb.event.EJB3PersistEventListener.saveWithGeneratedId(EJB3PersistEventListener.Java:69)
    at org.hibernate.event.def.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.Java:179)
    at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.Java:135)
    at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.Java:61)
    at org.hibernate.impl.SessionImpl.firePersist(SessionImpl.Java:808)
    at org.hibernate.impl.SessionImpl.persist(SessionImpl.Java:782)
    at org.hibernate.impl.SessionImpl.persist(SessionImpl.Java:786)
    at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.Java:672)
    ... 77 more

Soit dit en passant, je ne suis pas sûr que les annotations sur l'affilié principal étaient tout à fait justes pour commencer. Ils travaillaient, mais semblaient un peu kludgey. Je les ai donc modifiés depuis:

@Entity
@Table(name = "lead_affiliate")
public class LeadAffiliate
{
    private Long leadId;

    private Lead lead;

    @Id
    @GenericGenerator(name = "foreign", strategy = "foreign", parameters = {
                    @org.hibernate.annotations.Parameter(name = "property", value="lead")
    })
    @GeneratedValue(generator = "foreign")
    public Long getLeadId()
    {
        return leadId;
    }

    @OneToOne(mappedBy = "leadAffiliate")
    @PrimaryKeyJoinColumn
    public Lead getLead()
    {
        return lead;
    }
}

Cependant, avec ces changements, j'obtiens le même résultat. (Fonctionne en 3.5.0 mais pas en 3.5.6 ou 3.6.0)

Existe-t-il une nouvelle façon de procéder ou est-ce un bug? Mon souci est que mon code fonctionne actuellement à cause d'un bogue: /.

37
Mike Lively

La spécification indique que l'entité dérivée devrait être le côté propriétaire de la relation:

2.4.1 Clés primaires correspondant aux identités dérivées

L'identité d'une entité peut être dérivée de l'identité d'une autre entité (l'entité "parent") lorsque l'ancienne entité (l'entité "dépendante") est propriétaire d'une relation plusieurs-à-un ou un-à-un avec l'entité parent et une clé étrangère mappent la relation de dépendant à parent.

Dans votre cas, LeadAffiliate est dérivé, il doit donc être le propriétaire, lorsque Lead doit être marqué comme côté non propriétaire par mappedBy. Les éléments suivants fonctionnent dans les versions 3.5.0 et 3.5.6:

public class Lead { 
    @Id @GeneratedValue
    private Long leadId; 

    @OneToOne(cascade = CascadeType.ALL, mappedBy = "lead")
    private LeadAffiliate leadAffiliate; 

    ...
}

.

public class LeadAffiliate {  
    @Id
    private Long leadId;  

    @OneToOne @MapsId
    private Lead lead; 

    ...
}
40
axtavt

Ma réponse n'expliquera pas pourquoi les choses fonctionnent avec Hibernate 3.5.0-Final, mais pas avec 3.5.6-Final ou 3.6.0.Final (et vous devez le signaler, j'appelle cela une régression).

Quoi qu'il en soit, les identifiants dérivés sont beaucoup mieux pris en charge dans JPA 2.0, de manière standard, et dans votre cas, je pense que vous pouvez simplement annoter votre relation OneToOne avec une annotation Id.

Mise à jour: Comme souligné par axtavt, lors de l'utilisation d'un identifiant dérivé, l'entité "dépendante" doit être le propriétaire de la relation. Ainsi, le mappage complet de l'entité dépendante serait:

@Entity
@Table(name = "lead_affiliate")
public class LeadAffiliate {
    private Lead lead;

    @Id
    @OneToOne
    @JoinColumn(name="FK")
    public Lead getLead() {
        return lead;
    }
}

Et l'entité "parent":

@Entity
@Table(name = "lead")
public class Lead {
    private Long leadId;

    private LeadAffiliate leadAffiliate;

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    public Long getLeadId() {
        return leadId;
    }

    @OneToOne(cascade = CascadeType.ALL, mappedBy="lead")
    public LeadAffiliate getLeadAffiliate() {
        return leadAffiliate;
    }
}

Il s'agit d'un mappage JPA 2.0 valide, et fonctionne avec EclipseLink . Cependant, Hibernate ne l'aime pas et n'instanciera pas le EntityManagerFactory (bon sang!).

Comme solution de contournement, vous devrez utiliser le solution suggérée par axtavt ie pour déclarer un attribut de clé primaire ainsi que le l'attribut de relation et d'utiliser MapsId sur l'attribut de relation.

Mais ce qui précède devrait fonctionner, il y a IMO un bogue dans Hibernate (signalé comme HHH-5695 ).

Les références

9
Pascal Thivent