مروری اجمالی به آنچه که هایبرنیت می‌کند - قسمت دوم

در قسمت گذشته خلاصه‌ای از هایبرنیت خدمتتان ارائه دادم، و در نهایت یک آبجکت را در پایگاه‌داده به واسطه‌ی هایبرنیت Persist کردیم. به گمانم قسمت اول بسیار جذاب بود. امیدوارم آنطور که من انتظار داشتم مقاله‌ای کاربردی بوده باشه، اگر پیشنهادی یا انتقادی داشتید با من درمیان بگذارید. این اولین باری است که مطلبی فنی می‌نویسم و قطعا بی‌ایراد نخواهد بود.

متاسفانه وقتی قسمت اول نزدیک به ۴۰۰۰ هزار واژه شد (که بیشتر بخاطر وجود سورس کدها هم بود) ویرایشگر متن بشدت کند شده بود، به همین خاطر بالاجبار مطالب را شکستم و در چند قسمت ارائه خواهم داد.

در این قسمت به مباحث جدیدی خواهیم پرداخت.

لینک قسمت‌های مرتبط

قسمت اول


فهرست مطالب

  • ویژگی‌های هایبرنیت
  • راه‌اندازی هایبرنیت (بخش ۲)
  • توضیح Annotationهای پایه‌ای


ویژگی‌های هایبرنیت

قسمت پیش به خیلی از ویژگی‌های هایبرنیت اشاره کردیم. به گمانم پس از ملاحظه‌ی یک مثال عملی وقت آن است که کمی مفاهیم و واقعیت‌ها رو مرور کنیم.

با هم مرور کردیم که هایبرنیت اصلی‌ترین امکانش، ORM یا همان نگاشت (Mapping) است. و با هم دیدیم چطور یک آبجکت Student به رکوردی در جدول student تبدیل شد. در آینده این نگاشت را بسیار مفصل‌تر بررسی خواهیم کرد. از قابلیت‌های مهم دیگر هایبرنیت مستقل از نوع دیتابیس بودن است. ما در هنگام کار با هایبرنیت به این توجه نمی‌کنیم که دیتابیس‌ MySQL است یا Oracle! این امکان فوق‌العاده‌ای است که هایبرنیت به ما می‌دهد. اما غیر از این‌ها دیگر چه؟

هایبرنیت امکان Auto DDL دارد. زبان SQL را بیاد میاورید؟ زبان SQL از سه بخش عمده تشکیل شده است: DDL، DML و DCL. دستورات DDL مربوط به ساخت و حذف Schema می‌شوند. بیان ساده‌تر دستورات DDL مربوط به ساخت جداول و اعمال مشابه مربوط می‌شود.

قسمت قبل را بیاد بیاورید، آیا ما برای اینکه جدول student را بسازیم کاری کردیم؟ جز این که مشخص کردیم این کلاس (Student) یک Entity است. با همین مشخصه، هایبرنیت عملیات مربوط به ساخت و تغییر یا بروز کردن آن را انجام می‌دهد. پس ساخت جداول به عهده‌ی هایبرنیت است.

سوال. چه زمانی هایبرنیت جدول را می‌سازد و چه زمانی آپدیت می‌کند؟

پاسخ. امیدوارم بودم تا رسیدن به بخش تنظیمات این سوال را نمی‌پرسیدید، چرا که در آنجا مشخص می‌کنیم که جداول create بشوند یا update یا حتا validate و... . اجازه دهید در جای مناسب خودش مفصل توضیح خواهم داد.

هایبرنیت امکان Cache کردن را دارد. هایبرنیت کش کردن را در سه سطح انجام می‌دهد. در این خصوص صحبت خواهیم کرد.

هایبرنیت امکانات بسیار خوب دیگری نظیر primary key generation، exception free و... را می‌دهد. جدا از آن‌ها یک زبان مستقل بنام HQL (Hibernate Query Language) را عرضه کرده و همچنین API های مناسبی برای Dynamic Queries. در خلل مثال‌ها سعی‌ خواهم کرد به آن‌ها بپردازم.


