@OneToMany and composite primary keys?

78,067

Solution 1

After much experimentation and frustration, I eventually determined that I cannot do exactly what I want.

Ultimately, I went ahead and gave the child object its own synthetic key and let Hibernate manage it. It's a not ideal, since the key is almost as big as the rest of the data, but it works.

Solution 2

The Manning book Java Persistence with Hibernate has an example outlining how to do this in Section 7.2. Fortunately, even if you don't own the book, you can see a source code example of this by downloading the JPA version of the Caveat Emptor sample project (direct link here) and examining the classes Category and CategorizedItem in the auction.model package.

I'll also summarize the key annotations below. Do let me know if it's still a no-go.

ParentObject:

@Entity
public class ParentObject {
   @Id @GeneratedValue
   @Column(name = "parentId", nullable=false, updatable=false)
   private Long id;

   @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
   @IndexColumn(name = "pos", base=0)
   private List<ChildObject> attrs;

   public Long getId () { return id; }
   public List<ChildObject> getAttrs () { return attrs; }
}

ChildObject:

@Entity
public class ChildObject {
   @Embeddable
   public static class Pk implements Serializable {
       @Column(name = "parentId", nullable=false, updatable=false)
       private Long objectId;

       @Column(nullable=false, updatable=false)
       private String name;

       @Column(nullable=false, updatable=false)
       private int pos;
       ...
   }

   @EmbeddedId
   private Pk id;

   @ManyToOne
   @JoinColumn(name="parentId", insertable = false, updatable = false)
   @org.hibernate.annotations.ForeignKey(name = "FK_CHILD_OBJECT_PARENTID")
   private ParentObject parent;

   public Pk getId () { return id; }
   public ParentObject getParent () { return parent; }
}

Solution 3

You should incorporate the ParentObject reference just into ChildObject.Pk rather than map parent and parentId separately:

(getters, setters, Hibernate attributes not related to problem and member access keywords omitted)

class ChildObject { 
    @Embeddable
    static class Pk {
        @ManyToOne...
        @JoinColumn(name="parentId")
        ParentObject parent;

        @Column...
        String name...
        ...
    }

    @EmbeddedId
    Pk id;
}

In ParentObject you then just put @OneToMany(mappedBy="id.parent") and it works.

Solution 4

Firstly, in the ParentObject, "fix" the mappedBy attribute that should be set to "parent". Also (but this is maybe a typo) add an @Id annotation:

@Entity
public class ParentObject {
    @Id
    @GeneratedValue
    private String id;

    @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
    @IndexColumn(name = "pos", base=0)
    private List<ObjectChild> attrs;

    // getters/setters
}

Then, in ObjectChild, add a name attribute to the objectId in the composite key:

@Entity
public class ObjectChild {
    @Embeddable
    public static class Pk implements Serializable {
        @Column(name = "parentId", nullable = false, updatable = false)
        private String objectId;

        @Column(nullable = false, updatable = false)
        private String name;

        @Column(nullable = false, updatable = false)
        private int pos;
    }

    @EmbeddedId
    private Pk pk;

    @ManyToOne
    @JoinColumn(name = "parentId", insertable = false, updatable = false)
    private ParentObject parent;

    // getters/setters

}

AND also add insertable = false, updatable = false to the @JoinColumn because we are repeating the parentId column in the mapping of this entity.

With these changes, persisting and reading the entities is working fine for me (tested with Derby).

Solution 5

Found this question searching for the answer to it's problem, but it's answers didn't solve my problem, because I was looking for @OneToMany which isn't as good of a fit for the table structure I was going after. @ElementCollection is the right fit in my case. One of the gotchas of it I believe though is that it looks at the entire row of relations as being unique, not just the rows id.

@Entity
public class ParentObject {
@Column(nullable=false, updatable=false)
@Id @GeneratedValue(generator="...")
private String id;

@ElementCollection
@CollectionTable( name = "chidren", joinColumns = @JoinColumn( name = "parent_id" ) )
private List<ObjectChild> attrs;

...
}

@Embeddable
public static class ObjectChild implements Serializable {
    @Column(nullable=false, updatable=false)
    private String parentId;

    @Column(nullable=false, updatable=false)
    private String name;

    @Column(nullable=false, updatable=false)
    private int pos;

    @Override
    public String toString() {
        return new Formatter().format("%s.%s[%d]", parentId, name, pos).toString();
    }

    ... getters and setters REQUIRED (at least they were for me)
}
Share:
78,067
Kris Pruden
Author by

Kris Pruden

Updated on February 25, 2020

