كلاسهای ساده برای JDBC

از يكنواختی برنامه‌نويسی JDBC دوری كنيد

 

نويسنده:Madhu Siddalingaiah

Java World

مترجم: نازنين حقيقی

 

 

 

خلاصه

تقريبا هر برنامه كاربردي J2EE از طريق يك پايگاه داده رابطه‌اي به دادهها دسترسي پيدا ميكند. به همين دليل،Java Database Connectivity (JDBC) احتمالا يكي از مرسوم‌ترين توابع API قابل استفاده براي پلات‌فرم جاوا است. JDBC بطور فرض ساده و استفاده از آن آسان مي‌باشد، اما در برنامه‌هاي كاربردي توليد، جزئيات متعددي مي‌تواند حتي پيش پا افتاده‌ترين برنامه‌ كاربردي (CRUD) create/read/update/delete را دشوار سازد. گرچه بسياري از چارچوبها، ابزارها و توابع API مي‌توانند طراحي را ساده سازند، اغلب بسيار پيچيده هستند. نويسنده اين مقاله، به عنوان يك راهحل، مجموعه كوچكي از كلاسهايي كه كاربرد، نگهداري و توسعه‌شان آسان است را ارائه مي‌دهد.

با اين همه يك ابزار /API/Wizard/ حيطه ديگر فعاليت شما را دچار مشكل مي‌‌سازد. من مي‌توانم علت ترديد شما را بفهمم: سالهاست درباره مشكل mapping (تبديل) رابطه‌اي صحبت ميشود. هركس راه حلي دارد كه ساده‌تر، بهتر، سريعتر و ارزانتر است. چرا ما به كدهاي بيشتري نياز داريم كه احتمالا آنچه شخصي قبلا در جايي ديگر انجام داده است را انجام ميدهند؟ به دو دليل: اكثر راه‌حلها بيش از حد لازم پيچيده به نظر مي‌آيند و نياز به تعهدي مهم دارند. يك‌بار كه شما آن را تجربه كنيد، كنار گذاشتنش دشوار است.

من چيزهاي ساده را مي‌پسندم، يكي از همكاران اخيرا نقل قول زيبايي را مطرح كرد:

 

«سادگي هميشه ماندني است، پيچيدگي به گمنامي منتهي مي‌شود»

 

و همه ما جمله انيشتين را شنيده‌ايم:

«يك تئوري علمي بايد تا حد ممكن ساده باشد، ولي نه ساده‌تر»

 

در اينجا يك جمله از خود James Gosling مي‌آيد:

«پيچيدگي از بسياري جهات وحشتناك است. پيچيدگي درك چيزها، ساختنشان، اشكال زدايي، تكامل‌شان و تقريبا انجام هر كاري را دشوارتر مي‌كند.»

 

من مي‌توانم ادامه دهم، مثالهاي بسياري از پيچيدگي وجود دارد، بسيار زياد. در حالت دسترسي پايگاه داده، شما ميتوانيد از اجزاي واحد يا يك ابزار mapping رابطه‌اي يا جديدترين حيطه Open Source استفاده كنيد. من نارضايتي جدي از هيچ يك از آن ابزارها ندارم. مطمئنم طراحانشان در هنگام طراحي آنها ديدي وسيع پيش‌رو داشتند. امروز من چيزي ساده مي‌خواهم.

 

مشخص نمودن آزادی عمل

بياييد كمي به عقب برگرديم. اغلب مواقع يك برنامه كاربردي بايد اطلاعات را از يك كاربر بگيرد و آن را بصورت سطرهايي در يك ديتابيس create/edit/delete/view نمايد. يك توضيح مهم آن است كه اطلاعات از كاربر بايد به شكل يك رشته باشد. اين بدان معني است كه معتبرسازي تجزيه بايد در هنگام ورود و فرمت كردن در هنگام خروج صورت گيرد. اين امر دشوار نيست، اما تكراري و در برگيرنده چك‌هاي بي‌اهميت خسته كننده بسياري است. ويژگي مهم ديگر كه كاربران انتظارش را دارند كنترل‌هايVCR  هستند: توانايي بازبيني مجموعه وسيعي از نتايج يا پرش به انتها، كه انجام آن را Resultset به تنهايي، آسان نيست.

