Настройте JPA, чтобы позволить PostgreSQL генерировать значение первичного ключа.

Итак, наш проект использует базу данных PostgreSQL, и мы используем JPA для работы с базой данных. Мы создали объекты из базы данных с автоматическим созданием в Netbeans 7.1.2.

После небольших изменений наши значения первичного ключа описываются как:

@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Basic(optional = false)
@NotNull
@Column(name = "idwebuser", nullable = false)
private Integer idwebuser;

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

Есть ли вероятность того, что JPA может просто позволить базе данных автоматически сгенерировать идентификатор, а затем получить его после процесса создания? Или что может быть лучшим решением? Спасибо.

EDIT Более конкретно: у нас есть таблица пользователей, и моя проблема заключается в том, что при использовании любого типа типа генерации стратегии JPA вставляет новый объект с указанным идентификатором генератора. Что для меня неправильно, т.к. если я вношу изменения в таблицу самостоятельно, добавляя новые записи, то GeneratedValue для приложения будет ниже текущего ID - что приводит нас к исключению с дублирующимся ID. Можем ли мы это исправить ;)?

короткое примечание к ответу С моей стороны было немного лжи, потому что мы использовали PG Admin -> View first 100 Rows и редактировали строки оттуда вместо использования select. В ЛЮБОМ СЛУЧАЕ получается, что этот редактор каким-то образом пропускает процесс обновления идентификатора, и поэтому даже в БД, когда мы пишем правильный INSERT, он ВЫПОЛНЯЕТСЯ с неправильным идентификатором! Так что в основном это была проблема редактора, который мы использовали, а не базы данных и приложения...

теперь это работает даже с использованием @GeneratedValue(strategy=GenerationType.IDENTITY)


person Atais    schedule 06.08.2012    source источник
comment
Означает ли это, что JPA не использует последовательность PostgreSQL? Каково определение этого столбца в таблице? Это serial или просто целое число?   -  person a_horse_with_no_name    schedule 06.08.2012
comment
Мы используем последовательный тип поля. Подробнее опишу проблему в основном вопросе, @a_horse_with_no_name   -  person Atais    schedule 06.08.2012
comment
Значит ли это, что JPA не использует связанную последовательность? Это странно.   -  person a_horse_with_no_name    schedule 06.08.2012
comment
Итак, во время тестов у нас теперь около 17 строк, и мы используем: @SequenceGenerator(name="pk_sequence",sequenceName="entity_id_seq") @GeneratedValue(strategy=GenerationType.SEQUENCE,generator="pk_sequence") И мы только что получили ошибку Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.2.0.v20110202-r8913): org.eclipse.persistence.exceptions.DatabaseException Internal Exception: org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "webuser_idwebuser_pk" Detail: Key (idwebuser)=(14) already exists. Так что, похоже, что-то все еще не так.   -  person Atais    schedule 06.08.2012
comment
Существует конфликт в использовании последовательности. По-видимому, не все вставки используют одинаковую стратегию создания ПК.   -  person a_horse_with_no_name    schedule 06.08.2012
comment
@a_horse_with_no_name, Ну что сказать, я знаю, потому что, когда я пишу простой запрос INSERT (xx,xxx) INTO WEBUSER (просто пример); Я не указываю никакого Генератора. Значит ли это, что я не могу работать с самой базой данных, когда она используется приложением?   -  person Atais    schedule 06.08.2012
comment
конечно, вы можете, вам просто нужно указать JPA/Hibernate/... использовать ту же стратегию. Я считаю, что Ответ Крейга показывает вам все, что вам нужно.   -  person a_horse_with_no_name    schedule 06.08.2012


Ответы (5)


Учитывая определение таблицы:

CREATE TABLE webuser(
    idwebuser SERIAL PRIMARY KEY,
    ...
)

Используйте сопоставление:

@Entity
@Table(name="webuser")
class Webuser {

    @Id
    @SequenceGenerator(name="webuser_idwebuser_seq",
                       sequenceName="webuser_idwebuser_seq",
                       allocationSize=1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
                    generator="webuser_idwebuser_seq")
    @Column(name = "idwebuser", updatable=false)
    private Integer id;

    // ....

}

Именование tablename_columname_seq — это имя последовательности PostgreSQL по умолчанию для SERIAL, и я рекомендую вам придерживаться его.

allocationSize=1 важен, если вам нужен Hibernate для взаимодействия с другими клиентами базы данных.

