Hibernate JPAシーケンス(非ID)


138

識別子ではない/複合識別子の一部ではない一部の列にDBシーケンスを使用することは可能ですか?

私はjpaプロバイダーとしてhibernateを使用しており、(シーケンスを使用して)生成された値であるいくつかの列を持つテーブルがありますが、それらは識別子の一部ではありません。

私が欲しいのは、シーケンスの列が主キー(の一部)ではないエンティティの新しい値を作成するためにシーケンスを使用することです。

@Entity
@Table(name = "MyTable")
public class MyEntity {

    //...
    @Id //... etc
    public Long getId() {
        return id;
    }

   //note NO @Id here! but this doesn't work...
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "myGen")
    @SequenceGenerator(name = "myGen", sequenceName = "MY_SEQUENCE")
    @Column(name = "SEQ_VAL", unique = false, nullable = false, insertable = true, updatable = true)
    public Long getMySequencedValue(){
      return myVal;
    }

}

次に、これを行うと:

em.persist(new MyEntity());

IDがmySequenceVal生成されますが、プロパティはJPAプロバイダーによっても生成されます。

明確にするために、私はHibernatemySequencedValueプロパティの値を生成させたいと思っています。Hibernateがデータベース生成値を処理できることは知っていますが、プロパティの値を生成するためにHibernate自体以外のトリガーやその他のものを使用したくありません。Hibernateが主キーの値を生成できる場合、なぜそれが単純なプロパティに対して生成できないのですか?

回答:


76

この問題の答えを探して、私はこのリンクを見つけました

Hibernate / JPAがnon-id-propertiesの値を自動的に作成できないようです。@GeneratedValue注釈は、以下の組み合わせのみで使用される@Id自動番号を作成します。

@GeneratedValue注釈は、単にデータベースがこの値自体を生成していることにHibernateに伝えます。

そのフォーラムで提案されている解決策(または回避策)は、次のような、生成されたIDを使用して別のエンティティを作成することです。

@エンティティ
パブリッククラスGeneralSequenceNumber {
  @Id
  @GeneratedValue(...)
  プライベートLong番号。
}

@エンティティ 
パブリッククラスMyEntity {
  @Id ..
  プライベートLong ID。

  @OneToOne(...)
  プライベートGeneralSequnceNumber myVal;
}

@GeneratedValueのJavaドキュメントから:「GeneratedValueアノテーションは、Idアノテーションと組み合わせて、エンティティまたはマップされたスーパークラスの主キープロパティまたはフィールドに適用できます」
Kariem

11
@Column(columnDefinition = "serial")は完全に機能しますが、PostgreSQLでのみ機能することがわかりました。2番目のエンティティは「醜い」オプションなので、私にとってこれは完璧な解決策でした
セルゲイヴェデルニコフ

@SergeyVedernikov 非常に役に立ちました。別の回答として投稿していただけませんか。それは私の問題を非常に簡単かつ効果的に解決しました。
マットボール

@MattBall私はこれを個別の回答として投稿しました:) stackoverflow.com/a/10647933/620858
Sergey Vedernikov

1
@GeneratedValueidではないフィールドで許可する提案を開きました。2.2 java.net/jira/browse/JPA_SPEC-113
Petar Tahchievに

44

私はそれ@Column(columnDefinition="serial")が完璧に機能することを発見しましたが、PostgreSQLのみです。2番目のエンティティは「醜い」オプションなので、私にとってこれは完璧なソリューションでした。


こんにちは、それについての説明が必要です。もっと教えて頂けますか?
Emaborsa

2
@EmaborsaこのcolumnDefinition=ビットは基本的に、Hiberateに列定義を生成せず、代わりに指定したテキストを使用するように指示します。基本的に、列のDDLは文字通り名前+ columnDefinitionになります。この場合(PostgreSQL)mycolumn serialは、テーブル内の有効な列です。
Patrick

7
MySQLに相当するものは@Column(columnDefinition = "integer auto_increment")
Richard Kennard '28

2
この自動で値が生成されますか?このようなフィールド定義でエンティティを永続化しようとしましたが、値が生成されませんでした。列<column>にnull値を
スロー

7
以前@Column(insertable = false, updatable = false, columnDefinition="serial")は、hibernateがnull値を挿入したり、フィールドを更新したりするのを防いでいました。すぐに使用する必要がある場合は、挿入後に生成されたIDを取得するために、データベースを再クエリする必要があります。
Robert Di Paolo

20

私はこれが非常に古い質問であることを知っていますが、それは最初に結果に示され、JPAは質問以来大きく変化しました。

