Объяснить поведение при сопоставлении автоматически добавленной составной последовательности идентификаторов с Hibernate

У меня есть таблица

CREATE TABLE `SomeEntity` (
     `id` int(11) NOT NULL AUTO_INCREMENT,
     `subid` int(11) NOT NULL DEFAULT '0',
     PRIMARY KEY (`id`,`subid`),

У меня есть класс сущности с полем автоматического увеличения в нем. Я хочу прочитать идентификатор автоматического увеличения, назначенный ему, когда он будет сохранен

Аннотации к геттеру приведены ниже

  private long id;
   private int subid;
  @Id
  @GeneratedValue **//How do i correct this to have multiple rows with same id and different subid**
  @Column(name = "id")
  public long getId() {
    return id;
  }

  @Id
  @Column(name = "subid")
  public int getSubid() {
    return subid;
  }

Я хочу иметь объекты как

id 1 subid 0 
id 1 subid 1
id 1 subid 2
id 2 subid 0

subid по умолчанию 0 в базе данных, и я постепенно увеличиваю его на обновления в этой строке. Я попробовал решение, как в этом сообщении SO JPA - возврат автоматически сгенерированного идентификатора после сохранения()

 @Transactional
  @Override
  public void daoSaveEntity(SomeEntity entity) {
    entityManager.persist(entity);
  }

Теперь за пределами этой транзакции я пытаюсь получить идентификатор auto increment id

      @Override
      public long serviceSaveEntity(SomeEntity entity) {
        dao.daoSaveEntity(entity);
        return entity.getId();
      }

Я вызываю это из веб-службы

  @POST
  @Produces(MediaType.APPLICATION_JSON)
  @Consumes(MediaType.APPLICATION_JSON)
  public Response createEntity(SomeEntity entity) {

Метод обновления приведен ниже

 @Transactional
  public void updateReportJob(SomeEntity someEntity) {

    Query query =
        entityManager
.createQuery("UPDATE SomeEntity SET state=:newState WHERE id = :id");
    query.setParameter("newState","PASSIVE");
    query.setParameter("id", id);
    query.executeUpdate();
    double rand = Math.random();
    int i = (int) (rand * 300);
    try {
      Thread.sleep(i);  //only to simulate concurrency issues
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    List<Integer> resList =
        entityManager.createQuery("select max(subid) from SomeEntity WHERE id = :id")
            .setParameter("id", jobId).getResultList();
    // Increment old subid by 1
    int subid = resList.get(0);
    SomeEntity.setsubid(subid + 1);
    SomeEntity.setState("ACTIVE");
    // entityManager.merge(SomeEntity);
    entityManager.persist(SomeEntity);
  }

я отправляю N одновременных обновлений из N потоков для объекта с Id 1 и несколькими другими свойствами, как показано ниже

SomeEnity entity = new SomeEntity();

 entity.setId(1);
  long num = Thread.currentThread().getId();
  entity.setFieldOne("FieldOne" + num);
  entity.setFieldTwo("" + i);
  entity.setFieldThree("" + j);
  i++;
  j++;

Случай 1 с помощью @Id в id и @Id аннотации к субиду и `entityManager.persist 'в обновлении Когда я работал с 300 потоками, некоторые с ошибкой подключения "слишком много соединений" Состояние базы данных

   id 1 subid 0 
   id 1 subid 1
   id 1 subid 2
   ..  ....
   id 1 subid 150

субад всегда инкрементален, условие гонки - только то, что будет ACTIVE, undefined из-за состояния гонки

Случай 2 с @Id в id и @Id аннотации в субиде и `entityManager.merge 'в обновлении

id 1 субад 0      id 1 subid 0      id 2 subid 0      ......      id 151 subid 0 (Возможно, только совпадение, что еще один поток, чем случай 1, был успешным?)

Случай 3 С @GeneratedValue и @Id в id и ** NO @Id аннотация по субиду и entityManager.persist в обновлении **  exception - Отдельный объект, переданный для сохранения

Случай 3 С @GeneratedValue и @Id в id и ** NO @Id аннотация по субиду и entityManager.merge в обновлении **  если обновление выполняется последовательно, состояние базы данных

id 1 subid 0

после следующего обновления

id 1 subid 1

после обновления каждой новой версии обновления (что приводит только к одной строке за раз)

id 1 subid 2

Случай 4, аналогичный случаю 3 с параллельными обновлениямиесли запустить одновременно (с 300 потоками), я получаю следующее исключение

 org.hibernate.HibernateException: More than one row with the given identifier was found: 1
Состояние базы данных

id 1 subid 2 (Только один поток был бы успешным, но из-за обновленного состояния ранга от 0 до 2)

Случай 5 С @GeneratedValue и @Id на id и @Id аннотации на субад
Create также терпит неудачу с subid org.hibernate.PropertyAccessException: IllegalArgumentException произошло при вызове setter of SomeEntity.id

Пожалуйста, объясните причины. Из javadoc методов я знаю, что

persist - сделать экземпляр управляемым и постоянным.

merge - объединить состояние данного объекта в текущий контекст сохранения.

Мой вопрос больше связан с тем, как hibernate управляет аннотациями. Почему в случае 3 существует исключение отдельного объекта, если сеанс еще не закрыт? Почему в случае 5 существует исключение IllegalArgumentException?

Я использую hibernate 3.6 mysql 5 и Spring 4 Также попробуйте указать такой инкрементный id и субад. (Использование пользовательского SelectGenerator с демонстрационной реализацией или любым другим способом без выполнения столбца concat)

Ответ 1

Поскольку поле id уже уникально и автоматически увеличивается, в этом случае вам не нужен составной идентификатор, чтобы ваш объект мог выглядеть так:

@Id
@Column(name = "id")
public long getId() {
    return id;
}

@Column(name = "subid")
public int getSubid() {
    return subid;
}

Объект может быть извлечен с помощью идентификатора с помощью диспетчера сущностей:

entityManager.find(MyEntity.class, entityId); 

или вы можете получить объект, используя запрос, который принимает как id, так и subid:

MyEntity myEntity = entityManager.createTypeQuery("select me from MyEntity where id = :id and subid = :subid", MyEntity.class)
    .setParameter("id", entityId) 
    .setParameter("subid", entitySubId) 
    .getSingleResult();

Hibernate также имеет SelectGenerator, который может извлекать идентификатор из столбца базы данных, что полезно, когда база данных генерирует идентификатор, используя вызывать.

К сожалению, он не работает с составными идентификаторами, поэтому вы завялили свой собственный расширенный SelectGenerator или использовали столбец id_sub_id, который объединяет id и sub-id в один столбец VARCHAR:

'1-0'
'1-1'
'2-0'
'2-1' 

Вам нужно написать триггер базы данных, чтобы обновить два столбца, используя хранимую процедуру, специфичную для базы данных, и объединить два столбца в VARCHAR. Затем вы сопоставляете агрегированный столбец с помощью стандартного SelectGenerator в поле String:

@Id
@Column(name = "id_sub_id")
@GeneratedValue( strategy = "trigger" )
@GenericGenerator( 
    name="trigger", strategy="org.hibernate.id.SelectGenerator",
    parameters = {
        @Parameter( name="keys", value="id_sub_id" )
    }
)
public String getId() {
    return id;
}

Ответ 2

Скажем, у меня есть несколько книг с идентификатором и версией. Идентификатор принадлежит книге который может иметь много обновленных версий, причем последний из них является текущим которая будет в основном запрашиваться. Что было бы лучше подход? Должен ли я использовать отображение "один ко многим"

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

enter image description here

Сохранение книги:

@Transactional
public void saveBook(Book book) {
    em.persist(book);
}

Сохранение версии книги:

@Transactional
public void saveBookVersion(BookVersion bookVersion) {
    Book book = em.find(Book.class, bookVersion.getBook().getId());
    bookVersion.setBook(book);
    book.setLatestBookVersion(bookVersion);
    em.persist(bookVersion);
}

Получить последнюю версию книги:

@Transactional(readOnly=true)
public Book getLatestBookVersion(Long bookId) {
   // it is enough if lastestBookVersion is loaded eagerly
   return em.find(Book.class, bookId);
}

Отображение схемы таблицы логической модели выше:

enter image description here

Ответ 3

Просто потому, что никто не упоминал об этом.

У меня есть таблица

CREATE TABLE `SomeEntity` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`subid` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`,`subid`),

У меня есть класс сущности с полем автоматического увеличения в нем.

[..]

subid по умолчанию 0 в базе данных, и я постепенно увеличиваю его на обновления в этой строке.

Мне это очень похоже на управление версиями.

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

@Version

Вершинный объект помечен аннотацией @Versionкак показано в следующем фрагменте кода:

public class User {
    @ID Integer id;
    @Version Integer version;
}

и соответствующая схема базы данных имеет столбец версии, например, созданный следующим оператором SQL:

CREATE TABLE `User` (
    `id` NUMBER NOT NULL, 
    `version` NUMBER,
    PRIMARY KEY (`id`)
);

Атрибутом версии может быть int, short, long или timestamp. Он увеличивается, когда транзакция успешно совершает. Это приводит к операции SQL, например:

UPDATE User SET ..., version = version + 1
WHERE id = ? AND version = readVersion

источник: blogs.oracle.com (выделение)

Я немного изменил пример. Я рекомендую использовать типы полей Integer, Long и т.д. Для полей базы данных. Даже id, который равен NOT NULL, будет иметь значение null, прежде чем вы сохраните объект в первый раз. Я считаю, что использование типов ящиков делает его явным, что эти поля могут и будут null.