8

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:

Community
  • 1
  • 1
Mr_and_Mrs_D
  • 32,208
  • 39
  • 178
  • 361

2 Answers2

3

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.

Chris
  • 20,138
  • 2
  • 29
  • 43
  • Edited the question incorporating some of your suggestions - at some point I encountered a "Can not refresh not managed object" IAE. Still interested in some skeleton code for this, as the one I provide but commented/amended. A crew member can have many roles btw. – Mr_and_Mrs_D Feb 25 '14 at 20:33
  • The cannot refresh was due to me thinking that `merge(m)` would suffice - while I need `m=merge(m)` - will check this. – Mr_and_Mrs_D Feb 27 '14 at 00:33
  • On your answer - do you think that `crew.addMoviesHasCrews(this); movie.addMoviesHasCrews(this);` are unavoidable (one way or the other) ? Since I call `em.merge(moviesHasCrew);` there should be a way to tell JPA that the related (joined) entities should update their `List moviesHasCrews;`. Is this what `org.hibernate.annotations.CascadeType.SAVE_UPDATE` does (see [here](http://giannigar.wordpress.com/2009/09/04/mapping-a-many-to-many-join-table-with-extra-column-using-jpa/))? No way in vanilla JPA (maybe adding other annotations on the entities or performing `merge` on `m,w`) ? – Mr_and_Mrs_D Feb 27 '14 at 00:35
  • Also your answer presupposes that the `w,m` _instances_ are managed by JPA or not ? Cause I get them from the UI and are not managed as far as I understand by JPA (hence my `Can not refresh not managed object`) – Mr_and_Mrs_D Feb 27 '14 at 00:38
  • The mappings shown in the answer set the cascade.Merge on the MoviesHasCrew->crew and Movie relations, so if you use merge on the MoviesHasCrew instance, it cascades to the crew and movie and causes them to become managed. The SAVE_UPDATE is a Hibernate specific cascade option; it seems similar to JPA cascade.merge and cascade.persist. It does not perform relationship maintenance which is what you seem to be after. JPA requires you to set both sides of bidirectional relationships and does not do it for you. There are ways around it, but there are many other posts on them and their problems – Chris Feb 27 '14 at 02:18
  • Will try this at home, thanks. Meanwhile could you elaborate (in the answer) on the `JPA requires you to set both sides of bidirectional relationships and does not do it for you. There are ways around it, but there are many other posts on them and their problems` as this is the confusing part. – Mr_and_Mrs_D Feb 27 '14 at 10:52
  • JPA does not maintain bidirectional relationships and is the topic of many discussions. It allows for different levels of caching, so you are required to keep your object model in sync with what you are putting in the database, or the cached entities won't reflect everything until they are refreshed in some way. It is much easier to just set the 'other' side of bidirectional relationships and merge the referenced entity than it is to worry about caches, refreshing etc. – Chris Feb 27 '14 at 14:59
  • The info on caching is the last part of the question - it is about the `fetch` annotations I asked - but I'd rather you edit with more general info. How does your solution above interact with caching ? – Mr_and_Mrs_D Feb 27 '14 at 17:22
  • I am accepting this but I would _appreciate_ your going through the comments and the edited out material and add some back to your answer so it is a more of a review answer – Mr_and_Mrs_D Mar 03 '14 at 11:12
1

I think you shouldn't chase the right way, it doesn't exist. Personally I don't like too many cascade operations. What's the gain? In this case I'd use something like:

Service:

public void addCrew(long movieId, long crewId) {
     Movie m = em.getReference(Movie.class, movieId);
     Crew w = em.getReference(Crew.class, crewId);
     MoviesHasCrew moviesHasCrew = new MoviesHasCrew();
     moviesHasCrew.setCrewAndMovie(w,m);
     moviesHasCrew.setRole(Role.DEFAUT_ROLE);
     em.persist(moviesHasCrew);
}

MoviesHasCrew:

public void setCrewAndMovie(Crew c, Movie m){
    this.crew = c;
    this.movie = m;
    m.addMoviesHasCrew(this);
    c.addMoviesHasCrew(this);
}

It stays readable, cascade operations work sometimes like magic.

About the @MapsId: you need them because of the embedded id MoviesHasCrewPK. This way the attributes of the embedded id are mapped to the corresponding values of the @MapsId annotations. See also here. I wouldn't use this embedded id if I didn't have to. A generated id looks cleaner to me, but then you have an extra column in the join table.

Balázs Németh
  • 6,222
  • 9
  • 45
  • 60
  • Maybe no Right Way but I am interested in some skeleton code for this, like the one I provide - which contains at least the absolute minimum of setting this relation up and possible points of extension. Why are you using `em.getReference` ? – Mr_and_Mrs_D Feb 25 '14 at 20:27
  • That won't reload the whole entity, just get its reference (id). Compare to `em.find`. But the reason why I'm not passing the whole entity, just the id, is that it's more safe this way. Imagine if someone modifies `Crew` or `Movie`, and you do a `merge`. Obviously you don't want to update them, only their relations. – Balázs Németh Feb 26 '14 at 08:46