راه‌اندازی هایبرنیت (بخش ۲)

از اینجا به بعد مثال قسمت پیشین را بسط خواهیم داد و نکات مبهم آن را برطرف خواهیم کرد. در قسمت گذشته بارها توضیح موردی را به آینده موکول کردیم. امروز دقیقا وقت همان است که پاسخ آن‌ها را بدهیم.

فایل کانفیگ را بیاد بیاورید (در گام۲ قسمت اول)، یکی از Propertyهای آن بنام hbm2ddl.auto بود. مقدار این خصوصیت را ما برابر با create گذاشتیم. می‌خواهیم اکنون به مقدارهایی بپردازیم که می‌توانیم به آن خصیصه اختصاص دهیم.

  • مقدار create: وقتی برنامه اجرا می‌شود، و فایل کانفیگ خوانده می‌شود، اگر مقدار hbm2ddl.auto برابر با create باشد، فریمورک hibernate تمام جداول موجود در دیتابیس را (درصورت وجود) پاک می‌کند و دوباره ایجاد می‌کند. توجه داشته باشید که تمام اطلاعات موجود در دیتابیس ازبین خواهد رفت (drop می‌شود)، و دوباره جداول ساخته خواهد شد.

بنابراین چنانچه دیتایی در جداول خودتان دارید باید نسبت به استفاده از این مقدار هوشیار باشید. چون سطرهای جداول دوباره برنخواهند گشت، صرفا جدول را دوباره خواهد ساخت.

پس در واقع مقدار create دو کار را انجام می‌دهد:

  1. جداول موجود را پاک می‌کند.
  2. جداول جدیدی را ایجاد می‌کند.

حالا می‌دانید که اگر قصد آموزش و آزمایش ندارید، استفاده از این مقدار خطرناک است (خطر از دست دادن دیتا). معمولا تنها زمانی از این مقدار استفاده می‌کنند که می‌خواهند schemaی جداول را ایجاد کنند. ولی هنگامی که دیتایی درون دیتابیس وارد شد دیگر این مقدار را استفاده نخواهند کرد. چه مقداری را استفاده می‌کنند؟ همراه باشید!

  • مقدار update: شما می‌توانید بجای استفاده از create مقدار update را قرار دهید. این مقدار در صورت اجرای مجدد دیگر جداول موجود را پاک نخواهد کرد. رفتاری روشنفکرانه دارد. بطور دقیق، هایبرنیت وقتی با این مقدار تنظیم شده باشد ملاحظه می‌کند، اگر جدولی در دیتابیس وجود ندارد، ولی در جاوا Entity برای آن ساخته شده است، آن جدول را ایجاد می‌کند.
    ما برنامه را همواره توسعه می‌دهیم و ممکن است Entityهای زیادی را اضافه کنیم که بخواهیم درهنگام اجرا، جداول مربوطه‌ی آنها ایجاد شود و مشکلی هم برای جداول از پیش تعیین شده (برای Entityهای قدیمی) پیش نیاید. مقدار update برای ما چنین می‌کند.

یکی دیگر از کارهایی که مقدار update انجام می‌دهد، ملاحظه می‌کند کدام Entity قدیمی تغییر کرده است (بعنوان مثال فیلدی به آن اضافه شده است که در نتیجه باید ستونی به جدول مربوطه‌اش اضافه شود). مقدار update تمام جداولی را که باید تغییر کنند را نیز alter می‌کند. بعبارتی جداول را بروز می‌کند.

پس درواقع مقدار update دو کار انجام می‌دهد:

  1. اگر جدول مربوطه به Entityای وجود نداشته باشد، آن را ایجاد می‌کند.

۲. اگر جدول مربوط به Entityای تغییر کرده باشد، آن را تغییر می‌دهد.