Обратите внимание, что в этой последовательности будут «пробелы», если транзакции откатятся. Транзакции могут откатиться по разным причинам. Ваше приложение должно быть разработано, чтобы справиться с этим.

  • Никогда не предполагайте, что для любого идентификатора n существует идентификатор n-1 или n+1.
  • Никогда не предполагайте, что идентификатор n был добавлен или зафиксирован до идентификатора меньше n или после идентификатора больше n. Если вы действительно осторожны с тем, как вы используете последовательности, вы можете сделать это, но вы никогда не должны пытаться; вместо этого запишите метку времени в свою таблицу.
  • Никогда не добавляйте и не вычитайте из идентификатора. Сравните их на равенство и ничего больше.

См. документацию PostgreSQL для последовательностей и последовательные типы данных.

Они объясняют, что приведенное выше определение таблицы в основном является сокращением для:

CREATE SEQUENCE idwebuser_id_seq;
CREATE TABLE webuser(
    idwebuser integer primary key default nextval('idwebuser_id_seq'),
    ...
)
ALTER SEQUENCE idwebuser_id_seq OWNED BY webuser.idwebuser;

... что должно помочь объяснить, почему мы добавили аннотацию @SequenceGenerator для описания последовательности.


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


Примечание. Если вместо этого определение вашей таблицы выглядит следующим образом:

CREATE TABLE webuser(
    idwebuser integer primary key,
    ...
)

и вы вставляете в него с помощью (небезопасно, не используйте):

INSERT INTO webuser(idwebuser, ...) VALUES ( 
    (SELECT max(idwebuser) FROM webuser)+1, ...
);

или (небезопасно, никогда не делайте этого):

INSERT INTO webuser(idwebuser, ...) VALUES ( 
    (SELECT count(idwebuser) FROM webuser), ...
);

то вы делаете это неправильно и должны переключиться на последовательность (как показано выше) или на правильную реализацию последовательности без пропусков с использованием заблокированной таблицы счетчиков (опять же, см. выше и см. «последовательность postgresql без пропусков» в Google). Оба вышеперечисленных действия делают неправильную вещь, если в базе данных работает более одного соединения.

