web-dev-qa-db-ja.com

Hibernate JPAシーケンス(非ID)

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

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

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

@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が主キーの値を生成できる場合、なぜ単純なプロパティに対して生成できないのですか?

120
Miguel Ping

この問題への答えを探して、私はつまずいた このリンク

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

@GeneratedValueアノテーションは、データベースがこの値自体を生成していることをHibernateに伝えるだけです。

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

 @ Entity 
 public class GeneralSequenceNumber {
 @Id 
 @GeneratedValue(...)
 private Long number; 
} 
 
 @ Entity 
 public class MyEntity {
 @Id .. 
 private Long id; 
 
 @ OneToOne(...)
 private GeneralSequnceNumber myVal; 
} 
65
Morten Berg

@Column(columnDefinition="serial")は完璧に動作しますが、PostgreSQLでのみ動作することがわかりました。私にとって、これは完璧なソリューションでした。なぜなら、2番目のエンティティは「ugい」オプションだからです。

34

これは非常に古い質問ですが、最初に結果に基づいて示され、jpaは質問以来大きく変わりました。

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

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

Hibernateはこれを確実にサポートします。ドキュメントから:

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

挿入時にのみ生成されるプロパティの場合、プロパティマッピング(.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はそれらをサポートしています。

14
alasdairg

フォローアップとして、私はそれをどのように機能させたのですか?

@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();
}
7
Paul

@PrePersist注釈を使用して、HibernateでUUID(またはシーケンス)の生成を修正しました。

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

これは古いスレッドですが、解決策を共有し、うまくいけばこれに関するフィードバックを取得したいと思います。 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です。 Beanコンテナとしてスプリングを使用したことに注意してください。

@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が発生するため、ターゲットエンティティが現在永続化されている同じセッションを使用することはできません。

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

4
Sebastian Götz

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

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

これは満足のいくものではありませんが、現時点では回避策として機能します。

マリオ

3
Mario

Postgresqlを使用している場合
そして、私はスプリングブート1.5.6で使用しています

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

スレッドは古いように見えるので、ここにソリューションを追加したかっただけです(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;
}
1

JPA仕様のセッション9.1.9 GeneratedValueアノテーションでこの特定のメモを見つけました:「[43]ポータブルアプリケーションは、他の永続フィールドまたはプロパティでGeneratedValueアノテーションを使用しないでください。」したがって、少なくともJPAを使用して、主キー以外の値の値を自動生成することはできないと思います。

1
Gustavo Orair

これは、シーケンスの使用とは異なります。シーケンスを使用する場合、何も挿入または更新していません。次のシーケンス値を取得するだけです。休止状態ではサポートされていないようです。

0
kammy

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

その場合、必要な値を生成するUserTypeの実装を作成し、mySequenceValプロパティの永続性にそのUserTypeを使用するようにメタデータを構成する方法はありますか?

0
alasdairg