OneToOne между двумя таблицами с общим первичным ключом

#java #database #hibernate #jpa

#java #База данных #переход в спящий режим #jpa

Вопрос:

Я пытаюсь настроить следующие таблицы, используя JPA / Hibernate:

 User:

userid - PK
name 

Validation:

userid - PK, FK(user)
code
  

Пользователей может быть много, и у каждого пользователя может быть максимум один код проверки или ни одного.

Вот мои классы:

 public class User 
{
    @Id
    @Column(name = "userid") 
    @GeneratedValue(strategy = GenerationType.IDENTITY)    
    protected Long userId;

    @Column(name = "name", length = 50, unique = true, nullable = false)
    protected String name;

    ...
}

public class Validation 
{
    @Id
    @Column(name = "userid")
    protected Long userId;

    @OneToOne(cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn(name = "userid", referencedColumnName = "userid")
    protected User user;

    @Column(name = "code", length = 10, unique = true, nullable = false)
    protected String code;

    ...

    public void setUser(User user)
    {
        this.user = user;
        this.userId = user.getUserId();
    }

    ...
}
  

Я создаю пользователя, а затем пытаюсь добавить код проверки, используя следующий код:

 public void addValidationCode(Long userId)
{   
    EntityManager em = createEntityManager();
    EntityTransaction tx = em.getTransaction();

    try 
    {
        tx.begin();

        // Fetch the user
        User user = retrieveUserByID(userId);

        Validation validation = new Validation();
        validation.setUser(user);
        em.persist(validation);
        tx.commit();
    }
    ...
}
  

Когда я пытаюсь запустить его, я получаю org.hibernate.Исключение PersistentObjectException: отдельная сущность, переданная в persist: User

Я также пытался использовать следующий код в своем классе проверки:

 public void setUserId(Long userId)
{
    this.userId = userId;
}
  

и когда я создаю код проверки, я просто делаю:

 Validation validation = new Validation();
validation.setUserId(userId);
em.persist(validation);
tx.commit();
  

Но затем, поскольку User равен null, я получаю org.hibernate.Исключение PropertyValueException: свойство not-null ссылается на нулевое или временное значение: User.code

Был бы признателен за любую помощь относительно того, как наилучшим образом решить эту проблему!

Ответ №1:

Я смог решить эту проблему «OneToOne между двумя таблицами с общим первичным ключом» чистым способом JPA 2.0 (благодаря множеству существующих потоков на SOF). На самом деле в JPA есть два способа справиться с этим. Я использовал eclipselink в качестве поставщика JPA и MySQL в качестве базы данных. Чтобы еще раз подчеркнуть, что здесь не использовались проприетарные классы eclipselink.

  1. Первый подход заключается в использовании стратегии автоматической генерации типа в поле идентификатора родительского объекта.

    • Родительская сущность должна содержать элемент типа дочерней сущности в отношениях OneToOne (каскадный тип СОХРАНЯЕТСЯ и mappedBy = элемент типа родительской сущности дочерней сущности)

       @Entity
      @Table(name = "USER_LOGIN")
      public class UserLogin implements Serializable {
          @Id
          @GeneratedValue(strategy = GenerationType.AUTO)
          @Column(name="USER_ID")
          private Integer userId;
      
          @OneToOne(cascade = CascadeType.PERSIST, mappedBy = "userLogin")
          private UserDetail userDetail;
      // getters amp; setters
      }
        
    • Дочерний объект не должен содержать поле идентификатора. Он должен содержать элемент типа родительской сущности с аннотациями Id, OneToOne и JoinColumn. В JoinColumn должно быть указано имя поля ID таблицы базы данных.

       @Entity
      @Table(name = "USER_DETAIL")
      public class UserDetail implements Serializable {
          @Id
          @OneToOne
          @JoinColumn(name="USER_ID")
          private UserLogin userLogin;
      // getters amp; setters
      }
        
    • Описанный выше подход внутренне использует таблицу БД по умолчанию с именем SEQUENCE для присвоения значений полю идентификатора. Если еще не присутствует, эту таблицу необходимо создать, как показано ниже.