person Craig Ringer    schedule 06.08.2012
comment
Я не знаю, почему и как, но это все еще не работает; мы, очевидно, используем CREATE TABLE webuser( idwebuser SERIAL PRIMARY KEY, ... ) В объекте я отредактировал его с аннотацией последовательности, как указано в вашем примере. Еще: когда я запускаю приложение и регистрирую нового пользователя - он вставил его без проблем; затем я добавил 3 фиктивные строки со следующими идентификаторами с помощью PGAdmin и попытался зарегистрировать следующего пользователя, что привело к ошибке: ERROR: duplicate key value violates unique constraint "webuser_idwebuser_pk" Detail: Key (idwebuser)=(20) already exists. Меня тоже не волнуют пробелы, @Craig. - person Atais; 06.08.2012
comment
@Atais Все, что я могу вам сказать на данный момент, это то, что он отлично работает для меня и требуется для работы в соответствии со спецификацией и документацией PostgreSQL. Нам нужно выяснить, чем отличается ваша установка. Возможно, вам следует отредактировать свой ответ, чтобы показать обновленное сопоставление и показать точный текст запросов на вставку, которые вы использовали в PgAdmin-III. Попробуйте также включить log_statement = 'all' в postgresql.conf, перезапустить Pg и изучить журналы, чтобы увидеть, какие операторы Hibernate действительно выполняются, или включить hibernate.show_sql=true в persistence.xml. - person Craig Ringer; 06.08.2012
comment
ОМГ, я только что понял это! С моей стороны была небольшая ложь, потому что мы использовали PG Admin -> View first 100 Rows и редактировали оттуда строки вместо использования select. В ЛЮБОМ СЛУЧАЕ получается, что этот редактор каким-то образом пропускает процесс обновления идентификатора, и поэтому даже в БД, когда мы пишем правильный INSERT, он ВЫПОЛНЯЕТСЯ с неправильным идентификатором! Так что в основном это была проблема используемого нами редактора, а не базы данных и приложения... В любом случае спасибо! - person Atais; 06.08.2012
comment
@Atais Рад узнать, что проблема решена. Надеюсь, вы изучили некоторые приемы отладки и в следующий раз. Поверьте мне, в следующий раз при работе с JPA произойдет необъяснимое, что, черт возьми, происходит... - person Craig Ringer; 06.08.2012
comment
Просто для будущих гуглов: теперь это работает даже с использованием @GeneratedValue(strategy=GenerationType.IDENTITY) - person Atais; 06.08.2012
comment
@Atais Это работает в EclipseLink, но в последний раз, когда я проверял (по общему признанию, некоторое время назад), это не работало в Hibernate, мне пришлось использовать явную последовательность. Спецификация JPA 2.1 услужливо говорит, что эта спецификация не определяет точное поведение этих стратегий :-( - person Craig Ringer; 06.08.2012
comment
en.wikibooks.org/wiki/Java_Persistence/: Хотя последовательность идентификации выглядит самый простой способ присвоить идентификатор, у них есть несколько проблем. Во-первых, поскольку идентификатор не назначается базой данных до тех пор, пока строка не будет вставлена, идентификатор не может быть получен в объекте до тех пор, пока не произойдет фиксация или вызов сброса. Последовательность удостоверений также не допускает предварительного выделения последовательности, поэтому может потребоваться выборка для каждого вставляемого объекта, что может привести к серьезным проблемам с производительностью, поэтому в целом не рекомендуется. - person jansohn; 26.11.2015
comment
@Atais Большое спасибо, что вы нашли время и опубликовали эту строку кода, что позволило нам сэкономить много времени. Спасибо! - person Pavel_K; 04.08.2016

Кажется, вам нужно использовать генератор последовательности, например:

@GeneratedValue(generator="YOUR_SEQ",strategy=GenerationType.SEQUENCE)
person Denis Zevakhin    schedule 06.08.2012
comment
Могу ли я заставить Sequence возвращать значение текущего select count(*) from webuser вместо значения, которое будет увеличено приложением, @a_horse_with_no_name? - person Atais; 06.08.2012
comment
@Atais: Не используйте count() для создания идентификаторов. Никогда. Это просто не сработает. Используйте последовательность. Если вас беспокоят пробелы в цифрах, значит, у вас что-то не так с дизайном ПК. Последовательность — это наиболее эффективное, производительное, масштабируемое и надежное решение для создания уникальных идентификаторов. - person a_horse_with_no_name; 06.08.2012
comment
@Atais Нет, и если вы этого хотите, вы должны отредактировать свой ответ, чтобы так сказать, потому что это не последовательность базы данных. Похоже, вы пытаетесь получить последовательность без пробелов. - person Craig Ringer; 06.08.2012
comment
@a_horse_with_no_name, я только что переосмыслил идею count(), конечно, это неправильно. Может быть, он должен вернуть последний идентификатор +1 или что-то в этом роде? Для уточнения, пожалуйста, посмотрите на редактирование вопроса. - person Atais; 06.08.2012
comment
@Atais: Нет, не используйте last_id + 1 (это также будет неправильным). Последовательность является наиболее эффективным и масштабируемым решением для создания уникальных идентификаторов. - person a_horse_with_no_name; 06.08.2012
comment
@Atais, это тоже не сработает. Представьте, что произойдет, если две транзакции произойдут одновременно. Вам нужно использовать SEQUENCE в базе данных через SequenceGenerator и принять, что идентификаторы могут отличаться друг от друга, могут быть пробелы в два, три или более идентификаторов. Если вы абсолютно должны не иметь пробелов, см. stackoverflow.com/questions/2183932/, но вы не должны полагаться на это. - person Craig Ringer; 06.08.2012

Пожалуйста, попробуйте использовать GenerationType.TABLE вместо GenerationType.IDENTITY. База данных создаст отдельную таблицу, которая будет использоваться для создания уникальных первичных ключей, а также будет хранить последний использованный идентификационный номер.

person Leszek    schedule 06.08.2012
comment
Только что попробовал, никакого эффекта. Мы создали три первых объекта Java с приложением, затем с запросами я создал еще четыре. Итак, теперь последовательность JPA остановлена/была остановлена ​​на (новом) ID=4, но на самом деле она должна быть (новым) ID=8. Тип генерации таблицы выдал нам ошибку: - person Atais; 06.08.2012
comment
Зачем вам использовать генератор на основе Hibernate, если встроенный из СУБД намного лучше? (Кстати, Hibernate создаст эту таблицу, а не базу данных) - person a_horse_with_no_name; 06.08.2012
comment
Я действительно хочу использовать DMBS, но, похоже, он использует Hibernate. - person Atais; 06.08.2012
comment
GenerationType.TABLE почти во всех отношениях уступает использованию SequenceGenerator с GenerationType.SEQUENCE. Я бы подумал об его использовании только в том случае, если бы мне нужно было иметь сопоставление, которое было бы переносимым для любой базы данных с мертвым мозгом. - person Craig Ringer; 06.08.2012
comment
@CraigRinger, ну, это всего лишь простое приложение, связанное с базой данных, одна БД, одно приложение, не более того. Так что я думаю, что это слишком сложное решение. Я также отредактировал основной вопрос, если вы можете взглянуть на него. - person Atais; 06.08.2012
comment
@Atais Да, я видел это. Если вы хотите, чтобы изменения Hibernate и изменения, сделанные вне Hibernate, были синхронизированы, используйте генератор последовательности, потому что PostgreSQL использует его для столбцов SERIAL, которые вы (надеюсь) используете для сгенерированных вами первичных ключей. - person Craig Ringer; 06.08.2012
comment
Я думаю, что это полезно, только если вы хотите поддерживать несколько баз данных. - person fatihpense; 02.02.2016

Вы также можете сэкономить некоторые усилия, написав сценарий для массового преобразования общего GenerationType.IDENTITY в решение, предложенное выбранным ответом. Приведенный ниже сценарий имеет некоторые небольшие зависимости от того, как отформатирован исходный файл Java, и будет вносить изменения без резервного копирования. Пусть покупатель будет бдителен!

После запуска скрипта:

  1. Найдите и замените import javax.persistence.Table; на import javax.persistence.Table; import javax.persistence.SequenceGenerator;.
  2. Reformat the source code in NetBeans as follows:
    1. Select all the source files to format.
    2. Нажмите Alt+Shift+F
    3. Подтвердите переформатирование.

Сохраните следующий скрипт как update-sequences.sh или аналогичный:

#!/bin/bash

# Change this to the directory name (package name) where the entities reside.
PACKAGE=com/domain/project/entities

# Change this to the path where the Java source files are located.
cd src/main/java

for i in $(find $PACKAGE/*.java -type f); do
  # Only process classes that have an IDENTITY sequence.
  if grep "GenerationType.IDENTITY" $i > /dev/null; then
    # Extract the table name line.
    LINE_TABLE_NAME=$(grep -m 1 @Table $i | awk '{print $4;}')
    # Trim the quotes (if present).
    TABLE_NAME=${LINE_TABLE_NAME//\"}
    # Trim the comma (if present).
    TABLE_NAME=${TABLE_NAME//,}

    # Extract the column name line.
    LINE_COLUMN_NAME=$(grep -m 1 -C1 -A3 @Id $i | tail -1)
    COLUMN_NAME=$(echo $LINE_COLUMN_NAME | awk '{print $4;}')
    COLUMN_NAME=${COLUMN_NAME//\"}
    COLUMN_NAME=${COLUMN_NAME//,}

    # PostgreSQL sequence name.
    SEQUENCE_NAME="${TABLE_NAME}_${COLUMN_NAME}_seq"

    LINE_SEQ_GENERATOR="@SequenceGenerator( name = \"$SEQUENCE_NAME\", sequenceName = \"$SEQUENCE_NAME\", allocationSize = 1 )"
    LINE_GENERATED_VAL="@GeneratedValue( strategy = GenerationType.SEQUENCE, generator = \"$SEQUENCE_NAME\" )"
    LINE_COLUMN="@Column( name = \"$COLUMN_NAME\", updatable = false )\n"

    # These will depend on source code formatting.
    DELIM_BEGIN="@GeneratedValue( strategy = GenerationType.IDENTITY )"
    # @Basic( optional = false ) is also replaced.
    DELIM_ENDED="@Column( name = \"$COLUMN_NAME\" )"

    # Replace these lines...
    #
    # $DELIM_BEGIN
    # $DELIM_ENDED
    #
    # With these lines...
    #
    # $LINE_SEQ_GENERATOR
    # $LINE_GENERATED_VAL
    # $LINE_COLUMN

    sed -i -n "/$DELIM_BEGIN/{:a;N;/$DELIM_ENDED/!ba;N;s/.*\n/$LINE_SEQ_GENERATOR\n$LINE_GENERATED_VAL\n$LINE_COLUMN/};p" $i
  else
    echo "Skipping $i ..."
  fi
done

При создании приложения CRUD с помощью NetBeans атрибуты ID не будут включать редактируемые поля ввода.

person Dave Jarvis    schedule 23.08.2017

Это работает для меня

  1. создайте такую ​​таблицу, используйте SERIAL.
CREATE TABLE webuser(
    idwebuser SERIAL PRIMARY KEY,
    ...
)
  1. добавьте @GeneratedValue(strategy = GenerationType.IDENTITY) в поле id.
@Entity
@Table(name="webuser")
class Webuser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    // ....

}
person hang gao    schedule 26.06.2019
comment
@LF, спасибо за ваше предложение, я изменил ответ. - person hang gao; 27.06.2019