これを行う正しい方法は、@Generatedアノテーションを使用することです。シーケンスを定義し、列のデフォルトをそのシーケンスに設定して、列を次のようにマップできます。

@Generated(GenerationTime.INSERT)
@Column(name = "column_name", insertable = false)

1
この場合も、データベースで値を生成する必要がありますが、これは実際には質問の答えにはなりません。12cより前のOracleデータベースの場合でも、値を生成するにはデータベーストリガーを作成する必要があります。
バーニー2014年

9
また、これはJPAではなくHibernateアノテーションです。
caarlos0 2014

14

Hibernateは間違いなくこれをサポートしています。ドキュメントから:

「生成されたプロパティは、データベースによって生成された値を持つプロパティです。通常、Hibernateアプリケーションは、データベースが値を生成していたプロパティを含むオブジェクトを更新する必要がありました。ただし、プロパティを生成済みとしてマークすると、アプリケーションはこの責任をHibernateに委任できます。基本的に、Hibernateは、生成されたプロパティを定義したエンティティに対してSQL INSERTまたはUPDATEを発行するたびに、生成された値を取得するための選択をすぐに発行します。

挿入時にのみ生成されるプロパティの場合、プロパティマッピング(.hbm.xml)は次のようになります。

<property name="foo" generated="insert"/>

挿入時に生成されたプロパティの場合、プロパティマッピング(.hbm.xml)は次のようになります。

<property name="foo" generated="always"/>

残念ながら、JPAがわからないので、この機能がJPAを介して公開されているかどうかはわかりません(おそらくそうではないと思われます)。

または、挿入と更新からプロパティを除外して、「手動」でsession.refresh(obj)を呼び出すこともできます。挿入/更新してデータベースから生成された値をロードした後。

これは、挿入および更新ステートメントで使用されないようにプロパティを除外する方法です。

<property name="foo" update="false" insert="false"/>

繰り返しますが、JPAがこれらのHibernate機能を公開しているかどうかはわかりませんが、Hibernateはそれらをサポートしています。


1
@Generatedアノテーションは、上記のXML構成に対応しています。詳細については、休止状態のドキュメントのこのセクションを参照してください。
エリック

8

フォローアップとして、私がそれを機能させる方法を次に示します。

@Override public Long getNextExternalId() {
    BigDecimal seq =
        (BigDecimal)((List)em.createNativeQuery("select col_msd_external_id_seq.nextval from dual").getResultList()).get(0);
    return seq.longValue();
}

Hibernate 4.2.19とoracleを使用したバリアント: SQLQuery sqlQuery = getSession().createSQLQuery("select NAMED_SEQ.nextval seq from dual"); sqlQuery.addScalar("seq", LongType.INSTANCE); return (Long) sqlQuery.uniqueResult();
Aaron

6

@PrePersistアノテーションを使用してHibernateでUUID(またはシーケンス)の生成を修正しました。

@PrePersist
public void initializeUUID() {
    if (uuid == null) {
        uuid = UUID.randomUUID().toString();
    }
}

5

これは古いスレッドですが、私の解決策を共有し、うまくいけばこれについていくつかのフィードバックを得たいと思います。一部のJUnitテストケースでは、ローカルデータベースでのみこのソリューションをテストしたことに注意してください。したがって、これは今のところ生産的な機能ではありません。

プロパティを持たないSequenceというカスタムアノテーションを導入することで、この問題を解決しました。これは、インクリメントされたシーケンスから値を割り当てる必要があるフィールドの単なるマーカーです。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sequence
{
}

この注釈を使用して、エンティティにマークを付けました。

public class Area extends BaseEntity implements ClientAware, IssuerAware
{
    @Column(name = "areaNumber", updatable = false)
    @Sequence
    private Integer areaNumber;
....
}

データベースに依存しないようにするために、シーケンスの現在の値と増分サイズを保持するSequenceNumberというエンティティを導入しました。一意のキーとしてclassNameを選択したので、各エンティティクラスは独自のシーケンスを取得します。

@Entity
@Table(name = "SequenceNumber", uniqueConstraints = { @UniqueConstraint(columnNames = { "className" }) })
public class SequenceNumber
{
    @Id
    @Column(name = "className", updatable = false)
    private String className;

    @Column(name = "nextValue")
    private Integer nextValue = 1;

    @Column(name = "incrementValue")
    private Integer incrementValue = 10;

    ... some getters and setters ....
}

最後のステップで最も難しいのは、シーケンス番号の割り当てを処理するPreInsertListenerです。豆の容器として春を使用したことに注意してください。

