Отношение JPA "многие к одному" - нужно сохранить только идентификатор

У меня 2 класса: Водитель и Автомобиль. Таблица автомобилей обновляется в отдельном процессе. Что мне нужно, так это иметь свойство в Driver, которое позволяет мне читать полное описание автомобиля и записывать только идентификатор, указывающий на существующий автомобиль. Вот пример:

@Entity(name = "DRIVER")
public class Driver {
... ID and other properties for Driver goes here .....

    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "CAR_ID")
    private Car car;

    @JsonView({Views.Full.class})
    public Car getCar() {
      return car;
    }
    @JsonView({Views.Short.class})
    public long getCarId() {
      return car.getId();
    }
    public void setCarId(long carId) {
      this.car = new Car (carId);
    }

}

Объект Car — это обычный объект JPA без обратной ссылки на Driver.

Итак, чего я пытался добиться этим:

  1. Я могу прочитать полное описание автомобиля, используя подробный JSON View
  2. или я могу прочитать только идентификатор автомобиля в Short JsonView
  3. и самое главное, при создании нового водителя я просто хочу передать JSON ID автомобиля. Таким образом, мне не нужно делать ненужные чтения для автомобиля во время сохранения, а просто обновлять идентификатор.

Я получаю следующую ошибку:

object references an unsaved transient instance - save the transient instance before flushing : com.Driver.car -> com.Car

Я не хочу обновлять экземпляр автомобиля в БД, а просто ссылаюсь на него из драйвера. Есть идеи, как добиться того, чего я хочу?

Спасибо.

ОБНОВЛЕНИЕ: забыл упомянуть, что идентификатор автомобиля, который я передаю во время создания драйвера, является действительным идентификатором существующего автомобиля в БД.


person Andrei V    schedule 13.01.2015    source источник


Ответы (7)


Это сообщение об ошибке означает, что у вас есть временный экземпляр в графе объектов, который явно не сохраняется. Краткий обзор статусов, которые может иметь объект в JPA:

  • Временный: новый объект, который еще не был сохранен в базе данных (и, следовательно, неизвестен диспетчеру сущностей). Для него не задан идентификатор.
  • Управляемый: объект, который отслеживает менеджер сущностей. Управляемые объекты — это то, с чем вы работаете в рамках транзакции, и все изменения, внесенные в управляемый объект, будут автоматически сохранены после фиксации транзакции.
  • Отсоединенный: ранее управляемый объект, который по-прежнему доступен после фиксации транзакции. (Управляемый объект вне транзакции.) Имеет набор идентификаторов.

Сообщение об ошибке сообщает вам, что (управляемый/отсоединенный) объект Driver, с которым вы работаете, содержит ссылку на объект Car, который неизвестен Hibernate (он является временным). Чтобы заставить Hibernate понять, что любые несохраненные экземпляры Car, на которые ссылается Driver для сохранения, также должны быть сохранены, вы можете вызвать метод persist из EntityManager.

В качестве альтернативы вы можете добавить каскад на сохранение (я думаю, что это только что пришло мне в голову, не проверял его), который будет выполнять сохранение на автомобиле до сохранения драйвера.

@ManyToOne(fetch=FetchType.LAZY, cascade=CascadeType.PERSIST)
@JoinColumn(name = "CAR_ID")
private Car car;

Если вы используете метод слияния entitymanager для хранения драйвера, вы должны вместо этого добавить CascadeType.MERGE или и то, и другое:

@ManyToOne(fetch=FetchType.LAZY, cascade={ CascadeType.PERSIST, CascadeType.MERGE })
@JoinColumn(name = "CAR_ID")
private Car car;
person Tobb    schedule 13.01.2015
comment
Спасибо, Тобб. Итак, если я создаю драйвер из следующего Json {имя: Джо Доу, carId: 123}, где автомобиль с идентификатором 123 уже существует, то объект существует в БД. Означает ли это, что я не могу обновить Dirver::carId, если я также не обновлю указанный Car? - person Andrei V; 14.01.2015
comment
Первое, что нужно знать о JPA, это то, что вы должны перестать думать с точки зрения базы данных. JPA имеет дело с объектами Java, это то, что вы запрашиваете, и это то, что вы получаете. Driver не имеет атрибута carId, у него есть атрибут Car. Итак, если вы получите эти данные (я думаю, имя — это имя нового водителя), вы должны сначала выполнить EntityManager#find для carId, чтобы получить объект Car. Затем создайте новый драйвер, назначьте ему выбранный автомобиль и отправьте его в EntityManager#persist. Поскольку автомобиль уже сохранен и управляется, я не думаю, что вам нужен какой-либо каскад, чтобы это сработало. - person Tobb; 14.01.2015
comment
Я понимаю. Я предполагаю, что у меня нет возможности создать объект Car, как я, и пометить его как сохраненный (скармливая ему только идентификатор). Кажется неудобным ограничением для тех, кто пришел из чистого мира запросов к БД, где обновление столбца идентификатора является простой задачей. Я предполагаю, что другим решением было бы изменить поле CAR_ID с @ManyToOne на основное длинное поле и добавить поле Transient Car. Это позволит мне работать только с идентификаторами во время получения и установки, и когда мне нужно получить подробную информацию об автомобиле, я могу установить поле автомобиля из контроллера ресурсов, явно прочитав этот автомобиль. Ценю твою помощь. - person Andrei V; 14.01.2015
comment
Это противоречило бы цели JPA, если вы хотите так работать, вам следует проверить Spring JDBC, там вы можете написать все запросы самостоятельно. Использование фреймворка против его цели редко бывает хорошей идеей. - person Tobb; 14.01.2015
comment
Хм, разве это не звучит неуклюже, когда, если мне нужно создать/обновить драйвер, мне придется делать ненужное чтение (чтобы прочитать автомобиль из идентификатора), даже если я не использую все эти данные. Представьте, что у вас мало таких свойств, и вместо одного сохранения вы в конечном итоге делаете N чтений перед сохранением (где N — количество свойств, таких как Car) только ради идеологии. Я бы поморщился, считая это неоптимальным. - person Andrei V; 14.01.2015
comment
Это так, но мы должны помнить, что сейчас не 1983 год. Многие принципы, которые мы используем, действительно устарели и не применимы сегодня, с обилием мощности процессора, памяти и дискового пространства. JPA никоим образом не является оптимальным, но в 95% случаев он достаточно хорош. При этом я в настоящее время работаю над ускорением некоторых запросов JPA, и это может быть проблемой. Это действительно то, что зависит от проекта, вы должны сопоставить скорость с простотой программирования. В большинстве случаев скорость не будет проблемой, поэтому использование JPA будет правильным выбором, так как это сэкономит ваше время. - person Tobb; 14.01.2015
comment
В тех немногих случаях, когда скорость является приоритетом номер один (весит больше, чем стоимость разработки), следует избегать JPA, вместо этого следует использовать JDBC (в этих случаях будет очень полезен шаблон Spring JDBC, поскольку он удаляет много шаблонный код и приводит к более красивому коду.) - person Tobb; 14.01.2015
comment
Спасибо за обзор Тобб. - person Andrei V; 15.01.2015
comment
Проверьте ответ @scetix ниже, чтобы узнать, как использовать EntityManager.getReference() для обхода ненужного чтения. - person Ali; 26.11.2019

Вы можете сделать это с помощью getReference вызова в EntityManager:

EntityManager em = ...;
Car car = em.getReference(Car.class, carId);

Driver driver = ...;
driver.setCar(car);
em.persist(driver);

Это не будет выполнять оператор SELECT из базы данных.

person scetix    schedule 12.09.2015
comment
В Spring используйте JpaRepository#getOne(), см. Best Практика повышения производительности для Hibernate 5 и Spring Boot 2 (часть 1), пункт 11. Заполнение дочерней родительской ассоциации через прокси - person Adi Sutanto; 20.03.2019
comment
Я думаю, это то, что искал ОП, я искал то же самое, я хочу сохранить дочерние элементы без необходимости читать родителя из БД, поскольку мне это не нужно на данный момент, поэтому делаю выбор к БД было бы неэффективно - person Osmar; 09.02.2020

В качестве ответа на окутане см. Фрагмент:

@JoinColumn(name = "car_id", insertable = false, updatable = false)
@ManyToOne(targetEntity = Car, fetch = FetchType.EAGER)
private Car car;

@Column(name = "car_id")
private Long carId;

Итак, что здесь происходит, так это то, что когда вы хотите выполнить вставку/обновление, вы только заполняете поле carId и выполняете вставку/обновление. Поскольку поле car не вставляется и не обновляется, Hibernate не будет жаловаться на это, и поскольку в вашей модели базы данных вы в любом случае заполняете свой car_id только как внешний ключ, этого достаточно на данный момент (и ваши отношения внешнего ключа в базе данных будут обеспечить целостность ваших данных). Теперь, когда вы извлекаете свой объект, поле car будет заполнено Hibernate, что дает вам гибкость, когда только ваш родитель извлекается, когда это необходимо.