يك راه حل متداول، تبديل دستي يا اتوماتيك نتايج و طرحهاي جدول SQL به آبجكتها و كلاسها است. در ابتدا، اين منطقي به نظر مي‌آيد، گرچه طبيعي نيست. بطوركلي، طراحان ديتابيس و طراحان آبجكت تلاش ميكنند همان چيز را انجام دهند: كنترل اطلاعات تجاري به شيوهاي منطقي و قابل گسترش.

متاسفانه، شيريني كار در اينجا خاتمه مي‌يابد. طراحان آبجكت ترجيح مي‌دهند خود را دور از جزئيات گوناگون مربوط به اينكه چگونه دادهها وارد و خارج مي‌شوند نگه دارند. براي برخي، پايگاه داده تنها يكhashmap  بزرگ با بيش از يك كليد است. اين ممكن است يك ارزيابي دقيق باشد، اما يك بخش پيچيده SQL به نام اتصال join جدول وجود دارد. متخصصان پايگاه داده اينگونه بحث ميكنند كه اتصالات، به پايگاه‌هاي داده رابطهاي توانايي مي‌دهند. طراحان آبجكت ممكن است به يك اتصال به شكل موجود وحشي رام نشده‌اي بنگرند كه فقط بيش از آنچه ارزش دارد دردسرساز مي‌شود. جدا از نظرات شما، آبجكتها و پايگاه‌هاي داده احتمالا در آينده نزديك بطور چشمگير تغيير نخواهند كرد. برنامه‌هاي كاربردي و منطق تجاري همچنان از آبجكتها استفاده مي‌كنند و پايداري داده‌ها همچنان به شكل يك ديتابيس رابطه‌اي در مي‌آيد.

جدا از برخي موانع، تفاوتهاي فاحشي بين آبجكتها كلاسها و سطرهاي جداول وجود دارد. آبجكتها مي‌توانند در برگيرنده رفتار باشند كه آنها را از ساختارها و ساير انواع كلي مجزا مي‌نمايد. كلاسها وراثت و چند شكلي Polymorphism را ارائه مي‌دهند، كه آنها را قابل استفاده مجدد و قابل گسترش ميكند. اما ديتابيس‌ها تقريبا ذخيره مناسب و جستجوي سريعي دارند. وقتي شما قابليت سطرهاي اطلاعاتي را كه اگر ميليونها نباشد به ده‌ها‌هزار مي‌رسد در نظر بگيريد، نمي‌توان آن را دست كم گرفت. اين اطلاعات و دستيابي سودمندش درست به اندازه كدي كه آن را مديريت مي‌كند مهم مي‌باشد.

نكته مهم ديگر آن است كه تغيير كلاسها ‌آسان مي‌باشد و كد اغلب هنگامي كه مناسب و مفيد باشد مجدد فاكتورگيري مي‌شود. تغيير ساختاري ديتابيس كار بسيار بزرگتري است. تغيير اسامي ستونها يا حتي ايجاد جداول جديد و بارگذاري مجدد داده‌ها به تنهايي دشوار نيست، اما تنظيم تمام كدهايي كه متكي به آن ساختار است مي‌تواند مشكلساز باشد. به همان دليل، معمولا جداول ديتابيس پس از پر شدن اندكي تغيير مي‌كنند.

براي جلوگيري از يكنواختي، بسياري از ابزارها طي سالها تكامل يافتهاند. ويژگي J2EE يك مدل به شكل اجزاي واحد كنترل شده توسط محفظه و (Enterprise Java Bean Query Language) EJB-QL ارائه ميدهد. در يك جامعه Open Source، Hibernate يك راه حل متداول است. اين راه‌حلها بستگي به يك زبان جستجو مثل SQL دارد، اما نه بطور دقيق. نتيجه آن است كه ممكن است شما به جاي استفاده از آنچه ديتابيس به تنهايي قادر به انجام است، جستجوهايتان را تنظيم كنيد تا متناسب با ابزار شود.