یک کار سومی نیز انجام می‌دهد: به entityهایی که جداول متناظرشان وجود دارد و نیازی به تغییر ندارند، دست نمی‌زند!
  • مقدار create-drop: این مقدار رفتار جالبی دارد. ابتدا تمام جداول را پاک می‌کند (درصورت وجود) بعد همچون مقدار create، آنها را ایجاد می‌کند، و پس از آن که کار به اتمام رسید دوباره آن‌ها را پاک میکند.
  • مقدار validate: این مقدار در واقع فقط چک می‌کند که آیا mapping schema (همان کلاس‌های ما با Annotationهای مربوطه‌اش) با table schema که در دیتابیس قرار دارد هماهنگ هستند یا خیر. و هیچ کار اضافه‌ای انجام نخواهد داد.

وقتی که برنامه از حالت development به حالت production می‌رود، معمولا این مقدار (validate) را برای تنظیمات هایبرنیت درنظر می‌گیرند.

خب، تا اینجای کار مقدار hbm2ddl.auto را برابر با update قرار دهید تا با ری‌استارت‌ کردن‌های مجدد برنامه اطلاعات دیتابیس پاک نشود و به ادامه‌ی توضیح مثال قبل بپردازیم.

من تصویر مثال قبل را دوباره در این قسمت نیز آپلود می‌کنم:

آزمودن هایبرنیت، به خروجی توجه کنید!
آزمودن هایبرنیت، به خروجی توجه کنید!


اول اجازه دهید که تاکید کنم هنگام استفاده از هایبرنیت توجه ویژه‌ای به لاگ‌های خود فریمورک داشته باشید. هایبرنیت بطور کامل توضیح می‌دهد که دقیقا چه دستوراتی را اجرا می‌کند، و عملا چه رفتاری دارد.

متاسفانه من حوصله‌ی تحلیل لاگ را ندارم، ولی چیزی نیست که از عهده‌ی آن برنیایید.

پرسش. من وقتی هایبرنیت را اجرا می‌کنم هیچ چیز راجع به دستورات SQL نمایش نمی‌دهد!

پاسخ. احتمالا مقدار show_sql در فایل کانفیگ را برابر با false قرار دادید. همانطور که در فایل کانفیگ مشاهده می‌کنید ما این مقدار را برابر با true قرار دادیم.
پرسش. من Entityای بنام hibernate_sequence نساخته‌ام، با این وجود یک جدول به این نام در دیتابیس من وجود دارد، و هربار هم عملیات insert را انجام می‌دهم ابتدا یک مقداری در این جدول آپدیت می‌شود. این چه جدولی است؟

پاسخ. خوشحالم که به لاگ‌های هایبرنیت هوشیارانه نگرسته‌اید. بله، این جدول را هایبرنیت خودش برای مدیریت یکتایی مقدار در فیلد id درست کرده است. زمانی که ما از Annotation تولید مقادیر (یعنی @GeneratedValue) استفاده می‌کنیم این اتفاق میافتد. ولی می‌توانستیم استراتژی مشخصی برای GeneratedValue در نظر بگیریم. بطور مثال وقتی در MySQL از مقدار @GeneratedValue(strategy = GenerationType.IDENTITY) استفاده می‌کنیم، دیگر این جدول کمکی ایجاد نمی‌شود. چند و چون و استراتژی‌های مختلف را شرح خواهم داد. در هر حال توجه ویژه شما به لاگ‌ها شایسته‌ی تقدیر است.

هر عملیاتی که با هایبرنیت انجام می‌دهیم، می‌بایست در یک تراکنش مشخص باشد. نگران نباشید، این جمله را کاملا توضیح خواهم داد.

به تصویر توجه بکنید، ما یک Session از کلاس SessionFactory میگیریم (Session session = sessionFactory.getCurrentSession). سپس با استفاده از‌ آن آبجکت یک تراکنش را شروع می‌کنیم (session.beginTransaction). بطور محاوره، از اینجا تا زمانی که تراکنش شده را کامیت می‌کنیم (session.getTransaction().commit)، میتوانیم عملیات خود را با دیتابیس انجام دهیم (درج کنیم، حذف کنیم، تغییر دهیم و یا بازیابی کنیم). در این مثال یک درج ساده انجام داده‌ایم (از متد save استفاده کردیم).