@Component
public class SequenceListener implements PreInsertEventListener
{
    private static final long serialVersionUID = 7946581162328559098L;
    private final static Logger log = Logger.getLogger(SequenceListener.class);

    @Autowired
    private SessionFactoryImplementor sessionFactoryImpl;

    private final Map<String, CacheEntry> cache = new HashMap<>();

    @PostConstruct
    public void selfRegister()
    {
        // As you might expect, an EventListenerRegistry is the place with which event listeners are registered
        // It is a service so we look it up using the service registry
        final EventListenerRegistry eventListenerRegistry = sessionFactoryImpl.getServiceRegistry().getService(EventListenerRegistry.class);

        // add the listener to the end of the listener chain
        eventListenerRegistry.appendListeners(EventType.PRE_INSERT, this);
    }

    @Override
    public boolean onPreInsert(PreInsertEvent p_event)
    {
        updateSequenceValue(p_event.getEntity(), p_event.getState(), p_event.getPersister().getPropertyNames());

        return false;
    }

    private void updateSequenceValue(Object p_entity, Object[] p_state, String[] p_propertyNames)
    {
        try
        {
            List<Field> fields = ReflectUtil.getFields(p_entity.getClass(), null, Sequence.class);

            if (!fields.isEmpty())
            {
                if (log.isDebugEnabled())
                {
                    log.debug("Intercepted custom sequence entity.");
                }

                for (Field field : fields)
                {
                    Integer value = getSequenceNumber(p_entity.getClass().getName());

                    field.setAccessible(true);
                    field.set(p_entity, value);
                    setPropertyState(p_state, p_propertyNames, field.getName(), value);

                    if (log.isDebugEnabled())
                    {
                        LogMF.debug(log, "Set {0} property to {1}.", new Object[] { field, value });
                    }
                }
            }
        }
        catch (Exception e)
        {
            log.error("Failed to set sequence property.", e);
        }
    }

    private Integer getSequenceNumber(String p_className)
    {
        synchronized (cache)
        {
            CacheEntry current = cache.get(p_className);

            // not in cache yet => load from database
            if ((current == null) || current.isEmpty())
            {
                boolean insert = false;
                StatelessSession session = sessionFactoryImpl.openStatelessSession();
                session.beginTransaction();

                SequenceNumber sequenceNumber = (SequenceNumber) session.get(SequenceNumber.class, p_className);

                // not in database yet => create new sequence
                if (sequenceNumber == null)
                {
                    sequenceNumber = new SequenceNumber();
                    sequenceNumber.setClassName(p_className);
                    insert = true;
                }

                current = new CacheEntry(sequenceNumber.getNextValue() + sequenceNumber.getIncrementValue(), sequenceNumber.getNextValue());
                cache.put(p_className, current);
                sequenceNumber.setNextValue(sequenceNumber.getNextValue() + sequenceNumber.getIncrementValue());

                if (insert)
                {
                    session.insert(sequenceNumber);
                }
                else
                {
                    session.update(sequenceNumber);
                }
                session.getTransaction().commit();
                session.close();
            }

            return current.next();
        }
    }

    private void setPropertyState(Object[] propertyStates, String[] propertyNames, String propertyName, Object propertyState)
    {
        for (int i = 0; i < propertyNames.length; i++)
        {
            if (propertyName.equals(propertyNames[i]))
            {
                propertyStates[i] = propertyState;
                return;
            }
        }
    }

    private static class CacheEntry
    {
        private int current;
        private final int limit;

        public CacheEntry(final int p_limit, final int p_current)
        {
            current = p_current;
            limit = p_limit;
        }

        public Integer next()
        {
            return current++;
        }

        public boolean isEmpty()
        {
            return current >= limit;
        }
    }
}

上記のコードからわかるように、リスナーはエンティティクラスごとに1つのSequenceNumberインスタンスを使用し、SequenceNumberエンティティのincrementValueで定義された2つのシーケンス番号を予約しています。シーケンス番号が不足すると、ターゲットクラスのSequenceNumberエンティティを読み込み、次の呼び出しのためにincrementValue値を予約します。このようにして、シーケンス値が必要になるたびにデータベースを照会する必要がありません。次のシーケンス番号のセットを予約するために開かれているStatelessSessionに注意してください。EntityPersisterでConcurrentModificationExceptionが発生するため、ターゲットエンティティが現在永続化されている同じセッションを使用することはできません。

これが誰かを助けることを願っています。


5

postgresql
を使用していて、Spring Boot 1.5.6を使用している場合