اينها مي‌توانند راه‌حلهاي موثري براي دامنه وسيعي از مشكلات باشند، اما هنوز استاندارد سادگي دلخواه من را برآورده نمي‌سازند. من بر دلخواه بودن تاكيد مي‌كنم، زيرا اين راه‌حل‌ها نيازهاي بسياري از افراد را برطرف ميكنند، اين تنها يك تفاوت عقيده است و نه بيشتر.

 

تعقيب

بسياري از طراحاني كه من با آنها كار كرده‌ام طراحان آبجكت يا طراحان ديتابيس نيستند. آنها گروه سوم برنامه‌نويسان را تشكيل ميدهند و به يك اندازه با آبجكتها و SQL راحت كار مي‌كنند. آنها طراحان PowerBuilder هستند. PowerBuilder مثل هر محصول ديگري كيفيتهاي بد و خوب خودش را دارد، اما به نظر من، يك مورد استثنايي دارد: پنجره داده. به‌جاي تبديل هر جدول يا مجموعه نتايج احتمالي به يك كلاس مجزا، PowerBuilder يك كلاس دارد: پنجره داده. به عبارت ساده، پنجره داده شبيه يك ResultSet قابل به روزرساني است. جهت استفاده از آن، صرفا يك جستجو انجام دهيد، هر جستجويي، مهم نيست چقدر پيچيده باشد. هر نتيجه بدست آمده مي‌تواند به سادگي فرمت، دسته‌بندي و به هر صورتي هدايت شود. بعلاوه، هر يك از داده‌هاي بدست آمده را مي‌توان تغيير داد و در يك فراخواني متد به ديتابيس بازگرداند. پنجره‌ داده تمام مديريت و تراكنش‌هاي اوليه عمده را كنترل مي‌نمايد. پنجره داده نسبت موج ثابت Standing Wave Ratio (SWR) ناهماهنگي امپدانس (impedance) را از 10:1 به چيزي حدود 1.5:1 پايين مي‌آورد. اين كامل نيست، اما نتيجه بخش است.

 

Chase، نسخه1