از زمان شروع تراکنش تا زمان کامیت آن، آبجکت در وضعیت persistence قرار می‌گیرد. این وضعیت را بعدا توضیح می‌دهم.

پرسش. شما با گفتن بعدا توضیح می‌دهم، حس بدی را به ما منتقل می‌کنید. حس نفهمیدن، و حس اینکه کلی مباحث دیگر مانده است که ما نمی‌دانیم.

پاسخ. متاسفانه این حس را نیز من دارم (کلی مباحث مانده که من نمی‌دانم)، ولی از اینکه حس بدی به شما منتقل شده است متاسفم، هدف من این است که دست‌کم اشاره به تمام موضوعات (تا جایی که خودم می‌دانم) بکنم، تا بتوانید جست‌و‌جو کنید. برای اینکه باور کنید حالت‌های آبجکت در هایبرنیت هم چیز عجیب و غریبی نیست، توضیح مختصری می‌دهم.
در هایبرنیت آبجکت ۳ وضعیت کلی دارد:
۱. وضعیت transient: زمانی که آبجکت با عملگر new ساخته می‌شود و هیچ ارتباطی با هایبرنیت ندارد!
۲. وضعیت persistant: زمانی که آبجکت دقیقا درگیر با Session هایبرنیت است.
۳. وضعیت detached: زمانی که آبجکت از از session حذف می‌شود.
شفاف نبود؟ عیبی ندارد. کارایی اصلی دانستن این موضوع بیشتر در رابطه با بحث Caching است، پس مفصل خواهیم گفت.

دوست دارید به کدام مبحث بپردازیم؟ بحث CRUD در هایبرنیت؟ یا کمی Annotationهای پایه‌ای را معرفی کنیم؟ هر دو مبحث جذاب هستند، اما اصلا قابل قیاس با جذابیت بحث Association Mapping نیست!


توضیح Annotationهای پایه‌ای

خب، تصمیم گرفته شد! البته حتما پیش‌تر توی فهرست مطالب خوانده بودید و من آنگونه که باید نتوانستم شما را سوپرایز کنم!

  • @Table

این Annotation اطلاعاتی در رابطه با اینکه جدول ساخته شده از یک Entity مشخص چگونه باید باشد را می‌دهد! البته صرفا در مورد نام آن تصمیم می‌گیرد. با تغییر پارامتر name در آن می‌توانید نام جدول را نامی دلخواه بگذارید:

@Table(name="my_table_name")
  • @Entity

این Annotation را باز هم خواهیم دید، اما اینجا یک اشاره کوتاه به آن کنیم. این Annotation نیز پارامتری بنام name دارد که نام Entity را تغییر می‌دهد. توجه کنید که نام Entity با نام جدول متفاوت است. نام جدول چیزی است که دیتابیس می‌شناسد و با آن کار می‌کند. اما نام Entity چیزی است که جاوا می‌شناسد. اگر این نام را تغییر دهید، دیگر همیشه باید با نام تغییر شده در کدهای جاوا آن را صدا کنید. گیج کننده شد؟ عیبی ندارد، مثالش را خواهید دید.

پرسش. درست است که گفتید اگر متوجه نشدید عیبی ندارد، اما دقیقا یعنی چی؟ یعنی نام کلاس جاوا تغییر می‌کند؟

