Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to persist two entities with JPA

I am using the JPA in my webapp and I can't figure out how to persist two new entities that relate to each other. Here an example:

These are the two entities

+-----------------+   +--------------------+
|     Consumer    |   |   ProfilePicture   |
+-----------------+   +--------------------+
| id    (PK)      |---| consumerId (PPK+FK)|
| userName        |   | url                |
+-----------------+   +--------------------+

The Consumer has an id and some other values. The ProfilePicture uses the Consumer's id as it's own primary key and as foreign key. (Since a ProfilePicture will not exist without a Consumer and not every Consumer has a ProfilePicture)

I used NetBeans to generate the entity classes and the session beans (facades).

This is how they look like in short

Consumer.java

@Entity
@Table(name = "Consumer")
@NamedQueries({...})
public class Consumer implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    @Column(name = "id")
    private Integer id;

    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 50)
    @Column(name = "userName")
    private String userName;     

    @OneToOne(cascade = CascadeType.ALL, mappedBy = "consumer")
    private ProfilePicture profilePicture;

    /* and all the basic getters and setters */
    (...)
}

ProfilePicture.java

@Entity
@Table(name = "ProfilePicture")
@XmlRootElement
@NamedQueries({...})
public class ProfilePicture implements Serializable {

    @Id
    @Basic(optional = false)
    @NotNull
    @Column(name = "consumerId")
    private Integer consumerId;

    @Basic(optional = false)
    @NotNull
    @Size(min = 1, max = 255)
    @Column(name = "url")
    private String url;

    @JoinColumn(name = "consumerId", referencedColumnName = "id", insertable = false, updatable = false)
    @OneToOne(optional = false)
    private Consumer consumer;

    /* and all the basic getters and setters */
    (...)
}

So when I want to create a Consumer with his ProfilePicture I thought I would do it like this:

   ProfilePicture profilePicture = new ProfilePicture("http://www.url.to/picture.jpg");  // create the picture object
   Consumer consumer = new Consumer("John Doe"); // create the consumer object

   profilePicture.setConsumer(consumer);        // set the consumer in the picture (so JPA can take care about the relation      
   consumerFacade.create(consumer);             // the facade classes to persist the consumer
   profilePictureFacade.create(profilePicture);  // and when the consumer is persisted (and has an id) persist the picture

My Problem

I tried almost everything in every combination but JPA doesn't seem to be able to link the two entities on it's own. Most of the time I am getting errors like this:

 EJB5184:A system exception occurred during an invocation on EJB ConsumerFacade, method: public void com.me.db.resources.bean.ConsumerFacade.create(com.mintano.backendclientserver.db.resources.entity.Consumer)
 (...) 
Bean Validation constraint(s) violated while executing Automatic Bean Validation on callback event:'prePersist'. Please refer to embedded ConstraintViolations for details.

As far as I understand the problem, it is because the ProfilePicture doesn't know the id of the Consumer and thus, the entities cannot persist.

The only way it ever worked, was when persisting the Consumer first, setting it's id to the ProfilePicture and then persisting the picture:

   ProfilePicture profilePicture = new ProfilePicture("http://www.url.to/picture.jpg");  // create the picture object
   Consumer consumer = new Consumer("John Doe"); // create the consumer object

   consumerFacade.create(consumer);             // the facade classes to persist the consumer
   profilePicture.setConsumerId(consumer.getId()); // set the consumer's new id in the picture     

   profilePictureFacade.create(profilePicture);  // and when the consumer is persisted (and has an id) persist the picture

However these two tables are just an example and naturally the database is much more complex and setting the ids manually like this seems very inflexible and I am afraid of over complicating things. Especially because I can't persist all entities in one transaction (which seems very inefficient).

Am I doing it right? Or is there another, more standard way?

Edit: my solution

As FTR suggested, one problem was the missing id for the ProfilePicture table (I used the Consumer.id as foreign and primary)..

The tables look like this now:

+-----------------+   +--------------------+
|     Consumer    |   |   ProfilePicture   |
+-----------------+   +--------------------+
| id    (PK)      |_  | id (PK)            |
| userName        | \_| consumerId (FK)    |
+-----------------+   | url                |
                      +--------------------+

Then Alan Hay told me to Always encapsulate add/remove to relationships and then you can ensure correctness, which I did:

Consumer.java

public void addProfilePicture(ProfilePicture profilePicture) {
    profilePicture.setConsumerId(this);  
    if (profilePictureCollection == null) {
        this.profilePictureCollection = new ArrayList<>();
    }
    this.profilePictureCollection.add(profilePicture);
}

Since ProfilePicture has it's own id now, it became a OneToMany relationship, so each Consumer can now have many profile pictures. That's not what I intended at first, but I can life with it :) Therefore I can't just set a ProfilePicture to the Consumer but have to add it to a collection of Pictures (as above).

This was the only additional method I implemented and now it works. Thanks again for all your help!

like image 947
GameDroids Avatar asked Oct 06 '14 15:10

GameDroids


People also ask

How do I save multiple entities in JPA?

Basically what you are looking for is batch insert into database using JPA. These topics have already been brought up, these will help you: JPA/Hibernate bulk(batch) insert. Batch inserts with JPA/EJB3.

How do I join two entities in JPA?

The only way to join two unrelated entities with JPA 2.1 and Hibernate versions older than 5.1, is to create a cross join and reduce the cartesian product in the WHERE statement. This is harder to read and does not support outer joins. Hibernate 5.1 introduced explicit joins on unrelated entities.


1 Answers

When persisting an instance of the non-owning side of the relationship (that which contains the 'mappedBy' and in your case Consumer) then you must always ensure both sides of the relationship are set to have cascading work as expected.

You should of course always do this anyway to ensure your domain model is correct.

Consumer c = new Consumer();
ProfilePicure p = new ProfilePicture();
c.setProfilePicture(p);//see implementation
//persist c

Consumer.java

    @Entity
    @Table(name = "Consumer")
    @NamedQueries({...})
    public class Consumer implements Serializable {

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Basic(optional = false)
        @Column(name = "id")
        private Integer id;

        @Basic(optional = false)
        @NotNull
        @Size(min = 1, max = 50)
        @Column(name = "userName")
        private String userName;     

        @OneToOne(cascade = CascadeType.ALL, mappedBy = "consumer")
        private ProfilePicture profilePicture;

        public void setProfilePicture(ProfilePicture profilePicture){
            //SET BOTH SIDES OF THE RELATIONSHIP
            this.profilePicture = profilePicture;
            profilePicture.setConsumer(this);
        }
}

Always encapsulate add/remove to relationships and then you can ensure correctness:

public class Parent{
private Set<Child> children;

public Set<Child> getChildren(){
    return Collections.unmodifiableSet(children); //no direct access:force clients to use add/remove methods
}

public void addChild(Child child){
    child.setParent(this); 
    children.add(child);
}

public class Child(){
    private Parent parent;
}
like image 78
Alan Hay Avatar answered Nov 15 '22 00:11

Alan Hay