بررسی Sequence pre allocation در JPA (پیاده‌سازی‌های Hibernate و EclipseLink)

در JPA برای تولید مقدار خودکار (GeneratedValue) مورد استفاده در فیلد‌های Identifier استراتژی‌های مختلفی وجود دارد. در اینجا قصد داریم استراتژی Sequence را در JPA (پیاده‌سازی‌های EclipseLink و Hibernate) و با استفاده از دیتابیس Oracle که دارای مکانیزم تولید Sequence می‌باشد بررسی کنیم.

فرض کنید قصد داریم نام افراد را در جدول PERSON در دیتابیس Oracle ذخیره کنیم:

create table PERSON
(
    ID   NUMBER(19) not null
        primary key,
    NAME VARCHAR2(255 char)
)

همانطور که مشاهده می‌شود Primary Key جدول Person فیلد ID از نوع عددی می‌باشد. و قصد داریم فیلد ID را توسط GeneratedValue و با استفاده از Sequence مقداردهی کنیم.

همچنین Entity بنام Person که دارای دو فیلد Id و name می‌باشد نیز وجود دارد:

@Table
@Entity
public class Person implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = 
     &quotpersonSequenceGenerator&quot)
     @SequenceGenerator(name = &quotpersonSequenceGenerator&quot, sequenceName = 
     &quotSEQ_PERSON&quot, allocationSize = 50)
     private Long id;

   private String name;

// Getters and Setters...
  }

در زمان عملیات Insert فیلد Id از SequenceGenerator بنام personSequenceGenerator مقدار می‌گیرد که personSequenceGenerator نیز خود از SEQ_PERSON که یک Sequence تعریف شده در دیتابیس Oracle می‌باشد استفاده می‌کند:

ایجاد Sequence (SEQ_PERSON) در دیتابیس Oracle:

create sequence SEQ_PERSON
   start with 50
   increment by 50
   maxvalue 1000000

نکته حائز اهمیت این است که در این مثال مقدار allocationSize مربوط به Sequence تعریف شده در Entity برابر 50 (مقدار پیش فرض در JPA) و مقدار افزایش Sequence تولید شده (increment by) نیز برابر allocationSize یعنی 50 می‌باشد.

علت این امر نحوه تخصیص Sequence توسط JPA و بهینه سازی آن با استفاده از pre-allocation در زمان Insert رکورد در جدول می‌باشد.

در واقع در حالت ساده (بدون استفاده از JPA و یا عدم استفاده از بهینه‌سازی تخصیص Sequence در JPA) برای دسترسی به مقدار Sequence و استفاده از آن نیازمند دریافت مقدار جدید Sequence هستیم. یعنی به ازای هر بار Insert باید مقدار جدید Sequence را دریافت کنیم:

select SEQ_PERSON.nextval from dual;

اما چنانچه با توجه به تعدد عملیات Insert در لحظه، بخواهیم به حالت بهینه‌ای این کار را انجام دهیم نیازمند مکانیزمی هستیم که علاوه بر عدم اختصاص Sequence تکراری، تعداد مراجعات به دیتابیس را برای دریافت مقدار جدید Sequence کاهش دهد. یعنی به تعبیری باید یک Cache در سمت Application از مقادیر Sequence داشته باشیم تا در زمان نیاز به دریافت Sequence جدید ابتدا به Cache موجود در Application مراجعه کنیم.

خوشبختانه این موضوع توسط JPA پوشش داده شده است و پیاده‌سازی‌های مختلف نظیر EclipseLink و Hibernate نیز از آن پشتیبانی می‌کنند.

پس بهتر است ببینیم EclipseLink و Hibernate در عمل چطور این بهینه سازی را انجام می‌دهند.

فریم‌ورک‌های EclipseLink و Hibernate (در حالت پیش‌فرض)، با استفاده از مکانیزم pre-allocation تعداد مراجعه به دیتابیس را برای دریافت مقدار جدید sequence کاهش می‌دهند. این مقدار در پارامتر allocationSize در تعریف Sequence در ORM قابل تغییر است. (پیش فرض: 50)

اگر مستندات مربوط به allocation-size را بررسی کنیم این تعریف را خواهیم دید:

allocationSize

public abstract int allocationSize

(Optional) The amount to increment by when allocating sequence numbers from the sequence.

Default:50