پاسخ. خیر، نام کلاس جاوا تغییر نمی‌کند. بطور پیش‌فرض نام Entity همان نام کلاس جاوا است. بطور مثال کلاس Student نام Entityاش Student است. ولی وقتی تغییر دهیم، نام Entity دیگر نام پیش‌فرض نخواهد بود.
بخاطر دارید که در قسمت فلسفه‌ی هایبرنیت گفتم که مزیت اصلی هایبرنیت آن است که رویکرد شی‌گرا را حفظ کنیم؟ و بدون آنکه دیتابیس را مدنظر قرار دهیم به برنامه‌نویسی بپردازیم؟ مسئله همین جاست. بطور مثال ما در زبان HQL از نام Entity استفاده می‌کنیم که بطور پیش‌فرض نام کلاس است (بدون توجه به نام جدول)، وقتی نام Entity را تغییر می‌دهیم باید حواسمان همیشه جمع باشد که دیگر با نام تغییر کرده از آن در کد های جاوا استفاده کنیم (هرجا که نیاز به نام Entity هست، مثلا در استفاده از HQL)
  • @SecondaryTable(name="second_table")
  • @SecondaryTables({@SecondaryTable(name="second_table"),
    @SecondaryTable(name="another_second_table")})

خیلی وقت‌ها جداول دیتابیس آماده هستند و ما مجبور هستیم کلاس‌هایمان را با آن منطبق کنید. بطور مثال ممکن است یک کلاس جاوا، شامل ۲ جدول در دیتابیس باشد. چطور باید آن Entity را به ۲ جدول تقسیم کنیم؟ یا حتا بیشتر. اگر صرفا یک جدول اضافه هست (در مجموع ۲ جدول) کافی است از @SecondaryTable استفاده کنیم و نام جدول دوم را بدهیم. اگر بیش از ۲ جدول بود میباست از @SecondaryTables استفاده کنیم که یک "s" جمع دارد. و نوع استفاده از آن را در عنوان آورده‌ام. حال چگونه بگوییم کدام فیلد برای کدام جدول است؟ از @Column استفاده می‌کنیم. در @Column خصیصه‌ای هست بنام table که مقدار نام جدول دوم را برای آن در نظر می‌گیریم. اگر هیچ مقداری لحاظ نکنیم، بطور پیش‌فرض آن فیلد برای جدول اصلی درنظر گرفته می‌شود:

@Entity
@SecondaryTables({
        @SecondaryTable(name = "city"),
        @SecondaryTable(name = "country")
})
public class Address {
    @Id
    private Long id;
    private String street1;
    private String street2;
    @Column(table = "city")
    private String city;
    @Column(table = "city")
    private String state;
    @Column(table = "city")
    private String zipcode;
    @Column(table = "country")
    private String country;
    // Constructors, getters, setters
}

این تکه کد را از کتاب Beginning Java EE 7 برداشته‌ام. خوب به آن توجه کنید.

سوال، وقتی نگاشت مثال بالا صورت بگیرد، چه اتفاقی میافتد؟ لطفا در کامنت‌ها برای من بگویید.

کلیداصلی مرکب

خب، بنظور داشتن یک کلید مرکب، میتوان از قابلیت @Embeddable در JPA استفاده کرد. کلاسی را که Embeddable در نظر می‌گیریم می‌بایست حتما از قوانین JavaBeans پیروی کند. بعبارتی باید حتما یک سازنده‌ی بدون آرگومان (no-args constructor) داشته باشد، بهمراه متدهای getter و setter و همچنین متدهای equals و hashCode. نکته‌ی مهم دیگر اینکه کلاس Embeddable نباید هیچ کلیدی داشته باشد. بعبارتی فیلدی از آن نباید انوتیشن @Id داشته باشد. و اما چطور کار می‌کند؟ اجازه دهید ابتدا یک کلاس را بعنوان Embeddable ایجاد کنیم.

پس از ایجاد این کلاس، یک فیلد از جنس همین کلاس در کلاس مقصد (جایی که می‌خواهیم کلیدمرکب داشته باشیم) ایجاد می‌کنیم.

اگر گیج شده‌اید نگران نباشید، به مثالی که میزنم خوب دقت کنید. ما می‌خواهیم کلاس Student کلیدی مرکب داشته باشد، به این معنا که ترکیب کدملی و شماره‌شناسنامه‌ی داشنجو را بعنوان کلید درنظر بگیریم. تمام اطلاعات دیگر دانشجو هم سرجای خودش باقی بماند. حال چکار باید بکنیم؟ اولین کار ایجاد کلاس کلید! و دومین کار، تزریق آن به کلاس Student.

توجه کنید که کلاس Embeddable باید اینترفیس Serializable را impelement کند!

من تک تک کلاس‌ها را برای شما می‌گذارم، با دقت نگاه کنید، چرا که نسبت به مثال‌های پیشین اندکی تغییر داشته است.

کلاس StudentKey:

package entities;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;

@Embeddable
public class StudentKey implements Serializable {

 @Column(name = "shomare_meli")
    private int nationalId;
 @Column(name = "shomare_shenasname")
    private int sid;

    public StudentKey() { }

    public StudentKey(int nationalId, int sid) {
        this.nationalId = nationalId;
        this.sid = sid;
    }

    public int getNationalId() {
        return nationalId;
    }

    public void setNationalId(int nationalId) {
        this.nationalId = nationalId;
    }

    public int getSid() {
        return sid;
    }

    public void setSid(int sid) {
        this.sid = sid;
    }

 @Override
 public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        StudentKey that = (StudentKey) o;

        if (nationalId != that.nationalId) return false;
        return sid == that.sid;
    }

 @Override
 public int hashCode() {
        int result = nationalId;
        result = 31 * result + sid;
        return result;
    }
}

کلاس Student:

package entities;
import com.sun.istack.internal.NotNull;
import javax.persistence.*;

@Entity
public class Student {
    @EmbeddedId
    private StudentKey studentKey;
    @Column(name = "sname")
    private String name;
    @Column(name = "stell")
    private String tell;
    private String address;

    public Student(StudentKey studentKey, String name, String tell, String address) {
        this.studentKey = studentKey;
        this.name = name;
        this.tell = tell;
        this.address = address;
    }
    public StudentKey getStudentKey() {
        return studentKey;
    }
    public void setStudentKey(StudentKey studentKey) {
        this.studentKey = studentKey;
    }
    public Student() {} // non-arg constructor
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getTell() {
        return tell;
    }
    public void setTell(String tell) {
        this.tell = tell;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
}

کلاس Test:

import entities.Student;
import entities.StudentKey;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class Test {
    public static void main(String[] args) {
        StudentKey studentKey = new StudentKey(12345,67890);
        Student student = new Student(studentKey,"Mohamad", "09121112233", "Tehran");

        SessionFactory sessionFactory = new Configuration().configure("hibernate.cfg.xml")
                .addAnnotatedClass(Student.class)
                .buildSessionFactory();

        Session session = sessionFactory.getCurrentSession();
        session.beginTransaction();

        session.save(student);

        session.getTransaction().commit();
        session.close();
        sessionFactory.close();
    }
}

تصویر ساختار پروژه

ساختار پروژه
ساختار پروژه


می‌توانید عینا کدها را کپی/پیست کنید، گرچه توصیه می‌شود که بنویسید تا بهتر درک کنید. خروجی کد بالا را میتوانید در دیتابیس من ببینید:

خروجی کد بالا
خروجی کد بالا

بجای استفاده از انوتیشن‌های @Embeddable و @EmbeddedId می‌توانستیم از @IdClass استفاده کنیم. شما به من بگویید استفاده از آن چگونه است!


خب، با توجه به اینکه دوباره میزان کلمات استفاده شده در این پست زیاد شده است، ادیتور متن ویرگول سنگین شده است و سرعت تایپ من را پایین آورده است، باید این مطلب را ببندم و برای ادامه پست دیگری را باز کنم.

دوستان عزیزم به من بگویید که نظر شما در رابطه با این مطالب چیست. آیا چیزی دستگیرتان می‌شود یا مباحث همچنان برایتان گنگ است؟ نظرات شما کمک شایانی به من می‌کند. موفق باشید.

پایان قسمت دوم.