@Column(columnDefinition = "serial")
@Generated(GenerationTime.INSERT)
private Integer orderID;

1
それも私にとってはうまくいきました、私はスプリングブート2.1.6.RELEASE、Hibernate 5.3.10.Finalを使用しています。すでに指摘したことに加えて、セキュリティを作成しseq_order、フィールドから参照を作成する必要があり ましたnextval('seq_order'::regclass)
OJVM

3

私はあなたのような同じ状況で実行しますが、JPAで非IDプロパティを生成することが基本的に可能かどうかについても、深刻な答えは見つかりませんでした。

私の解決策は、ネイティブのJPAクエリでシーケンスを呼び出して、永続化する前に手動でプロパティを設定することです。

これは満足できるものではありませんが、当面の回避策として機能します。

マリオ


2

セッション9.1.9のJPA仕様のGeneratedValueアノテーションでこの特定のノートを見つけました。したがって、少なくともJPAを使用するだけでは、非主キー値の値を自動生成することはできないと思います。


1

スレッドが古いようです、ここに私のソリューションを追加したかっただけです(AspectJ-春のAOPの使用)。

解決策は@InjectSequenceValue、次のようにカスタム注釈を作成することです。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectSequenceValue {
    String sequencename();
}

エンティティの任意のフィールドに注釈を付けることができるようになり、基になるフィールド(Long / Integer)の値がシーケンスのnextvalueを使用して実行時に挿入されます。

このように注釈を付けます。

//serialNumber will be injected dynamically, with the next value of the serialnum_sequence.
 @InjectSequenceValue(sequencename = "serialnum_sequence") 
  Long serialNumber;

ここまでで、シーケンス値を挿入する必要があるフィールドをマークしました。マークしたフィールドにシーケンス値を挿入する方法を見ていきます。これは、AspectJでポイントカットを作成することによって行われます。

save/persistメソッドが実行される直前にインジェクションをトリガーします。これは以下のクラスで行われます。

@Aspect
@Configuration
public class AspectDefinition {

    @Autowired
    JdbcTemplate jdbcTemplate;


