علاقهمند به یادگیری. آن کس که "چرایی" را یافته است، "چگونگی" را نیز خواهد توانست. • فردریش نیچه •
مروری اجمالی به آنچه که هایبرنیت میکند - قسمت چهارم
سلام.
خوشحالم که به اینجا رسیدیم. اگر کار را پس از همین پست به پایان ببرم نیز از خودم راضی خواهم بود. چرا که بخش اعظمی از مبحث نگاشت را پایان دادهام. دوستان زیادی تلاش کردهاند به فارسی hibernate را توضیح دهند، اما از دید من (تاکید میکنم برای من) مقالهای نبوده است که یکپارچه و یک تیکه به تمام ماجرا بنگرد. به همین خاطر ارزش زیادی برای این سری از مقالات قائل هستم. چون من از ابتدای کار، به کمک چندین منبع و چکیده کردن آنها و ساعتها مطالعه در بحث هایبرنیت توانستم برای خودم مباحث را یکپارچه کنم.
اولین باری که سراغ هایبرنیت رفتم مهرماه سال ۹۶ بود، متاسفانه با حجم عظیمی از ندانمها طرف بودم، و سختترین بخش برای من بحث نگاشت روابط بود (Relational Mapping). امروز دقیقا به همین بحث میپردازیم. مطمئن باشید که در کار به شدت به این بخش وابسته خواهید بود. و در هر مصاحبه کاری که قرار باشد مهارت شما نسبت به هایبرنیت سنجیده شود دستکم ۲ پرسش از این بخش خواهد بود.
مسئلهی دیگری که شاید بد نباشد با شما درمیان بگذارم، علت استفاده از متن به جای ویدیو است. ویدیو یک مزیت بزرگ دارد، شما دقیقا با چند و چون پیادهسازی آشنا میشوید. گرچه من تمام کدهایی را که برای پیادهسازی نیاز بوده را اینجا به اشتراک گذاشتهام، اما عمل کردن و فهمیدن آن تلاش مضاعفی میخواهد که باید به گردن بگیرید. مزیت دیگر ویدیو راحتی ایجاد آن است. نوشتن به مراتب سختتر است و انرژی بیشتری میطلبد. ویدیو را راحتتر میتوانستم تهیه کنم چرا که هم به ابزار تدوین، و هم ابزار با کیفیت ضبط دسترسی داشتم. اما چرا به سراغ مقالهی متنی آمدم؟ قطعا دلیلم جستوجوی اینترنتی و دیده شدن نبوده است. این دست آموزشها در یوتیوب با اقبال بهتری روبهرو میشود. من تلاش میکنم تا بتوانم دفتری را بعنوان رفرنس تهیه کنم، صد البته که رفرنس کامل نخواهد بود، منظور من این است که هر گاه مطلبی را فراموش کردید بتوانید به راحتی پیدا کنید، و به سراغش بیایید، نه این که مجبور باشید چندین ویدیو را بالا پایین بکنید تا ببینید دقیقا در کدام قسمت فلان مطلب ذکر شده بود!
مباحثی مثل هایبرنیت مخاطب عام ندارد، تقریبا فیدبک جدیای که تا کنون دریافت کردهام، ولی درک میکنم که شاید شمایی که اکنون در حال مطالعه هستید در زمانی باشید که ماها از نگارش این مقاله گذشته باشد. پس ۲ نکته را با شما درمیان میگذارم. من تمام تلاشم را برای نگارش دقیق مقالات بکار میبرم، سعی میکنم سمبلکاری نشود، پس نگران این موضوع نباشید. نکتهی دوم اینکه هایبرنیت در حال پیشرفت است، همین حالا که صحبت میکنیم نسخهی ۶ آن در شرف انتشار است. شاید بعضی از APIهایی که از آنها استفاده میکنیم، تغییر کند، اما دوست گرانقدر، بخش اعظمی از مباحثی که تاکنون گفته شده است کاملا تئوریک هستند، دقیقا مشابه این قسمت و ۲ قسمت قبلی و ابتدای قسمت اول. اینها تقریبا هیچ زمانی نقض نمیشوند، شاید به آنها اضافه شود، ولی احتمال کاسته شدن از آن کم است، این اتفاق هم بیافتد چیزی از دست نخواهید داد.
لینک قسمتهای مرتبط
فهرست
نگاشت روابط - Relational Mapping
دنیای برنامهنویسی شیگرا احاطه شده از کلاسها و روابط بین آنها. این روابط ساختارهایی هستند که آبجکتها را بنوعی دیگر از آبجکتها مرتبط میکند (لینک میکند).
نکته. من در اینجا (بخشی که مربوط به جاوا است) وقتی از روابط صحبت میکنم منظورم چیست؟ منظورم لغت Associations است. چون relation رو هم رابطه میگویم (در بخشی که مربوط به دیتابیس است)، الزامی دیدم که بگویم من هر دوی اینها را یک چیز میگویم.
پرسش. گیجمان نکنید، اصلا association چه چیزی بود توی دنیای برنامهنویسی که میگویید آن احاطه شده از کلاسها و associationهای آنها؟!
پاسخ. خواهش میکنم سخت نگیرید، مطمئن هستم بلد هستید. association به روابط بین چندین آبجکت اشاره میکنه (refers to the relationship between multiple objects). بعبارت دیگر به چگونگی ارتباط آبجکتها با هم و همچنین چگونگی استفاده از قابلیتهای یکدیگر اشاره میکند(refers to how objects are related to each other and how they are using each other's functionality). در دنیای برنامهنویسی شیگرا (نه آنطور که شما میگید فقط دنیای برنامهنویسی) ۲ نوع رابطهی مهم دارید. یک Composition و یکی Aggregation. اینها چه هستند را سرچ کنید!
پیش از هرچیز بگویم، هر ارتباط جهت (direction) دارد. میتواند یک طرفه (unidirectional) یا دو طرفه (bidirectional) باشد. ارتباط یک طرفه بطور مشخص با فلش جهت آن نشان داده میشود:
و ارتباط دو طرفه در UML با خط ممتد مشخص میشود:
حتا میتوان تعداد آبجکتهای اشاره شده در ارتباط را نیز مشخص کرد. بعبارتی در UML چندی ارتباط یا همان کاردینالیتی آن را مشخص کرد. به تصویر زیر دقت کنید، هر ارتباطی که بین کلاسها برقرار شده است (که با فلش یا خط نشان دادهشده است) شامل ۲ عدد نیز میشود. بعنوان مثال ارتباط بین Platform و Decive ID را مشاهده کنید. اول از همه با توجه به فلش متوجه میشویم که ارتباط یکطرفه است، و مالک ارتباط Device ID است. و همچنین به کمک * و 1 که در ارتباط نشان داده شده، متوجه میشویم که یک ارتباط چندبهیک است. در جاوا وقتی بیشتر از یک آبجکت را میخواهیم از انواع مجموعهای Collections، List، Set و حتا Map که همهی آنها در پکیج java.util هستند، استفاده میکنیم.
هر ارتباط نیز یک مالکیت دارد (ownership). بعبارتی کسی صاحب (owner) رابطه است. در روابط یک طرفه مالک رابطه بطور ضمنی مشخص است (در عکس اول (Unidirectional Association) مشخص است که کلاس A مالک رابطه است)، اما در ارتباطهای دوطرفه باید بطور واضح مالکیت رابطه را مشخص کنیم.
روابط در دیتابیسهای رابطهای - Relationships in Relational Databases
در دنیای رابطهای مسائل متفاوت است چرا که همه چیز اینجا رابطه (relation) است. که به آنها جدول هم میگوییم. به این معنا که همه چیز اینجا با جدول مدل میشود. برای نگاشت یک association دیگر List، Set و یا Map نداریم، در عوض جدول داریم. فلذا برای نگاشت رابطهی بین یک کلاس با کلاس دیگر باید از ارجاع جدولی (table reference) استفاده کنیم. که این ارجاع به ۲ روش مختلف مدل میشود:
- استفاده از کلید خارجی (join column)
- استفاده از جدول میانی (join table)
اجازه بدید این ۲ روش را با تصویر به شما نمایش بدم. از کتاب Beginning Java EE این تصاویر را برمیدارم. فرض کنید موجودیتی بنام Customer داریم که با موجودیت Address یک ارتباط یک به یک دارد. حال با استفاده از متد Join Column به این شکل میشود:
همانطور که ملاحظه میکنید در روش اول با استفاده از کلیدخارجی (Join Column) این ارتباط نمایش داده شده است. حال همین ارتباط یک به یک را میتوان با استفاده از جدول میانی نمایش داد. توجه داشته باشید که ارتباط یک به یک را معمولا با استفاده از کلیدخارجی مدل میکنند، اما در اینجا که هدف ما بحث آموزش است روش جدول میانی (Join Table) را هم نمایش میدهیم:
همانطور که خدمتتون عرض کردم، از جدول میانی عموما برای مدل کردن ارتباطهای یکبهچند و یا چندبهچند استفاده میشود.
خب، نگاهی به مطالب بالا بیاندازید، چه چیزهایی را گفتیم؟ گفتیم در جاوا ارتباطها جهت دارند، یا به عبارتی مالکیت در آنها مطرح است. و نشان دادیم که چندی ارتباط را در جاوا چطور پیاده سازی میکنیم (در صورت نیاز از انواع کالکشنها استفاده میکنیم) و نشان دادیم که در دیتابیس چگونه است. گفتیم ارتباطات در دیتابیس به ۲ شکل (یکی با کلیدخارجی و دیگری با جدول میانی) مدل میشود. حالا وقت آن است که برویم سراغ JPA و امکاناتی که آن برای نگاشت آبجکتهای جاوا به جداول دیتابیس فراهم کرده نگاهی بیاندازیم. البته مشخصا منظور ما نگاشت ارتباطات است. ارتباطات بعضا پیچیده، چرا که پیش از این انواع دیگر نگاشت (بغیر از وارثت) را بررسی کردیم.
ارتباطات موجودیت - Entity Relationships
هایبرنیت یا دقیقتر JPA روابط را خودش آنطور که میفهمد بصورت پیشفرض برای ما نگاشت میکند (بدون آنکه چیزی به آن بگوییم) اما احتمالا این روش همان چیزی نباشد که مناسب کار ما باشد. به همین خاطر امکاناتی برای سفارشیسازی (customize) کردن عملیات نگاشت فراهم کرده است.
چندی ارتباطات ممکن بین ۲ موجودیت شامل یکبهیک، یکبهچند، چندبهیک و چندبهچند میشود. به همین ترتیب برای نگاشت آن انوتیشنهای @OneToOne، @OneToMany، @ManyToOne و @ManyToMany را فراهم کرده است.
تمام این ارتباطات میتواند در حالت یکطرفه یا دوطرفه برقرار شود. (خاطرتان هست که قسمت اول در مورد مالکیت و جهت ارتباط صحبت کردیم). فلذا تمام حالاتی را که میتواند رخ دهد را در عکس زیر آوردهام:
من به تمام این حالات میپردازم، اما خواهید دید که ارائهی چند مثال بقیه مباحث رو تکراری میکنه. پس بهتر است بگویم توضیحاتی در ادامه خواهم داد که تمام این حالات را پوشش بدهد.
متوجه هستید که وارد چه مباحث جذابی شدهایم؟ در عین سادگی جذابیت زیادی دارند. همراه باشید!
یکطرفه (Unidirectional) و دوطرفه (Bidirectional)!
همانطور که تو بخش اول هم متذکر شدیم، جهت در بحث مدلکردن آبجکت امری طبیعی است. در یک ارتباط یکطرفه، آبجکت A به آبجکت B اشاره میکند، در یک ارتباط دوطرفه، هر دو آبجکت بهم اشاره میکنند. اجازه بدهید دیگر وارد مثال شویم تا بهتر درک کنیم.
در یک ارتباط یکطرفه، موجودیت Customer یک فیلد از نوع Address دارد (در عکس زیرین نیز مشخص است). این ارتباط یک طرفه است، از یک سمت به سمت دیگر اشاره میشود. بعبارت دیگر Customer مالک این ارتباط است.
خب، اجازه دهید در ادبیات دیتابیس هم این موضوع را بررسی کنیم. در ادبیات دیتابیس جدول CUSTOMER کلیدخارجیای (Join Column) دارد که به ADDRESS اشاره میکند. هر زمان هم که شما مالک ارتباط هستید، میتوانید ارتباط را تغییر دهید. یعنی چه؟ یعنی اگر بخواهیم اسم کلیدخارجی (که به ADDRESS اشاره میکند) را تغییر دهیم باید موجودیت Customer را تغییر دهیم (مالک رابطه).
همانطور که اشاره کردیم یک ارتباط میتواند دوطرفه نیز باشد. در همین مثال خودمان اگر یک فیلد از نوع Customer در موجودیت Address قرار دهیم، این ارتباط را به صورت دوطرفه درآوردهایم.
پرسش. من آشناییِ خیلی کمی با UML دارم. در ارتباط یکطرفه وقتی گفتید یک فیلد از Address در کلاس Customer قرار دهیم، من در دیاگرام Customer فیلد آدرس را مشاهده نکردم. و به همین ترتیب در ارتباط دوطرفه هم که فیلدی از جنس Customer در Address قرار دادیم، دوباره چیزی مشاهده نکردم! در صورتیکه مابقی فیلدها نوشته شده است (بعنوان مثال در Customer فیلدهای firstName، lastName و...)
پاسخ. در دیاگرامهای کلاس UML وقتی فیلدی با رابطه مشخص شده باشد، دیگر نوشته نمیشود. بعنوان مثال در ارتباط یکطرفه وقتی فلشی جهتدار از Customer به Address رفته است بیانگر فیلدی از جنس Address در کلاس Customer است، به همین خاطر دیگر آن را نمینویسند.
در یک ارتباط دوطرفه، چه کسی مالک رابطه است؟ چه کسی اطلاعات ستون پیوندی (کلیدخارجی یا همان join column) و یا اطلاعات جدول پیوندی (جدول میانی یا همان join table) را در خودش دارد؟
درحالیکه ارتباطات یکطرفه تنها سمت مالک دارد، ارتباطات دوطرفه هم سمت مالک دارد هم سمت مقابل (inverse side) که با المان mappedBy در انوتیشنهای @OneToOne، @OneToMany و ManyToMany قابل مشخص کردن است. در اصل mappedBy مشخص میکند که کدام خصیصه مالک رابطه است و وجودش در ارتباطهای دوطرفه الزامی است.
کمی گیج کننده است! میدانم. اجازه دهید یک توضیح کوچک دیگر هم بدهم. فرض کنید دو entity بنامهای Customer و Address داریم. بین این دو انتیتی، یک ارتباط یکبهیک برقرار است (هر مشتری تنها یک آدرس دارد، و هر آدرس تنها متعلق به یک مشتری است). مالک رابطه کدام انتیتی است؟ Customer!
اجازه دهید خیلی خودمانی توضیح دهم که چه رخ داده است، با انوتیشن @OneToOne مشخص کردیم که یک ارتباط یکبهیک داریم، این انوتیشنرا در دوطرف استفاده کردیم، پس یک ارتباط دوطرفه داریم. در سمت Address از المان mappedBy استفاده کردیم و با این مشخص کردیم که Address سمت مقابلِ (inverse side) مالک قرار دارد و همچنین مشخص کردیم دقیقا به چه ارتباطی، مرتبط است. با @JoinColumn هم مشخص کردیم که یک کلید خارجی میخواهیم به Address و نام ستوناش را نیز مشخص کردیم. توجه داشته باشید، مقداری که در mappedBy قرار میگیرد باید با نام propertyای که در سمت مالک تعریف شده است یکی باشد (چون میخواهد مشخص کند که بین این دو ارتباطی برقرار است). حالا شفاف شد؟ بنظرم باید شده باشد، ولی چنانچه نشده است نگران نباشد، تازه میخواهیم وارد آب شویم و تنی به آب بزنیم!
ارتباطِ یکطرفهی یکبهیک (OneToOne Unidirectional)
چنین ارتباطی به چندی ۱ از یک طرف به سمت دیگر است. بیایید همان مثال مشتری و آدرس را توسعه دهیم. همچنان میخواهیم هر مشتری تنها یک آدرس داشته باشد اما (بنا به هر دلیلی) نمیخواهیم آدرس چیزی از مشتری بداند. یعنی ما از مشتری میخواهیم به آدرس برسیم، اما از آدرس به مشتری نه! عکس زیر گویای مطلب هست، نمونههایش را هم در مثالهای قبل مشاهده کردید.
اینبار چون ارتباط یک طرفه است و مالک ارتباط هم Customer میباشد، اطلاعات مروبط به نگاشت را فقط در انتیتی Customer قرار میدهیم و نیازی نیست که انتیتی آدرس چیزی از آن بداند.
@Entity
public class Customer {
@Id @GeneratedValue
private Long id;
private String firstName;
private String lastName;
private String email;
private String phoneNumber;
@OneToOne (fetch = FetchType.LAZY)
@JoinColumn(name = "add_fk", nullable = false)
private Address address;
// Constructors, getters, setters
}
به گمانم کاملا واضح است، اما باز هم برای رفع هر سوتفهامی، کد مربوط به انتیتی Address را هم قرار میدهم تا ملاحظه کنید که هیچ چیز آن تغییر نکرده است:
@Entity
public class Address {
@Id @GeneratedValue
private Long id;
private String street1;
private String street2;
private String city;
private String state;
private String zipcode;
private String country;
// Constructors, getters, setters
}
همانطور که ملاحظه میکنید آدرس هیچ اطلاعاتی مربوط به نگاشت را ندارد.
پرسش. در انتیتی Customer المانی بنام fetch اضافه کردید و مقدار آن را برابر با LAZY قرار دادید، آن چیست؟
پاسخ. بله، متاسفانه هنوز به سراغ fetch کردن نرفتهایم، به زودی آن را توضیح خواهیم داد. اما اگر بطور خلاصه بخواهم بگویم هدف من در اینجا این بوده است که اگر من اطلاعات مشتری را خواستم نیازی نیست اطلاعات مربوط به آدرس آن هم برای من بازیابی شود مگر اینکه صراحتا بگویم! فعلا به همین توضیح مختصر بسنده کنید.
ارتباط یکطرفهی یکبهچند (ManyToMany Unidirectional)
خب، حال فرض کنید که ۲ انتیتی بنامهای Order و OrderLine داریم که یک ارتباط یکبهچند یکطرفه بین آنها برقرار است. مالک ارتباط نیز Order میباشد. خب، تا همینجای کار باید حدس زده باشید که چطور باید نگاشت صورت بگیرد. اگر نه، عیبی ندارد، اجازه دهید بیشتر توضیح دهم.
هر Order چندین OrderLine دارد. ارتباط از سمت Order به OrderLine است (یک طرفه). فلذا OrderLine هیچ اطلاعاتی در مورد نگاشت این ارتباط ندارد. پس ما فقط باید یک آبجکت از OrderLine درون انتیتی Order قرار دهیم، درست است؟ نه کاملا! ما باید لیستی از آبجکتهای OrderLine را درون انتیتی Order قرار دهیم (چرا که هر Order چندین OrderLine دارد و بعبارتی ارتباط یک به چند است).
خب، کد انتیتیهای مربوطه به شکل زیر است:
Order
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@Temporal(TemporalType.TIMESTAMP)
private Date creationDate;
private List<OrderLine> orderLines;
// Constructors, getters, setters
}
OrderLine
@Entity
@Table(name = "order_line")
public class OrderLine {
@Id @GeneratedValue
private Long id;
private String item;
private Double unitPrice;
private Integer quantity;
// Constructors, getters, setters
}
خوب نگاه کنید! بنظر هیچ اطلاعاتی مربوط به نگاشت یکبهچند در انتیتی Order نیست! ما گفتیم هیچ انوتیشنی(اطلاعاتی) نباید در انتیتی OrderLine قرار داد چرا که ارتباط یکطرفه است. اما اینجا انوتیشنی هم در Order قرار ندادهایم و فقط لیستی از آبجکتهای OrderLine را تعریف کردهایم. چرا؟
هایبرنیت زمانیکه ببینید یک انتیتی لیستی از یک انتیتی دیگر را در خودش دارد، بصورت خودکار متوجه میشود که یک ارتباط یکبهچند وجود دارد. و انگار که خودش انوتیشن @OneToMany را برای فیلد مربوطه قرار میدهد. اما سوال دیگر، چطور در دیتابیس نگاشت میکند؟ بصورت پیشفرض هایبرنیت ارتباط یکبهچند را با جدول میانی (@JoinTable) ایجاد میکند.
میبینید؟ جدولی میانی بنام ORDER_ORDER_LINE ایجاد کرده است و ترکیب آیدی جدول ORDER و آیدی جدول ORDER_LINE را بعنوان کلید اصلی آن قرار داده است. اسامی چطور انتخاب شده است؟ منظورم اسامی جدول ایجاد شده با اسم ۲ ستون آن است. این اسامی بصورت پیشفرض گذاشته شده است. چنانچه بخواهید آنها را تغییر دهید، دیگر باید خودتان صراحتا وارد عمل شوید و اجازه ندهید هایبرنیت تنظیمات پیشفرضش را اعمال کند. پس من انتیتی Order را به شکل زیر تغییر میدهم:
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@Temporal(TemporalType.TIMESTAMP)
private Date creationDate;
@OneToMany
@JoinTable(name = "jnd_ord_line",
joinColumns = @JoinColumn(name = "order_fk"),
inverseJoinColumns = @JoinColumn(name = "order_line_fk") )
private List<OrderLine> orderLines;
// Constructors, getters, setters
}
کد را خوب بنگرید! قسمتی را که بولد کردم را خوب نگاه کنید، در اینجا اسم جدول میانی و همچنین اسم ستونهای آن را اضافه کردهام. با این تنظیمات همان مدل قبلی ایجاد میشود ولی با اسامی جدید و دلخواه من.
همه چیز شفاف است؟ امیدوارم باشد. ببینید دوستان پیشفرض برای مدل کردن ارتباط یکبهچند استفاده از جدول میانی است، اما فرض کنید بنابههردلیلی من تصمیم بگیرم که از کلیدخارجی (Join Column) استفاده کنم. چکار باید کرد؟ حتما میدانید که به راحتی فقط باید بجای @JoinTable که جدول میانی ایجاد میکند از @JoinColumn استفاده کنم. نگاه کنید که چطور دوباره کد مربوط به انتیتی Order را ریفکتور میکنم:
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@Temporal(TemporalType.TIMESTAMP)
private Date creationDate;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "order_fk")
private List<OrderLine> orderLines;
// Constructors, getters, setters
}
به همین راحتی. حالا مدل من به چه شکل میشود در دیتابیس؟ عکس زیر را نگاه کنید:
خب، یک مدل نگاشت دیگر باقی میماند تا بحث مربوط به آن تمام شود. متاسفانه ویرایشگر ویرگول بسیار سنگین شده است و نوشتن در آن سخت شده است. بعضی از عکسهایی هم که آپلود میکنم بیخودی و بیدلیل میپَرَد! بهتر است مطلب جدیدی را شروع کنم.
ممنونم که تا اینجا همراهی کردید، و امیدوارم که این مقالات به درد شما خورده باشد!
مطلبی دیگر از این انتشارات
تحلیل بازاریابیِ خردهفروشیها – بخش هفتم
مطلبی دیگر از این انتشارات
مروری اجمالی به آنچه که هایبرنیت میکند - قسمت اول
مطلبی دیگر از این انتشارات
فیصله دادن به مبحث Logging در جاوا (قسمت اول)