       DROP TABLE TEST.SEQUENCE ;
      CREATE TABLE TEST.SEQUENCE (SEQ_NAME VARCHAR(50), SEQ_COUNT DECIMAL(15));
      INSERT INTO TEST.SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 0);
        
  2. Второй подход заключается в использовании настраиваемой стратегии создания типов ТАБЛИЦ и аннотации TableGenerator в поле идентификатора родительского объекта.

    • За исключением вышеуказанного изменения в поле идентификатора, все остальное остается неизменным в родительском объекте.

       @Entity
      @Table(name = "USER_LOGIN")
      public class UserLogin implements Serializable {
          @Id
          @TableGenerator(name="tablegenerator", table = "APP_SEQ_STORE", pkColumnName = "APP_SEQ_NAME", pkColumnValue = "USER_LOGIN.USER_ID", valueColumnName = "APP_SEQ_VALUE", initialValue = 1, allocationSize = 1 )  
          @GeneratedValue(strategy = GenerationType.TABLE, generator = "tablegenerator")
          @Column(name="USER_ID")
          private Integer userId;
      
          @OneToOne(cascade = CascadeType.PERSIST, mappedBy = "userLogin")
          private UserDetail userDetail;
      // getters amp; setters
      }
        
    • В дочернем объекте изменений нет. Это остается таким же, как и в первом подходе.

    • Этот подход к созданию таблиц внутренне использует таблицу DB APP_SEQ_STORE для присвоения значений полю идентификатора. Эту таблицу необходимо создать, как показано ниже.

       DROP TABLE TEST.APP_SEQ_STORE;
      CREATE TABLE TEST.APP_SEQ_STORE
      (
          APP_SEQ_NAME VARCHAR(255) NOT NULL,
          APP_SEQ_VALUE BIGINT NOT NULL,
          PRIMARY KEY(APP_SEQ_NAME)
      );
      INSERT INTO TEST.APP_SEQ_STORE VALUES ('USER_LOGIN.USER_ID', 0);
        

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

1. первый подход не приводит к бесконечному циклу при попытке загрузить UserLogin?

Ответ №2:

Если вы используете режим гибернации, вы также можете использовать

 public class Validation {

    private Long validationId;
    private User user;

    @Id
    @GeneratedValue(generator="SharedPrimaryKeyGenerator")
    @GenericGenerator(name="SharedPrimaryKeyGenerator",strategy="foreign",parameters =  @Parameter(name="property", value="user"))
    @Column(name = "VALIDATION_ID", unique = true, nullable = false)
    public Long getValidationId(){
        return validationId;
    }

    @OneToOne
    @PrimaryKeyJoinColumn
    public User getUser() {
        return user;
    }

}
  

Hibernate гарантирует, что идентификатор проверки будет таким же, как идентификатор пользовательского набора объектов.

Ответ №3:

Используете ли вы JPA или JPA 2.0?

Если проверка PK — это FK для пользователя, то вам не нужен атрибут Long userId в классе проверки, но вместо этого выполняйте @Id аннотацию самостоятельно. Это было бы:

 Public class Validation 
{
    @Id
    @OneToOne(cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn(name = "userid", referencedColumnName = "userid")
    protected User user;

    @Column(name = "code", length = 10, unique = true, nullable = false)
    protected String code;

    ...

    public void setUser(User user)
    {
        this.user = user;
        this.userId = user.getUserId();
    }

    ...
}
  

Попробуйте с этим и сообщите нам свои результаты.

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

1. Как вы можете задать userId поле в setUser() , если вы его не определяете?

Ответ №4:

Вам нужно установить оба userId и user .

Если вы задаете только user , то id for Validation равно 0 и считается отсоединенным. Если вы задаете только userId , то вам нужно сделать user свойство обнуляемым, что здесь не имеет смысла.

На всякий случай вы, вероятно, можете установить их оба в одном вызове метода:

 @Transient
public void setUserAndId(User user){
    this.userId = user.getId();
    this.user = user;
}
  

Я пометил метод @Transient , чтобы Hibernate игнорировал его. Кроме того, таким образом, вы все еще можете иметь setUser и setUserId работать, как ожидалось, без каких-либо «побочных эффектов».