JPA - correct way to insert to a join table (with extra columns)

Mr_and_Mrs_D picture Mr_and_Mrs_D · Feb 16, 2014 · Viewed 9k times · Source

I managed to insert crew for my movie - now I want to do it the right way. Entities (abbreviated):

@Entity
@Table(name = "movies")
public class Movie implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int idmovie;
    // bi-directional many-to-one association to MoviesHasCrew
    @OneToMany(mappedBy = "movy", cascade = CascadeType.PERSIST)
    private List<MoviesHasCrew> moviesHasCrews;
}

@Entity
@Table(name = "movies_has_crew")
public class MoviesHasCrew implements Serializable {
    @EmbeddedId
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private MoviesHasCrewPK id;
    // bi-directional many-to-one association to Crew
    @ManyToOne
    @JoinColumn(name = "crew_idcrew", columnDefinition = "idcrew")
    @MapsId("crewIdcrew")
    private Crew crew;
    // bi-directional many-to-one association to Movy
    @ManyToOne
    @JoinColumn(name = "movies_idmovie")
    @MapsId("moviesIdmovie")
    private Movie movy;
    // bi-directional many-to-one association to Role
    @ManyToOne
    @JoinColumn(name = "roles_idrole")
    @MapsId("rolesIdrole")
    private Role role;
}

@Entity
@Table(name = "crew")
public class Crew implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int idcrew;
    // bi-directional many-to-one association to MoviesHasCrew
    @OneToMany(mappedBy = "crew", cascade = CascadeType.PERSIST)
    private List<MoviesHasCrew> moviesHasCrews;
}

Sorry for 'movy' and 'crews' that's the tools (and qualifies for a bug report)

Controller and form:

@ManagedBean
@ViewScoped
public class MovieController implements Serializable {
    @EJB
    private MovieService service;
    private Crew crewMember;
    private Movie movie;

    public String addCrewMember() {
        if (movie.getIdmovie() == 0) {
            movie = (Movie) FacesContext.getCurrentInstance()
                .getExternalContext()
                .getSessionMap().get("movie");
        }
        service.addCrew(movie, crewMember);
        return null;
    }
}

<h:form id="movie_add_crew_form" rendered="#{sessionScope.movie != null}">
<h:panelGrid columns="2">
    <h:selectOneListbox id="crewMember" redisplay="true" size="8"
        value="#{movieController.crewMember}"
        converter="#{movieController$CrewConverter}">
        <f:selectItems value="#{movieController.allCrew}" var="entry"
            itemValue="#{entry}" itemLabel="#{entry.name}" />
        <f:ajax event="blur" render="crewMemberMessage" />
    </h:selectOneListbox>
    <h:message id="crewMemberMessage" for="crewMember" />
</h:panelGrid>
<h:commandButton value="Add" action="#{movieController.addCrewMember}">
    <f:ajax execute="@form" render="@form :movie_crew" />
</h:commandButton></h:form>

And finally the service:

@Stateless
public class MovieService {

    @PersistenceContext
    private EntityManager em;

    public void addCrew(Movie m, Crew w) {
        MoviesHasCrew moviesHasCrew = new MoviesHasCrew();
        moviesHasCrew.setCrew(w);
        moviesHasCrew.setMovy(m);
        moviesHasCrew.setRole(Role.DEFAUT_ROLE);
        em.persist(moviesHasCrew);
        m.addMoviesHasCrew(moviesHasCrew); // (1)
        em.merge(m); // noop
    }
}

Question 1: I want to have the Crew and Movie entities' fields moviesHasCrews updated on persisting the MoviesHasCrew entity (ie drop m.addMoviesHasCrew(moviesHasCrew); em.merge(m);) but my cascade annotations do not seem to do it. Should I do it the other way round ? That is add to moviesHasCrews in movies and merge/perist Movie and have the MoviesHasCrew updated - this I read needs hibernate but I work with generic JPA - is it still not doable in vanilla JPA ?

Question 2: a rundown of how this should be done would be appreciated (for instance should I add fetch=Lazy (in Movie and Crew) @Transient to the moviesHasCrews fields ?). Is @MapsId("moviesIdmovie") etc needed in the join table entity ? Is this the most minimal/elegant way of doing it ?

The schema:

enter image description here

References:

Answer

Chris picture Chris · Feb 24, 2014

The problem is that JPA does not maintain both sides of bidirectional relationships for you. This is much more apparent when you use a JPA provider that has a second level cache. The reason it is apparent is that when you set the owning side of a relationship - in this case call moviesHasCrew.setCrew(w) and then em.flush()- this causes the database FK to be updated. But if you immediately check your object model, you will see that the Crew member referenced does not have a corresponding moviesHasCrew instance in its collection. JPA doesn't manage your references and set them for you, so it is out of sync with what is in the database.

This should be expected in the same EntityManager. When a second level cache is involved though, every time you query for that Crew instance, it will return the cached copy that is now stale.

The only way to have the collection updated is cause the Crew instance to be reloaded from the database. This can be done by clearing the cache, or by forcing a refresh.

The better alternative is to maintain both sides of bidirectional relationship and keep them in sync with each other. In the code case you have, this means calling:

public void addCrew(Movie m, Crew w) {
    MoviesHasCrew moviesHasCrew = new MoviesHasCrew();
    moviesHasCrew.setCrew(w);
    w.addMoviesHasCrew(moviesHasCrew); 
    moviesHasCrew.setMovy(m);
    m.addMoviesHasCrew(moviesHasCrew); // (1)
    moviesHasCrew.setRole(Role.DEFAUT_ROLE);
    em.persist(moviesHasCrew);
    em.merge(m); // noop unless it is detached
    em.merge(w); // noop unless it is detached
}

The merge is required if they are detached instances, as the change to the collections needs to be put into the EntityManager so it can be merged into the cache.

If these merges are something you want to avoid, you can rely on the moviesHasCrew->Movies and moviesHasCrew->Crew relationships to handle it for you by setting the CascadeType.MERGE option on those relationship, and then use em.merge(moviesHasCrew); instead of the 3 em calls. Merging moviesHasCrew will cause it to be inserted into the database the same as persist would, but the merge will cascade over all referenced entities with relationships marked CascadeType.MERGE - so the referenced Crew and Movie will also get merged.