توضیحات فوق شاید کمی گمراه کننده باشد، اما بصورت کلی اتفاقی که می‌افتد این است که ORM ابتدا یک بازه معتبر از مقادیر Sequence مورد نظر را ایجاد و در زمان Insert از این بازه استفاده می‌کند و پس از اتمام مقادیر مجددا بازه جدیدی از Sequence ها را ایجاد و مورد استفاده قرار می‌دهد و بدین ترتیب در مراجعه به دیتابیس برای دریافت Sequence صرفه جویی قابل توجهی انجام می دهد.

همانطور که در مثال بالا دیدیم، مقدار allocation-size در Entity و increment by در Sequence یکسان و برابر مقدار 50 می باشد.

بدین‌ ترتیب EclipseLink و Hibernate، در زمان insert ابتدا یک بار برای دریافت مقدار جدید Sequence به دیتابیس مراجعه می‌کنند:

select SEQ_PERSON.nextval from dual

که این query در بار اول مقدار 50 را برمی‌گرداند.

سپس با استفاده از فرمول زیر مقادیر sequence را تعیین می‌کنند (Cache از مقادیر Sequence ایجاد می‌کند):

nextval = SEQ.nextval = 50 
First Sequence = nextval - allocation_size + 1 = 50 - 50 + 1 = 1
Last Sequence = nextval = 50

بر این ‌اساس مقدار شروع Sequence تخصیصی به فیلد id با محاسبه انجام شده (50-50+1) برابر عدد 1 می‌شود.

و برای بهبود کارایی و عدم مراجعه به دیتابیس در insert های بعدی از مقدار شروع (First Sequence) تا مقدار پایانی (Last Sequence)، مقدار Sequence توسط ORM (بدون مراجعه به دیتابیس) یک به یک افزایش و مورد استفاده قرار می‌گیرد.

مثلا در EclipseLink:

<sequencing preallocation for SEQ_PERSON: objects: 50 , first: 1, last: 50>

زمانی که مقدار Sequence به عدد 50 رسید، ORM مجدد به دیتابیس مراجعه می‌کند تا nextvalue را دریافت کند:

select SEQ_PERSON.nextval from dual

که این بار این query مقدار 100 را بر می‌گرداند.

حال مجدد همین روال تکرار می‌شود:

nextval = 100
First Sequence = 100 - 50 + 1 = 51
Last Sequence = 100

در واقع ORM در زمان insert، تا زمانی که به مقدار Last Sequence (در این مرحله عدد 100) نرسیده باشد به دیتابیس مراجعه نخواهد کرد.

یعنی به ازای هر 50 عملیات insert تنها یک مراجعه به دیتابیس برای دریافت nextvalue از sequence مورد نظر می‌شود که بهبود قابل توجهی در کارایی خواهد بود!

و نکته دیگر اینکه چنانچه Application، در حین insert نمودن رکوردها و پیش از رسیدن به مقدار Last Sequence به هر علتی Restart شود، به دلیل از بین رفتن مقدار Last Sequence، مجددا First Sequence و Last Sequence را محاسبه و استفاده می‌کند.

که این امر طبیعتا فاصله ای را در مقادیر sequence مورد استفاده ایجاد می‌کند:

Application Starts Running:
select SEQ_PERSON.nextval from dual
nextval = 50
First Sequence = 50 - 50 + 1 = 1
Last Sequence= nextval = 50
--------------------------------------------------------
insert into person (id, name) values (1, &quotAli&quot)
...
...
...
...
insert into person (id, name) values (23, &quotRamin&quot)
(Next Value for id: 24)
--------------------------------------------------------
Application Restarts:
select SEQ_PERSON.nextval from dual
nextval = 100
First Sequence = 100 - 50 + 1 = 51
Last Sequence = nextval  = 100
insert into person (id, name) values (51, &quotReza&quot)
....

در آخر باید این نکته را هم ذکر کنیم که JPA بصورت کلی مکانیزم بهینه‌سازی تخصیص Sequence را تعریف کرده و نحوه اجرای آن را به پیاده‌سازی‌ها واگذار کرده که ما در اینجا روش پیش فرض مورد استفاده در پیاده‌سازی‌های EclipseLink و Hibernate را بررسی کردیم.

در Hibernate به این روش Pooled Optimizer گفته می‌شود. برای اطلاعات بیشتر و سایر روش های مورد استفاده در Hibernate می توانید مقالات زیر را مطالعه کنید: