JPA-правильный способ вставки в таблицу соединений (с дополнительными столбцами)


Мне удалось вставить съемочную группу для моего фильма - теперь я хочу сделать это правильно. Сущности (сокращенно):

@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;
}

Извините за "movy" и "crews", это инструменты (и подходит для отчета об ошибке)

Контроллер и форма:

@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>

И, наконец, служба:

@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
    }
}

Вопрос 1 : я хочу, чтобы поля Crew и Movie entities moviesHasCrews обновились при сохранении сущности MoviesHasCrew (т. е. drop m.addMoviesHasCrew(moviesHasCrew); em.merge(m);), но мои каскадные аннотации, похоже, этого не делают. Должен ли я сделать это наоборот ? То есть добавить к moviesHasCrews в фильмах и объединить / perist Movie и обновить MoviesHasCrew - это я читаю должен hibernate, но я работаю с generic JPA - это все еще не выполнимо в vanilla JPA ?

Вопрос 2 : краткое изложение того, как это должно быть сделано, было бы желательно (например, я должен добавить fetch=LazyMovie и Crew) @Transient к полям moviesHasCrews?). Это @MapsId("moviesIdmovie") и т. д. Необходимо в присоединяемой таблице сущностей ? Является ли это самым минимальным / элегантным способом в том, чтобы сделать это ?

Схема:

Введите описание изображения здесь

Ссылки:

2 6

2 ответа:

Проблема в том, что JPA не поддерживает обе стороны двунаправленных отношений для вас. Это гораздо более очевидно при использовании поставщика JPA, который имеет кэш второго уровня. Причина очевидна в том, что когда вы устанавливаете собственническую сторону отношений - в этом случае называйте moviesHasCrew.setCrew (w), а затем em.flush () - это приводит к обновлению базы данных FK. Но если вы сразу же проверите свою объектную модель, то увидите, что упомянутый член экипажа не имеет соответствующий экземпляр moviesHasCrew в своей коллекции. JPA не управляет вашими ссылками и не устанавливает их для вас, поэтому он не синхронизирован с тем, что находится в базе данных.

Этого следует ожидать в том же EntityManager. Однако при использовании кэша второго уровня каждый раз, когда вы выполняете запрос к этому экземпляру команды, он возвращает кэшированную копию, которая уже устарела.

Единственный способ обновить коллекцию-это перезагрузить экземпляр Crew из базы данных. Это может это можно сделать путем очистки кэша или принудительного обновления.

Лучшая альтернатива-поддерживать обе стороны двунаправленных отношений и держать их в синхронизации друг с другом. В случае кода, который у вас есть, это означает вызов:
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
}

Слияние требуется, если они являются отделенными экземплярами, так как изменение коллекций должно быть помещено в EntityManager, чтобы его можно было объединить в кэш.

Если вы хотите избежать этих слияний, вы можете положиться на moviesHasCrew - >фильмы и moviesHasCrew - >отношения экипажа, чтобы обработать его для вас, установив CascadeType.Параметр слияния для этих отношений, а затем использовать их.слияние (moviesHasCrew); вместо вызовов 3 em. Слияние moviesHasCrew приведет к тому, что он будет вставлен в базу данных так же, как и persist, но слияние будет каскадным по всем ссылочным сущностям с отношениями, помеченными CascadeType.Слияние-таким образом, упомянутая съемочная группа и фильм также будут объединены.

Я думаю, что вы не должны преследовать right way, его не существует. Лично мне не нравится слишком много каскадных операций. В чем же выгода? В этом случае я бы использовал что-то вроде:

Обслуживание:

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);
}
Он остается читаемым, каскадные операции иногда работают как по волшебству.

О @MapsId: они нужны вам из-за встроенного идентификатора MoviesHasCrewPK. Таким образом, атрибуты встроенного идентификатора сопоставляются с соответствующими значениями аннотаций @MapsId. Видеть также здесь . Я бы не стал использовать этот встроенный идентификатор, если бы не было необходимости. Сгенерированный код выглядит чище меня, но тогда у вас есть дополнительный столбец в таблице.