JPAでシーケンスを使ってIDを自動採番する場合の注意点


Oracle シーケンスのデフォルトはNOORDERである罠 の続きです。

JPAでEntityのIDを自動採番するとき、DBMSがOracleの場合はシーケンスを使うことになります。シーケンスを使う場合は、@GeneratedValuestrategyプロパティにGenerationType.SEQUENCEを指定します。たぶんGenerationType.AUTOでもシーケンスになります。(GenerationType.AUTO を指定するとはDBMSによって適切な採番方法が選択されます)

1
2
3
4
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name="user_id")
private String userId;

SequenceGeneratorは、デフォルトではシステムでひとつのシーケンス(名前はhibernate_sequence)を共有で使用します。該当テーブル専用のシーケンスを使う場合は@SequenceGeneratorsequenceNameプロパティで指定することができます。以下の例ではuser_id_seqがシーケンスオブジェクト名です。

1
2
3
4
5
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_id_generator")
@SequenceGenerator(name = "user_id_generator", sequenceName = "user_id_seq")
@Column(name="user_id")
private String userId;

SequenceGeneratorは採番都度シーケンスにアクセスして採番するのではなく、あらかじめ一定の採番枠を確保し、その採番枠内の増分であればDBアクセスを発生させないようにしています。

この採番枠は@SequenceGeneratorallocationSizeというプロパティで指定でき、デフォルトは50となっています。

また、user_id_seq.nextvalした時にallocationSize分増加している必要があるので、allocationSizeとシーケンスのincrement byは一致している必要があります。(事実、Spring Bootにおいてspring.jpa.hibernate.ddl-auto=updateとしてシーケンスオブジェクトを自動作成するとincrement by 50のシーケンスが作成されます。)

しかし、これには一つ問題があり、同じテーブルのIDを採番するSequenceGeneratorインスタンスが複数存在すると(つまり複数のアプリケーションサーバが存在するLB構成だと)、インスタンスAは1~50、インスタンスBは51~100の枠内でそれぞれ採番することになるので、テーブルのIDは1, 51, 2, 52...のように番号の戻りが発生してしまいます。これは Oracle シーケンスのデフォルトはNOORDERである罠 と同じ問題です。

このため、IDに対して順序を期待する場合はallocationSize = 1を指定して都度SEQUENCEから採番させる必要があります。

1
2
3
4
5
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_id_generator")
@SequenceGenerator(name = "user_id_generator", sequenceName = "user_id_seq", allocationSize = 1)
@Column(name="user_id")
private String userId;

そんな感じです。