سلام
بعد مدتها فرصت شد شروع به مطالعه یک کتاب تخصصی در مورد ORM کنم.
یکی از بهترین کتابهایی که تو این حوزه نوشته شده است کتاب Java persistence with Hibernate می باشد. قبلاً براساس نیاز و وقتم مقالات پراکندهای در مورد ORM بهخصوص Hibernate خونده بودم ولی هیچ موقع فرصت / قسمت / اراده نشد تا به صورت مفهومی و عمیق در مورد این حوزه مطالعهای داشته باشم.
چند روزی است مطالعه کتاب مذکور را شروع کردم با مرور سرفصل هاش انتظار داشتم خیلی زود چند فصل اول را تمام کرده و به مباحث مورد نظرم برسم ولی از همان فصل اول کتاب ، انقدر مفاهیم مفهومی و عمیق و چرایی ضرورت رفتن سراغ این رهیافت ( ORM ) برایم جذاب آمد که تصمیم گرفتم قلم برداشته و نکات مهم و جالبش را یادداشت کنم . وسطهای نوشتن روی کاغذ به ذهنم رسید چرا این مطالب را با دوستان همکار و علاقه مندم به اشتراک نگذارم شاید حین مطالعه ،من مفاهیمی را درست متوجه نشده یا از چندین جنبه بررسی نکرده و ندیده باشم، اینطوری میتوانم از دیدگاهها و تجربیات دوستان هم صنفی خودم استفاده کنم!
خیلی خوشحال میشم نظرات خودتون را با من به اشتراک بگذارید اشتباهات لفظی / معنایی و تفسیریم را تذکر دهید .
من نیت و قصدم این است با مطالعه هر فصل کتاب ، مقاله گونهای تهیه کرده و با شما به اشتراک بگذارم . این مقاله اولین نوشته از سلسله نوشتارهای من در مورد ORM و بخصوص Hibernate است .
لطفاً منو همراهی کنید….
در هر پروژه نرم افزاری تصمیم گیری در مورد نحوه ذخیره سازی دادهها یک تصمیم گیری از جنس طراحی ست . البته صرف ذخیره سازی داده مهم نیست بلکه بحث integrity و یکپارچگی داده و اطلاعات نیز مطرح است( اگر قصدمان فقط ذخیره داده است همون سیستم فایل کافیست اگر از دروس و کتابهای دوره کارشناسی و درس پایگاه دادهها یادمان باشد سیستمهای مدیریت پایگاه دادهها برای این به وجود آمدنده اند تا یکسری از مشکلاتی که در سیستم ذخیره سازی فایلی بود برطرف و راهکارهای جدیدی برای مدیریت داده ارایه دهند طوری که کل دادهها و اطلاعات برامده از آنها دارای دو ویژگی integrity و consistency باشند و همچنین با مطرح کردن مباحثی مثل نرمال سازی مشکل افزوونگی دادهها ( redunduncy ) را نیز برطرف یا کمینه کنند ) .
اما چرا ORM ؟
قبل از اینکه سراغ ORM برویم بیاید نحوه ذخیره سازی / بازیابی دادهها در/ از پایگاه داده را در یک زبان برنامه نویسی شی گرا مرور کنیم :
۱- اول از همه نوع پایگاه داده را مشخص میکنید مثل Mysql یا Postgres یا …
۲- براساس انتخابتون در مرحله اول سراغ دانلود و گرفتن کتابخونه API های اون پایگاه داده جهت استفاده در زبان برنامه نویسی انتخابی خود میروید ( درواقع به ازای هر سیستم مدیریت پایگاه دادهها یک کتابخانه معادلی در اکثر زبانهای برنامه نویسی رایج دنیا وجود دارد که امکان ارتباط با یک پایگاه داده از داخل یک زبان برنامه نویسی فراهم میکند).
۳- بعداز افزودن و معرفی این کتابخانه به ClassPath پروژه خودتون ، سراغ معرفی کردن مشخصات دیتابیس موردنظرتون به پروژه میروید از جمله اینکه نام بانک اطلاعاتی مورد نظر شما در DBMS چیست ( آدرس Url اون بانک اطلاعاتی را معمولاً می نویسید طبق سینتکس مشخص مثلاً student_db ) ، با چه نام کاربری و رمزی و روی چه سرور و پورتی باید به این بانک اطلاعاتی وصل بشوید و اگر سیستم DBMS شما از نوع رابطهای است اصطلاحاً لهجه (dialect ) SQL ی اون DBMS چی هست در همون جا اعلام میکنیدو یکسری اطلاعات اضافی دیگری برحسب اینکه پروژه شما چقدر بزرگ و پیچیده باشد، ولی اطلاعات پایهای تقریباً اینها هستند.
۴- ساخت یک کانکشن با دیتابیس جهت بازیابی / ذخیره اطلاعات
۵- نوشتن کوئیری های مورد نظر و انجام دونه دونه عمل نگاشت ویژگیهای آبجکت خود به ستونهای جدول
۶- اجرای کوئیری و گرفتن نتایج و این بار نگاشت ستونهای جدول به ویژگیهای آبجکت موردنظرتون ( در داخل یک حلقه ).
۷- بستن کانکشن
۸- مدیریت خطاها و exception ها در صورت لزوم
ببینید همه این ۸ مرحله تقریباً در همه زبانهای برنامه نویسی شی گرا تکرار میشود مهم نیست پروژه شما در چه حوزه ای باشد و بیزنس کار چطوری باشد یا ساختار به اصطلاح domain model شما چیست و چقدر بزرگ یا کوچک است این کارهای تکراری در تمامی پروژه ها توسط برنامه نویسان شی گرا باید تکرار بشود.
در دنیای مهندسی نرمافزار و کلاً طراحی سیستم یک اصلی است بنام Separation of concerns به این معنی که سیستم را طوری باید طراحی کنید که هرکس بر حسب تخصص خود در هر مرحله از کار فقط روی یک موضوع تمرکز کند این کارهای تکراری هم مستقل از بیزنس کار هستند و میتوان انجام آنها را به یک ناظر بیرونی و سطح بالاتر (مثلاً کتابخانه / فریم ورک ) تخصیص داد و لازم نباشد برنامه نویس درگیر باز و بسته کردن کانکشن و نگاشت آبجکت /ستونهای جدول و … باشد در ثانی برنامه نویس شی گرا نباید درگیر مباحث زبانهای دیتابیسی مثل Sql باشد آن هم در این سطح وسیع .
تذکر : داخل پرانتز باید یادآور بشوم اول به خودم بعد به مخاطبین این مقاله که دروغه این حرف! برای یک برنامه نویس خوب شدن برای یک معمار و طراح نرمافزار خوب شدن باید به حوزه وسیعی ازتخصص ها و فیلدها وارد بشوید در حد لزوم از هرکدام اطلاعاتی داشته باشید به عنوان مثال در همین برنامه نویسی شی گرایی هنوز سناریوهایی پیش میاد که راهکارهای ORM نه تنها کمکی به ما نمیکند بلکه دست و پای ما رو هم از نظر فنی هم از نظر Performance ی میبندد و ما ناچاریم با همان Sql خام در داخل محیط برنامه نویسی شی گرایی خودمان با دیتابیس ارتباط بگیریم .
همچنین برای tune کردن performance پروژه های Hibernate نیاز به داشتن دانش عمیقی در حوزه SQL هستیم .
مطالبی که عنوان شد درو واقع اولین انگیزه برای رفتن سراغ رهیافت ORM است. همچنین یکی از دلایل روی آرودن به ORM این است که این روش کمک میکند نگهداری و قابلیت استفاده مجدد کد آسان تر صورت گیرد .
اما این تنها دلیل نیست بین پارادایم Relational و Object-oriented یکسری عدم تطبیق هایی ظاهر میشود که خود دلیل مهمی دیگری است جهت رفتن به سراغ تکنولوژی های مبتنی بر ORM .
درواقع مشکل در جایی خودش را نشان میدهد که در سطح domain model ( جایی که شما یکسری entity دارید برای ذخیره سازی در سطح دیتابیس ) تعداد زیادی entity دارید با سلسله مراتب مختلف و روابط زیاد بین آنها .
حال این عدم تطبیق ها چیا هستن
عدم تطبیق ها عبارتند از :
حال به نوبت هر یک را بررسی میکنیم :
عدم تطبیق در سطح دانه بندی (granularity)
مفهوم granularity :
گاهی به هنگام نگاشت آبجکت ↔ جدول نیاز است یک آبجکت به چندین جدول نگاشت بشود یا چندین آبجکت به یک جدول . در اینجا یک رابطه یک به یک بین تعداد و اندازه آبجکتها و جدولها وجود ندارد برای حل این مشکل راه حلهای مختلفی ارایه شده است . به مثال زیر توجه کنید
در سطح پروژه ما دو تا موجودیت داریم بنام های User و billingDetails . همچنین در سطح دیتابیس نیز دوتا جدول به همین نام داریم اما حین کار فرض کنید ما یک موجودیت دیگری بنام Address لازم داریم تعریف کنیم تا آدرسهای یک کاربر را درش نگه داریم. بزارید طور دیگری این موضوع را بیان کنم درسطح کلاس طراح ما به هردلیل منطقی نیاز دارد یک موجودیتی بنام address تعریف کند تا هر موجودیت مستقل دیگری که نیاز به ذخیره آدرس داشت بتواند این موجودیت فرعی ( موجودیتی که به خودی خود در bussiness model ما و در domain model ما وجود ندارد بلکه بخاطر نیاز موجودیتهای اصلی این موجودیت عینیت و وجود پیدا میکند ) را مورد ارجاع قرار داده و ازش استفاده کند .
public class User {
String username;
Set<Address> addresses;
Set<BillingDetails > billingDetails;
// Accessor methods (getter/setter), business methods, etc.
}
public class BillingDetails {
String account;
String bankname;
User user;
// Accessor
methods (getter/setter), business methods, etc.
}
حال سؤالی که مطرح است اینکه آیا چون در سطح برنامه ما سه تا موجودیت داریم ( دو موجودیت اصلی و یک موجودیت فرعی ) باید همین تعداد هم در سطح دیتابیس ، جدول داشته باشیم ؟
جواب مسلماً خیر است . روشهای مختلفی برای حل این موضوع است یک روش این است که تمام فیلدهای موجودیت آدرس رو در همان جدول User کنار بقیه فیلدها کپی کنیم چون واقعاً نیازی نداریم این رکوردهای آدرس را جدا از موجودیت اصلی شان یعنی user بازیابی / ذخیره سازی کنیم . این روش به نظر روش بهتری است اما روشهای دیگری هم وجود دارد ازجمله اینکه بیاییم از امکانات ارایه شده در سطح DBMS ها بنام User defined datatype ها استفاده کنیم . یعنی در سطح دیتابیس بیاییم یک نوع داده بنام address تعریف کنیم بعد یک ستون به جدول user اضافه کرده و نوع داده اش را از نوع address قرار بدیم . اشکالی که این روش دارد این است که کاملاً وابسته به محصول DBMS ی است که ازش درحال حاضر استفاده میکنیم و همه vendor ها این موضوع را به یک روش استاندارد پیاده نمیکنند .
روشهای نگاشت رو در ادامه و در مقاله های آتی ( اگر عمری باقی باشد ) با جزییات بیشتری بررسی خواهیم کرد.
نکته : در جاوا سطوح مختلفی از دانه بندی وجود دارد از دانه درشتهایی بنام کلاس گرفته تا دانههای متوسطی به اصطلاح value object ها تا دانههای ریزی چون enum ها و مجموعه های شمارشی . . این درحالیست که در سطح دیتابیس ما دو سطح دانه بندی بیشتر نداریم . دانه درشتی بنام جدول و دانههای ریزی بنام انواع دادههای توکار مثل number و char و varchar و datetime و…
عدم تطبیق در سطح سلسله مراتب و مبحث ارث بری :
در سطح برنامه نویسی ما در حوزه data model ها مفهومی بنام inheritance داریم درحالی که چنین چیزی در سطح db مشاهده نمیکنیم . از طرف دیگر وقتی مبحث inheritance مطرح میشود بلافاصله به دنبالش مفهوم polymorphism هم به میان میآید به مثال توجه کنیم :
در سطح برنامه نویسی در زمان اجرا یک نمونه آبجکت از کلاس user میتواند به یک نمونه از زیرنوع های BillingDetails مراجعه کند ( که این میشود polymorphism در سطح آبجکت ). به طور مشابه شما میخواهید قادر به نوشتن polymorphic query هایی باشید که به کلاس BillingDetails ارجاع کرده و آن query بتواند نمونههایی از زیر کلاس هایش برگرداند .
در سطح دیتابیس ما مفهومی بنام کلید خارجی داریم که ارتباط دو جدول را نمایش میدهد ولی فعلاً قادر نیستیم روش استانداردی و مستقل از DBMS خاص در پیش بگیریم بطوری که اون کلید خارجی موجود در جدول user علاوه بر اینکه به جدول BillingDetails ارجاع میکند بتواندبه هر کدام از زیرکلاس های اون BillingDetails نیز مراجعه کند . به عبارت دیگر کلید خارجی فقط میتواند به یک جدول خاص ارجاع کند نه چندین جدول . از طرف دیگر باید مکانیسمی داشته باشیم تا بتوانیم ساختار سلسله مراتبی مانند تصویر بالایی رو بدهیم به RDBMS و اون موقع بازیابی وjoin بتواند تشخیص دهد که الان باید جدول user را با مثلاً BillingDetail جوین کند یا با bankAccount
عدم تطبیق در حوزه Identity :
در زبان جاوا برابری دو obj ( اینکه بدانیم آیا a همان b است و نیز b همان a است ) دو روش وجود دارد:
یعنی a و b هر دو به یک مکان حافظه heap اشاره کنند و همچنین از نظر دو متد equals و hash مقدار true برگرداند یعنی ویژگی هاشون نظیر به نظیر مقدار یکسانی داشته باشند و کد hash تولیدشان برابر با هم . این موضوع درسیستم های مالی خیلی اهمیت دارد . در سطح دیتابیس ولی برابری دو آبجکت فقط از طریق مفهومی بنام primary key تعریف میشود . یعنی اگر هر دو آبجکت دارای primary key یکسانی باشند در نتیجه یکی هستند . Primary key ها در محیط های multi threading نقش حیاتی بازی میکنند بخصوص در مباحث caching و transaction ها .
عدم تطبیق در رابطه با مفهوم association :
در domain model مفهوم association رابطه بین entity ها را نمایش میدهد . عمل نگاشت association و مدیریت ارتباط بین entity ها جزو مفاهیم مهم در هر رهیافت ذخیره سازی آبجکت است . در زبانهای شی گرایی مفهوم association با object reference نمایش داده میشود. در سطح دیتابیس این مفهوم با قید کلید خارجی . این قیدها و بخصوص قید کلید خارجی یکپارچگی ارتباط بین جداول و موجودیتها را تضمین میکند . در سطح زبان این ارتباط بین موجودیتها ذاتاً جهت دار است و اگر در موقعیتهایی نیاز باشد این رابطه دو جهته باشد به صورت زیر تعریف میکنیم :
public class User {
set<BillingDetails> billingDetails;
}
public class BillingDetail {
User user;
}
اما در سطح دیتابیس اینکه ارتباط دو موجودیت در چه جهتی باشد اصلاً چنین چیزی مفهوم و معنی ندارد .درآنجا با عملگرهای join و projection میتوانیم این association را معنی میکنیم (حتی نیازی نیست بین دو جدول حتماً از طریق کلید خارجی ارتباطی وجود داشته باشد ) . د رمقاله های بعدی نحوه مدیریت ارتباط موجودیتها در سطح زبان و چگونگی نگاشت آن ارتباط ها در سطح دیتا بیس بیشتر خواهیم آموخت .
عدم تطبیق در حوزه پیمایش دادهها :
شاید مشکلترین مسأله در حوزه ذخیره سازی آبجکت مسأله پویایی باشد اینکه چطور یک داده در زمان اجرا مورد دستیابی قرار گیرد .
در سطح زبان برنامه نویسی نحوه دسترسی به یک آبجکت به این صورت است :
someUser.getBillingDetail().iterator().next();
یعنی دسترسی به یک آبجکت به صورت پشت سرهم و به روش پیمایشی در یک شبکه مفهومی از آبجکتهاست . اما چنین روشی در سطح db امکانپذیر نیست . در سطح دیتابیس تنها راهی که شما به هنگام دسترسی به دادهها باید انجام دهید این است که تعداد درخواستهای کمتری به سمت دیتابیس بفرستید تا performance سیستم بالا برود . اما اینکار مستلزم این است که شما همه آبجکتهایی که لازم دارید را با تعداد join های بیشتری بازیابی کنید و این کار باعث هدر رفت حافظه میشود البته با روشهای پیچیدهای چون حاغظه کش ثانویه میتوان تاحدودی این موارد را بهبود بخشید . اما اینکار یعنی شما علاوه بر داده / آبجکتهای مورد نیاز دادههای غیر ضروری زیادی هم بازیابی میکنید . بهترین کار این است که دقیقاً بدانید چه بخشی از شبکه آبجکتها رو میخواهید مورد دسترسی قرار دهید.
در مقاله های بعدی در مورد مفهوم Lazy loading و مسأله n+1 select بیشتر خواهیم آموخت .
پا نوشت :
تئوری CAP :
طبق تئوری Consistent ,Available, Partition tolerant (CAP) یک سیستم توزیع شده نمیتواند همزمان هر سه فاکتور consistency ، availablity و tolerancy را تأمین کند .
Consistent : یعنی همه نودها در شبکه دادههای یکسانی را ببینید
Available : همه درخواست های ارسال به سرور حتماً یک جوابی مبنی بر موفقیت آمیز بودن یا نبود درخواستشان دریافت کنند
Partition tolerant : حتی در صورت پیش آمد شکست سیستم بتواند به کار خودش ادامه دهد .
مثلاً اگر سیستم در حال تحمل بار زیادی است و امکان ارایه سرویس به کاربر نیست واضح به کاربر اعلان کند که لطفاً چند دقیقه دیگر مراجعه فرمایید سیستم در حال حاضر قادر به پاسخگویی نیست ( سایت فروش خودروها و اغلب سایتهای دولتی و دانشگاهی خوشبختانه این اصل را کاملاً رعایت میکنند!!!!! ) .