پس از اينكه خود را راضي كردم كه پنجره داده مي‌تواند يك راه‌حل موثر باشد، شروع به نوشتن چيزي مشابه در جاوا روي  JDBC Database Connectivity)(Java كردم. من يك راه‌حل نسبتا كامل داشتم، اما مشكلاتي وجود داشت. يكي از مزاياي پنجره داده‌ها آن است كه بطور اتوماتيك كليدهاي اصلي و انواع داده‌هاي ستوني را كنترل مي‌نمايد. اين بطور قابل ملاحظه‌اي فشار روي طراح را كم مي‌كند. مشكل اجراي من قابليت استفاده از درايورهاي ديتابيس با كيفيت بالا بود. اجرا بطور زيادي بستگي به فوق‌داده‌هايmetadata  مجموعه نتايج دارد. اين فوق‌داده‌ها مانند هر فوق داده ديگر، از قبيل Java Reflection و XML Schema وقتي دقيق باشند قدرتمند است. مشكل آن است كه بسياري درايورها، حتي درايورهاي با كيفيت بالا تمام فوق‌داده‌هاي لازم را برنمي‌گردانند. در ويژگي JDBC هيچ‌ چيز درايورها را ملزم به انجام چنين كاري نمي‌كند. متدها وجود دارند، اما لزوما براي تمام تركيبهاي درايورها و ديتابيس‌ها كار نمي‌كنند.

طبق آنچه به من گفته شد، هنگام استفاده از ODBC(Open Database Connectivity) درايورهاي محلي، PowerBuilder به مشكلات فوق‌دادهها نيز برمي‌خورد. راه‌حل، اجراي درايورهاي اختصاصي ديتابيس براي هر ديتابيس پشتياني شده جهت تضمين كارآمدي قوي است. من ناآگاهانه زمان زيادي صرف بررسي اين نكته كردم كه چرا برخي درايورها صرفا تمام فوق‌داده‌ها را ارائه ننمودند. پاسخ ساده است: تعداد زيادي ديتابيس و درايور وجود دارند و تعداد اندكي آنها را طراحي مي‌كنند. به احتمال بسيار زياد، فوق‌داده كامل در بالاي ليست اولويتها قرار نمي‌گيرد. پشتيباني 100درصد احتمالا در آينده نزديك بدست نخواهد آمد.

 

Chase، نسخه 2

من تلاش نمودم نيازهاي فوق‌داده‌ را به اميد پشتيباني متداولترين درايورهاي ديتابيس و ديتابيس‌ها تقليل دهم. اين به نظر بهتر آمد، اما نسبت به اجراي اصلي نامناسب‌تر است، و هنوز بعضي چيزها را مورد توجه قرار نمي‌دهد.

نتيجه‌گيري كردم كه شايد خيلي جسور بودم. مثل بقيه، سعي كردم يك مشكل بسيار بزرگ را حل كنم. من به چيزي قابل كنترل‌تر، چيزي ساده عقب‌نشيني نمودم. به اين نتيجه رسيدم آنچه بسيار مي‌پسنديدم امكانات مسيريابي، فرمت كردن، تجزيه و معتبرسازي بود. ايده مديريت كليدهاي اصلي را كنار گذاشتم و آن را به طراح سپردم. نتيجه گرفتم كه در نهايت، مديريت كليدهاي اصلي آنقدرها هم دشوار نيست، كه به نظر من توافقي مناسب است.

نتيجه نهايي، يك راه‌حل تقليل يافته است كه با محدوديتهاي درايورهاي ديتابيس در دسترس قرار ميگيرد، اما هنوز تقريبا كاربرد آن آسان است.

 

مرحله عمل

پيش زمينه كافي است، زمان آن است كه اجرا و چند مثال را ببينم. خواهيم ديد چگونه پكيج ميتواند جهت انجام فعاليتهاي اساسي select/insert/update/delete و نيز فرمت و معتبرسازي داده‌ها به كار رود.

در اينجا يك دياگرام UML ساده شده پكيج اين مقاله مي‌آيد كه مي‌توانيد آن را از بخش منابع دريافت كنيد:

 

دياگرام UML كلاسهای اصلي

 

كلاس Database دو كار انجام مي‌دهد: يك نقشه فرمت كننده‌ها را نگه مي‌دارد و روشهايي جهت select، insert، update، delete سطرها را اجرا مي‌كند. همچنين يك متد Validate() نيز وجود دارد كه من در مورد آن مختصرا صحبت خواهم كرد. Formatter يك كلاس انتزاعي است كه سه روش را مشخص مي‌كند: Parse()، Format() و Validate(). همانگونه كه قبلا اشاره شد، مشاهده داده‌هاي كاربر به شكل رشته‌هايي است. بنابراين، فرمتكننده‌ها مسئول تبديل انواع ديتابيس به رشته‌ها هستند. هنگام وارد كردن داده‌ها، داده‌هاي نامعتبر هميشه امكانپذير است، براي مثال، كاراكترهاي غيرعددي در يك فيلد عددي، تاريخهايي كه به غلط فرمت شده‌اند يا رشته‌هايي كه با يك مدل ناهماهنگ هستند. براي هر يك از انواع داده‌هاي ستوني يك زير مجموعه Formatter موجود است.

بياييد به يك مثال ساده انتخاب داده‌ها نگاه كنيم:

Connection con = getConnection();
myDB = new Database(false);
myDB.loadDefaultFormatters(con, "roles");
String[] params = {"roles.id", "99"};
Results rs = myDB.select(con, "select id,name,role from roles where id = ?", params, 10);
System.out.println("Name is " + rs.getString(0, 1));
System.out.println("Password is " + rs.getString(0, "roles.role"));

 

خط اول يك ارتباط ديتابيس مي‌دهد. فرض مي‌كنم شما روشهايي براي برقراري ارتباط داريد، اكثر برنامه‌هاي كاربردي چنين كاري را انجام مي‌دهند. معمولا اين از يك منبع داده‌ها از پيش مشخص شده يا يك فراخواني ساده به DriverManager بدست مي‌آيد. سازنده Database يك آرگومان Boolean اختيار مي‌كند كه به عنوان مثال مي‌گويد، بطور اتوماتيكي ارتباطها را پس از يك فعاليت ببندد. همانگونه كه احتمالا مي‌دانيد، بستن ارتباطها حياتي و خطاپذير است، بنابراين اگر بخواهيد اتوماتيكي مي‌شود. متدloadDefaultFormatters() يك روش مناسب بارگذاري فرمت كننده‌هاي مناسب براي يك جدول معين است. اين متكي‌بر متدgetColumnClass() در ResultSetMetaData است. ممكن است اين كار نكند، اما بنابر تجربه من، به نظر مي‌رسد اين متد براي تمام ديتابيس‌هايي كه من تست كردم يعني Access، SQL Server، My SQL، Oracle و DB2 عملي باشد. نوع استفاده شما ممكن است تفاوت كند. به جاي آن مي‌توانيد فرمت كننده را از روي اسم براي ستونهاي مشخص تنظيم نماييد. احتمالا مي‌خواهيد اين كار را به هر صورت در توليد انجام دهيد، زيرا فرمت كننده‌هاي پيش‌فرض محدوديتهاي معتبرسازي زيادي ندارند.

اين مثال، يك انتخاب ساده است. توجه كنيد كه SQL چيزي بيش از آنچه ممكن است براي PreparedStatement فراهم كنيد نيست. در واقع، اين دقيقا همان چيزي است كه به شما امكان استفاده از هر SQL كه ديتابيس شما اجرا مي‌كند را مي‌دهد. همچنين، نيازي به يادگيري زبان جستجوي ديگري نداريد. آرگومان Params آرايه‌اي از Strings است كه حاوي اسم ستون و زوجهاي مقادير است. آرايه Params شبيه يك Hash Map مرتب است، اما ساخت و راه‌اندازي آن آسان‌تر است. نام ستون لازم است، بنابراين مقادير پارامتر مي‌تواند بدرستي تجزيه شود. يك فرم اضافي از Select() است كه چنانچه درايور ديتابيس شما بدرستي آن اطلاعات را براي هر ستون برنگرداند يك آرايه رشته‌اي از columnNames را اختيار مي‌كند. اكثر ديتابيس‌ها اين كار را انجام مي‌دهند، اما برخي اينطور نيستند. مواردي نيز هست كه هيچ جدول يا نام ستوني در دسترس نيست، زيرا ممكن است يك عمل يا مقدار ثابت SQL در Select استفاده شود.

متدselect()  يك آبجكت Results را برمي‌گرداند كه شبيه يك ResultSet است، اما نتايج را برمبناي نام ستون فرمت مي‌كند. اگر نام ستون قابل دسترسي نباشد، سعي مي‌كند از نوع ستون استفاده نمايد. مقادير براي يك سطر و ستون مشخص را مي‌توان به هر صورتي بازيابي نمود. توجه كنيد كه ايندكس سطر و ستون برمبناي صفر هستند و از رفتار ResultSet منحرف مي‌شوند. همچنين توجه داشته باشيد كه نام ستون يا شماره ستون را مي‌توان مشخص نمود. چنانچه چيزي اشتباه شود متد Select()، SQLException را كنار مي‌گذارد. Insertها اينگونه صورت مي‌گيرند:

String[] values = {
   "roles.id", "99",
   "roles.name", "ahab",
   "roles.role", "captain"
};
String[] errors = myDB.validate(values);
if (errors != null) {
   for (int i=0; i<errors.length; i+=1) {
      System.out.println(values[2*i] + ": " + errors[i]);
   }
} else {
   myDB.insertRow(con, values);
}

 

اين مثال نشان مي‌دهد چگونه مي‌توان از معتبرسازي استفاده نمود. كلاس Database يك متد Validate() دارد كه آرايه‌اي از مقادير جهت معتبرسازي را اختيار مي‌كند و پيغامهاي خطا، اگر باشند، را برمي‌گرداند. هر مقدار يا فرمتكننده متناظرش چك مي‌شود. اگر هر يك از مقادير معتبر نباشد، آرايه پيامهاي خطاي مشابه را در برخواهد داشت. مقادير بدون خطا يك رشته خالي (معتبر) را در بردارند. يك مقدار برگشت نامعتبر هيچ خطاي معتبرسازي را نشان نمي‌دهد.

متد insertRow() يك ارتباط و مقادير جهت insert را مي‌پذيرد. مقادير به شكل آرايه‌اي از Strings كه حاوي نام ستون و زوجهاي مقادير است فرمت مي‌شوند. اگر چيزي اشتباه شود SQLException كنار گذاشته مي‌شود.

 

يك مثال Update:

String[] values = {
   "roles.id", "99",
   "roles.name", "ahab",
   "roles.role", "fool"
};
String[] params = {"roles.id", "99"};
int n = myDB.updateRows(con, values, "id=?", params);

 

سومين پارامتر مقياس براي يك جمله اختياري WHERE است. اين پارامتر مي‌تواند نامعتبر باشد كه در آن حالت تمام سطرها Update مي‌شوند.

در نهايت، يك مثال delete:

String[] params = {"roles.id", "99"};
String[] errors = myDB.validate(params);
if (errors != null) {
   for (int i=0; i<values.length; i+=1) {
      System.out.println(values[2*i] + ": " + errors[i]);
   }
} else {
   int n = myDB.deleteRows(con, "roles", "id=?", params);
}

توجه كنيد كه معتبرسازي نيز مي‌تواند (و بايد) با پارامترها صورت گيرد.

 

پيشروی

هدف من جايگزين كردن هر چارچوب ديتابيس موجود نبود. اين صرفا تلاشي جهت حل كلاسي از مشكلات با مقادير اندكي كد بود. اين امكان وجود دارد تا اين كلاسها را به عنوان پايه يك چارچوب جامع‌تر به كار برد كه دادهها را Cache و مستقيما باJava Server Pages/Servlets  ادغام مي‌كند.

ساختن taglibs يا مولفه‌هاي اختصاصي براي ساير چارچوبهاي Web از قبيل Tapestry دشوار نخواهد بود.

البته، راه‌حل كامل نيست. راه‌اندازي فرمتكننده‌ها براي هر ستون در هر جدولي كه تصميم به دسترسي به آن را داريد مي‌تواند كار زيادي ببرد. خوشبختانه شما تنها بايد يك بار آن را انجام دهيد. بطور ايدهآل، تمام آن اطلاعات در هنگام تعريف نمودن جدول بايد مشخص شود، اما معمولا اين كار انجام نمي‌شود. اطلاعات را مي‌توان از يك فايل XML بارگذاري كرد يا در خود ديتابيس ذخيره نمود. اين همان كاري است كه PowerBuilder انجام مي‌دهد.

آيا اين به اندازه كافي ساده نيست؟ تصميم‌گيري در مورد آن را به عهده شما مي‌گذارم. امكان دارد كه اين بسيار ساده باشد. برخي ممكن است اين نكته را متذكر شوند كه اين براي استفاده وسيع آماده نيست، ايرادي ندارد. به نظر من، اضافه كردن كارآمدي به يك راه‌حل ساده آسانتر از ساده كردن يك راه‌حل پيچيده است.

 

 

 

منابع

· Download the source code that accompanies this article: http://www.javaworld.com/javaworld/jw-06-2004/sql/jw-0607-sql.zip

· Browse through Elliotte Rusty Harold's collection of quotes: http://www.cafeaulait.org/quotes2004.html

· Sybase PowerBuilder: http://www.sybase.com/products/internetappdevttools/powerbuilder

For more articles on JDBC, browse the Java Database Connectivity (JDBC) section of JavaWorld's Topical Index: http://www.javaworld.com/channel_content/jw-jdbc-index.shtml

  

Copyright 2004 IDG News Service.All right reserved.
Copyright 2004, PC World Iran, All rights reserved.