Comments

  • Kris Pruden
    Kris Pruden about 4 years

    I'm using Hibernate with annotations (in spring), and I have an object which has an ordered, many-to-one relationship which a child object which has a composite primary key, one component of which is a foreign key back to the id of the parent object.

    The structure looks something like this:

    +=============+                 +================+
    | ParentObj   |                 | ObjectChild    |
    +-------------+ 1          0..* +----------------+
    | id (pk)     |-----------------| parentId       |
    | ...         |                 | name           |
    +=============+                 | pos            |
                                    | ...            |
                                    +================+
    

    I've tried a variety of combinations of annotations, none of which seem to work. This is the closest I've been able to come up with:

    @Entity
    public class ParentObject {
        @Column(nullable=false, updatable=false)
        @Id @GeneratedValue(generator="...")
        private String id;
    
        @OneToMany(mappedBy="parent", fetch=FetchType.EAGER, cascade={CascadeType.ALL})
        @IndexColumn(name = "pos", base=0)
        private List<ObjectChild> attrs;
    
        ...
    }
    
    @Entity
    public class ChildObject {
        @Embeddable
        public static class Pk implements Serializable {
            @Column(nullable=false, updatable=false)
            private String parentId;
    
            @Column(nullable=false, updatable=false)
            private String name;
    
            @Column(nullable=false, updatable=false)
            private int pos;
    
            @Override
            public String toString() {
                return new Formatter().format("%s.%s[%d]", parentId, name, pos).toString();
            }
    
            ...
        }
    
        @EmbeddedId
        private Pk pk;
    
        @ManyToOne
        @JoinColumn(name="parentId")
        private ParentObject parent;
    
        ...
    }
    

    I arrived at this after a long bout of experimentation in which most of my other attempts yielded entities which hibernate couldn't even load for various reasons.

    UPDATE: Thanks all for the comments; I have made some progress. I've made a few tweaks and I think it's closer (I've updated the code above). Now, however, the issue is on insert. The parent object seems to save fine, but the child objects are not saving, and what I've been able to determine is that hibernate is not filling out the parentId part of the (composite) primary key of the child objects, so I'm getting a not-unique error:

    org.hibernate.NonUniqueObjectException:
       a different object with the same identifier value was already associated 
       with the session: [org.kpruden.ObjectChild#null.attr1[0]]
    

    I'm populating the name and pos attributes in my own code, but of course I don't know the parent ID, because it hasn't been saved yet. Any ideas on how to convince hibernate to fill this out?

    Thanks!

  • Kris Pruden
    Kris Pruden about 14 years
    Sadly, that didn't help: I get the same error. The only changes I see from what I had are the addition of the @ForeignKey attribute, and the addition of the {insertable,updatable}=false on the parent attribute. Is there something else I'm not seeing? Thanks!
  • RTBarnard
    RTBarnard about 14 years
    There are a few other changes. (1) Try changing the parentId data type to Long; (2) I believe that the getters are necessary; (3) pay very close attention to the column names for ParentObject#id, ParentObject#attrs, ChildObject.Pk#objectId, and ChildObject#parent; (4) The id field of ParentObject needs the @Id annotation (and @GeneratedValue if you're not manually supplying the id); (5) I renamed ChildObject#pk to ChildObject#id, although I don't think this change was necessary. If you've got all this, can you supply the reading code that's throwing with some surrounding context?
  • John
    John about 14 years
    Re (2) above: Getters and setters aren't necessary. Hibernate can construct proxies to do what needs doing. I find it best to consider this "magic".
  • RTBarnard
    RTBarnard about 14 years
    @John: Thanks for the clarification about getters/setters. @Kris: I also don't think that the parentId field actually needs to be Long; I'm just trying to minimize the number of differences between my working code and your code.
  • ahaaman
    ahaaman almost 11 years
    '@JoinColumns({ @JoinColumn(name="userfirstname_fk", referencedColumnName="firstName"), @JoinColumn(name="userlastname_fk", referencedColumnName="lastName") })'
  • jasop
    jasop almost 11 years
    A good decision. Trying to do anything except basic stuff in Hibernate requires too much un-intuitive messing around. I usually do the same - give EVERYTHING a primary - it keeps things simpler for Hibernate to understand.
  • Popeye
    Popeye over 8 years
    You must use Set<> to avoid the key duplication by using hashCode and equals definition. Hibernate treats two "logically identical" elements in a list to be two elements
  • Adam
    Adam over 7 years
    Just spent half an hour trying to solve this issue and my problem was that my legacy database had the foreign key in the table as "forecastId" and that's what I copied into my JPA annotation and for reasons unknown, Spring decided that the column must therefore be "forecast_id" in the database and it fell over. Must be an algorithm to automatically identify columns that was slightly wrong. I changed the @Column(name= to "forecastid" (all lower case) and Spring decided to behave.
  • Klesun
    Klesun about 2 years
    In my case it caused StackOverflowException when hibernate tried to resolve @ManyToOne relationship ;c