    //@Before("execution(* org.hibernate.session.save(..))") Use this for Hibernate.(also include session.save())
    @Before("execution(* org.springframework.data.repository.CrudRepository.save(..))") //This is for JPA.
    public void generateSequence(JoinPoint joinPoint){

        Object [] aragumentList=joinPoint.getArgs(); //Getting all arguments of the save
        for (Object arg :aragumentList ) {
            if (arg.getClass().isAnnotationPresent(Entity.class)){ // getting the Entity class

                Field[] fields = arg.getClass().getDeclaredFields();
                for (Field field : fields) {
                    if (field.isAnnotationPresent(InjectSequenceValue.class)) { //getting annotated fields

                        field.setAccessible(true); 
                        try {
                            if (field.get(arg) == null){ // Setting the next value
                                String sequenceName=field.getAnnotation(InjectSequenceValue.class).sequencename();
                                long nextval=getNextValue(sequenceName);
                                System.out.println("Next value :"+nextval); //TODO remove sout.
                                field.set(arg, nextval);
                            }

                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }

        }
    }

    /**
     * This method fetches the next value from sequence
     * @param sequence
     * @return
     */

    public long getNextValue(String sequence){
        long sequenceNextVal=0L;

        SqlRowSet sqlRowSet= jdbcTemplate.queryForRowSet("SELECT "+sequence+".NEXTVAL as value FROM DUAL");
        while (sqlRowSet.next()){
            sequenceNextVal=sqlRowSet.getLong("value");

        }
        return  sequenceNextVal;
    }
}

これで、以下のようにエンティティに注釈を付けることができます。

@Entity
@Table(name = "T_USER")
public class UserEntity {

    @Id
    @SequenceGenerator(sequenceName = "userid_sequence",name = "this_seq")
    @GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "this_seq")
    Long id;
    String userName;
    String password;

    @InjectSequenceValue(sequencename = "serialnum_sequence") // this will be injected at the time of saving.
    Long serialNumber;

    String name;
}

0

「トリガーや、Hibernate自体以外のものを使用して、プロパティの値を生成したくない」

その場合、必要な値を生成するUserTypeの実装を作成し、mySequenceValプロパティの永続化にそのUserTypeを使用するようにメタデータを構成するのはどうでしょうか。


0

これはシーケンスを使用することと同じではありません。シーケンスを使用する場合、何も挿入または更新しません。あなたは単に次のシーケンス値を取得しています。hibernateはそれをサポートしていないようです。


0

UNIQUEIDENTIFIERタイプの列があり、挿入時にデフォルトの生成が必要であるが、列がPKではない場合

@Generated(GenerationTime.INSERT)
@Column(nullable = false , columnDefinition="UNIQUEIDENTIFIER")
private String uuidValue;

dbでは

CREATE TABLE operation.Table1
(
    Id         INT IDENTITY (1,1)               NOT NULL,
    UuidValue  UNIQUEIDENTIFIER DEFAULT NEWID() NOT NULL)

この場合、必要な値のジェネレータを定義しません(自動的にのおかげになります columnDefinition="UNIQUEIDENTIFIER")。他の列タイプでも試すことができるのと同じ


0

Springアプリケーションで@PostConstructとJdbcTemplateを使用して、MySqlデータベースでこれに対する回避策を見つけました。他のデータベースでも実行できる場合がありますが、ここで紹介するユースケースは、auto_incrementを使用するため、MySqlでの私の経験に基づいています。

最初に、@ ColumnアノテーションのColumnDefinitionプロパティを使用して列をauto_incrementとして定義しようとしましたが、自動増分にするために列をキーにする必要があるため機能しませんでしたが、明らかに列は次のように定義されません。定義されるまでのインデックス。デッドロックを引き起こします。

ここで、auto_increment定義なしで列を作成し、その後に追加するというアイデアを思いつきまし、データベースの作成ました。これは、@ PostConstructアノテーションを使用して可能です。これにより、JdbcTemplateのupdateメソッドと組み合わせて、アプリケーションがBeanを初期化した直後にメソッドが呼び出されます。

コードは次のとおりです。

私のエンティティ:

@Entity
@Table(name = "MyTable", indexes = { @Index(name = "my_index", columnList = "mySequencedValue") })
public class MyEntity {
    //...
    @Column(columnDefinition = "integer unsigned", nullable = false, updatable = false, insertable = false)
    private Long mySequencedValue;
    //...
}

PostConstructComponentクラスで:

@Component
public class PostConstructComponent {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void makeMyEntityMySequencedValueAutoIncremental() {
        jdbcTemplate.update("alter table MyTable modify mySequencedValue int unsigned auto_increment");
    }
}

0

@Morten Bergの承認されたソリューションの隣に代替手段を提供したいと思います。

このアプローチでは、実際の目的のNumberタイプLong(私のユースケースでは)ではなく、フィールドをフィールドに定義できますGeneralSequenceNumber。これは、JSON(デ)シリアライゼーションなどに役立ちます。

欠点は、データベースのオーバーヘッドが少し増えることです。


最初に、型のActualEntity自動インクリメントgeneratedが必要なinが必要ですLong

// ...
@Entity
public class ActualEntity {

    @Id 
    // ...
    Long id;

    @Column(unique = true, updatable = false, nullable = false)
    Long generated;

    // ...

}

次に、ヘルパーエンティティが必要Generatedです。パッケージActualEntityの実装の詳細を保持するために、の横にpackage-privateを配置しました。

@Entity
class Generated {

    @Id
    @GeneratedValue(strategy = SEQUENCE, generator = "seq")
    @SequenceGenerator(name = "seq", initialValue = 1, allocationSize = 1)
    Long id;

}

最後に、を保存する直前にフックする場所が必要ActualEntityです。そこで、Generatedインスタンスを作成して永続化します。これにより、生成されたidタイプのデータベースシーケンスが提供されますLong。この値を使用して、ActualEntity.generatedます。

私の使用例では@RepositoryEventHandlerActualEntity取得が永続化される直前に呼び出されるSpring Data RESTを使用してこれを実装しました。それは原則を示すべきです:

@Component
@RepositoryEventHandler
public class ActualEntityHandler {

    @Autowired
    EntityManager entityManager;

    @Transactional
    @HandleBeforeCreate
    public void generate(ActualEntity entity) {
        Generated generated = new Generated();

        entityManager.persist(generated);
        entity.setGlobalId(generated.getId());
        entityManager.remove(generated);
    }

}

実際のアプリケーションではテストしていませんので、注意してお楽しみください。


-1

私はあなたのような状況にあり(@Idフィールド以外のJPA / Hibernateシーケンス)、私は挿入時に一意のシーケンス番号を追加するトリガーを私のdbスキーマに作成してしまいました。JPA / Hibernateで動作することはありません


-1

時間を費やした後、これは私が問題をうまく解決するのに役立ちました:

Oracle 12cの場合:

ID NUMBER GENERATED as IDENTITY

H2の場合:

ID BIGINT GENERATED as auto_increment

また作る:

@Column(insertable = false)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.