person Jonck van der Kogel    schedule 16.05.2018
comment
› Теперь, когда вы извлекаете свой объект, поле car будет заполнено Hibernate, что дает вам гибкость, когда только ваш родитель извлекается, когда это необходимо. Но тогда не должно ли fetch = FetchType.EAGER быть LAZY? - person Niklas; 11.04.2020
comment
@Никлас, на самом деле это не имеет значения, это может быть ЖЕЛАНИЕ или ЛЕНЬ, в зависимости от вашего варианта использования. Дело в том, что это оптимизация записи, поэтому вам не нужно извлекать родительский объект перед вставкой дочернего объекта, вы можете просто заполнить идентификатор, сохраняя туда и обратно в базу данных. Когда вы извлекаете своего ребенка, вы, вероятно, хотите, чтобы был получен полный граф, включая поле автомобиля, и (по крайней мере, в моем случае) я хотел получить его, используя 1 запрос, а не 2, поэтому EAGER. Если это еще не ясно, дайте мне знать, и я разработаю полный рабочий пример, демонстрирующий это. - person Jonck van der Kogel; 13.04.2020
comment
О, это действительно полезно. Я путал вещи раньше, но теперь это намного яснее. - person Niklas; 13.04.2020

Вы можете работать только с идентификатором car следующим образом:

@JoinColumn(name = "car")
@ManyToOne(targetEntity = Car.class, fetch = FetchType.LAZY)
@NotNull(message = "Car not set")
@JsonIgnore
private Car car;

@Column(name = "car", insertable = false, updatable = false)
private Long carId;
person Radu Gancea    schedule 06.03.2017
comment
Установив insertable = false, updateable = false для объекта, а не для идентификатора, мне удалось вставить дочерний элемент с допустимым родительским идентификатором без необходимости извлечения родителя. Спасибо за совет! - person Jonck van der Kogel; 14.02.2018
comment
@JonckvanderKogel не поделитесь фрагментом? :) - person okutane; 15.05.2018
comment
@okutane, пожалуйста, посмотрите фрагмент ниже. Надеюсь, поможет! - person Jonck van der Kogel; 16.05.2018

public void setCarId(long carId) {
      this.car = new Car (carId);
    }

На самом деле это не сохраненная версия файла car. Итак, это временный объект, потому что у него нет id. JPA требует, чтобы вы заботились об отношениях. Если объект является новым (не управляется контекстом), его следует сохранить, прежде чем он сможет связываться с другими управляемыми/отсоединенными объектами (фактически ГЛАВНЫЙ объект может поддерживать своих дочерних элементов с помощью каскадов).

Два способа: каскады или сохранение и извлечение из базы данных.

Также вам следует избегать установки объекта ID вручную. Если вы не хотите обновлять/сохранять автомобиль с помощью его ГЛАВНОГО объекта, вам следует получить CAR из базы данных и поддерживать свой драйвер с его экземпляром. . Итак, если вы сделаете это, Car будет отсоединен от контекста персистентности, НО по-прежнему будет иметь идентификатор и может быть связан с любой сущностью без каких-либо последствий.

person 00Enthusiast    schedule 13.01.2015
comment
Спасибо Энтузиаст. Означает ли это, что он временный, потому что Id не существует, или я каким-то образом не инициализировал его, прочитав? Причина Id машины, которую я проезжаю, действителен - person Andrei V; 14.01.2015
comment
Да, потому что он не регистрируется в контексте постоянства. JPA вообще не может видеть его как сохраненный объект! :) Даже его нет в БД, так как же можно соотносить его с тем, чего нет?:) - person 00Enthusiast; 14.01.2015

Добавьте необязательное поле, равное false, как показано ниже.

@ManyToOne(optional = false) // Telling hibernate trust me (As a trusted developer in this project) when building the query that the id provided to this entity is exists in database thus build the insert/update query right away without pre-checks
private Car car; 

Таким образом, вы можете установить только идентификатор автомобиля как

driver.setCar(new Car(1));

а затем сохранить нормальный драйвер

driverRepo.save(driver);

Вы увидите, что автомобиль с идентификатором 1 идеально назначен водителю в базе данных.

Описание:

Итак, что делает этот крошечный optional=false, может быть, это поможет больше https://stackoverflow.com/a/17987718

person Youans    schedule 30.03.2019
comment
Это также меняет семантику отношения. Когда установлено optional=false, оно изменяет значение по умолчанию (true), и с этого момента нет возможности сохранить экземпляр драйвера, у которого есть (driver.getCar() == null) == true. Если это нормально, возможно, это ограничение должно быть там до того, как задан какой-либо вопрос. - person Lubo; 09.02.2020

Использовать каскад в аннотации manytoone @manytoone(cascade=CascadeType.Remove)

person user3619843    schedule 13.01.2015