Skip to content

19. تطبيق الويب MVC في بنية ثلاثية الطبقات – المثال 5، MySQL

19.1. قاعدة البيانات MySQL

في هذا الإصدار، سنقوم بتثبيت قائمة الأشخاص في جدول قاعدة بيانات MySQL 4.x. استخدمنا الحزمة [Apache – MySQL – PHP] المتوفرة على الرابط [http://www.easyphp.org]. فيما يلي، تأتي لقطات الشاشة من عميل EMS MySQL Manager Lite [http://www.sqlmanager.net/fr/products/mysql/manager]، وهو عميل إدارة مجاني لـ SGBD MySQL.

تسمى قاعدة البيانات [dbpersonnes]. وهي تحتوي على جدول [PERSONNES]:

Image

ستحتوي الجدولة [PERSONNES] على قائمة بالأشخاص الذين تديرهم تطبيق الويب. وقد تم إنشاؤها باستخدام الأوامر التالية SQL:

CREATE TABLE `personnes` (
  `ID` int(11) NOT NULL auto_increment,
  `VERSION` int(11) NOT NULL default '0',
  `NOM` varchar(30) NOT NULL default '',
  `PRENOM` varchar(30) NOT NULL default '',
  `DATENAISSANCE` date NOT NULL default '0000-00-00',
  `MARIE` tinyint(4) NOT NULL default '0',
  `NBENFANTS` int(11) NOT NULL default '0',
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

MySQL يبدو 4.x أقل شمولاً من الأمرين السابقين SGBD. لم أتمكن من وضع قيود (فحوصات) على الجدول.

  • السطر 10: يجب أن يكون نوع الجدول [InnoDB] وليس النوع [MyISAM] الذي لا يدعم المعاملات.
  • السطر 2: المفتاح الأساسي من النوع auto_increment. إذا أدخلنا سطراً بدون قيمة للعمود ID في الجدول، فسيقوم MySQL تلقائياً بإنشاء رقم صحيح لهذا العمود. وهذا سيوفر علينا عناء إنشاء المفاتيح الأساسية بأنفسنا.

قد تحتوي الجدولة [PERSONNES] على المحتوى التالي:

Image

نحن نعلم أنه عند إدراج كائن [Personne] بواسطة طبقتنا [dao]، فإن الحقل [id] لهذا الكائن يساوي -1 قبل الإدراج ويكون له قيمة مختلفة عن -1 بعد ذلك، وهذه القيمة هي المفتاح الأساسي المخصص للسطر الجديد الذي تم إدراجه في الجدول [PERSONNES]. لنرى من خلال مثال كيف يمكننا معرفة هذه القيمة.

الطلب SQL

SELECT LAST_INSERT_ID()

معرفة آخر قيمة تم إدخالها في الحقل ID في الجدول. ويجب إصدارها بعد الإدخال. وهذا يختلف عن أوامر SGBD و [Firebird] و [Postgres] حيث كنا نطلب قيمة المفتاح الأساسي للشخص المضاف قبل الإدراج. سنستخدمها في الملف [personnes-mysql.xml] الذي يجمع الأوامر SQL الصادرة على قاعدة البيانات.

19.2. مشروع Eclipse للطبقات [dao] و [service]

لتطوير الطبقات [dao] و [service] لتطبيقنا باستخدام قاعدة البيانات MySQL، سنستخدم مشروع Eclipse [mvc-personnes-05] التالي:

Image

المشروع هو مشروع Java بسيط، وليس مشروع ويب Tomcat.


مجلد [src]


يحتوي هذا المجلد على أكواد المصدر للطبقات [dao] و [service] بالإضافة إلى ملفات التكوين لهاتين الطبقتين:

Image

قد تكون جميع الملفات التي تحتوي على [mysql] في اسمها قد خضعت أو لم تخضع لتعديل فيما يتعلق بإصدارات Firebird و Postgres. وفيما يلي، نصف تلك التي تم تعديلها.


المجلد [database]


يحتوي هذا المجلد على البرنامج النصي لإنشاء قاعدة بيانات MySQL للأشخاص:

Image

# EMS MySQL Manager Lite 3.2.0.1
# ---------------------------------------
# المضيف     : localhost
# المنفذ     : 3306
# قاعدة البيانات : dbpersonnes


SET FOREIGN_KEY_CHECKS=0;

CREATE DATABASE `dbpersonnes`
    CHARACTER SET 'latin1'
    COLLATE 'latin1_swedish_ci';

USE `dbpersonnes`;

#
# هيكل جدول `personnes`: 
#

CREATE TABLE `personnes` (
  `ID` int(11) NOT NULL auto_increment,
  `VERSION` int(11) NOT NULL default '0',
  `NOM` varchar(30) NOT NULL default '',
  `PRENOM` varchar(30) NOT NULL default '',
  `DATENAISSANCE` date NOT NULL default '0000-00-00',
  `MARIE` tinyint(4) NOT NULL default '0',
  `NBENFANTS` int(11) NOT NULL default '0',
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

#
# بيانات جدول `personnes`  (LIMIT 0,500)
#

INSERT INTO `personnes` (`ID`, `VERSION`, `NOM`, `PRENOM`, `DATENAISSANCE`, `MARIE`, `NBENFANTS`) VALUES 
  (1,1,'Major','Joachim','1984-01-13',1,2),
  (2,1,'Humbort','Mélanie','1985-01-12',0,1),
  (3,1,'Lemarchand','Charles','1986-01-01',0,0);

COMMIT;

المجلد [lib]


يحتوي هذا الملف على الأرشيفات اللازمة للتطبيق:

تجدر الإشارة إلى وجود برنامج التشغيل jdbc الخاص بـ SGBD و MySQL. جميع هذه الملفات المضغوطة هي جزء من Classpath التابع لمشروع Eclipse.

19.3. الطبقة [dao]

الطبقة [dao] هي التالية:

Image

نقدم فقط التغييرات التي طرأت مقارنة بالإصدار [Firebird].

ملف التعيين [personne-mysql.xml] هو التالي:


<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">

<sqlMap>
    <!-- اسم مستعار لفئة [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- جدول التعيين [PERSONNES] - الكائن [Personne] -->
    <resultMap id="Personne.map" 
        class="istia.st.mvc.personnes.entites.Personne">
        <result property="id" column="ID" />
        <result property="version" column="VERSION" />
        <result property="nom" column="NOM"/>
        <result property="prenom" column="PRENOM"/>
        <result property="dateNaissance" column="DATENAISSANCE"/>
        <result property="marie" column="MARIE"/>
        <result property="nbEnfants" column="NBENFANTS"/>
    </resultMap>
    <!-- قائمة بجميع الأشخاص -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
    <!-- الحصول على شخص معين -->
        <select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#القيمة#</select>
    <!-- إضافة شخص -->
    <insert id="Personne.insertOne" parameterClass="Personne.classe">
        insert into 
        PERSONNES(VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#الإصدار#، #اللقب#، #الاسم الأول#، #dateNaissance#، #ماري#، 
        #nbEnfants#) 
        <selectKey keyProperty="id">
            select LAST_INSERT_ID() as value
        </selectKey>         
    </insert>
    <!-- تحديث شخص -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#الإصدار#+1، NOM=#اللقب#، PRENOM=#الاسم الأول#، DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#، NBENFANTS=#nbEnfants# WHERE ID=#id# و 
        VERSION=#version#</update>
    <!-- حذف شخص -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#value# </delete>
    <!-- الحصول على قيمة المفتاح الأساسي [id] لآخر شخص تم إدراجه -->
    <select id="Personne.getNextId" resultClass="int">select 
        LAST_INSERT_ID()</select>
</sqlMap>

وهو نفس محتوى [personnes-firebird.xml] باستثناء التفاصيل التالية:

  • تغير ترتيب SQL " Personne.insertOne " في الأسطر 29-37:
  • يتم تنفيذ أمر الإدراج SQL قبل الأمر SELECT الذي سيسمح باسترداد قيمة المفتاح الأساسي للسطر الذي تم إدراجه
  • أمر الإدراج SQL ليس له قيمة في العمود ID من الجدول [PERSONNES]

وهذا يعكس مثال الإدراج الذي ناقشناه في الفقرة 19.1.

تجدر الإشارة إلى أنه قد يكون هناك مصدر محتمل للمشاكل بين الخيوط المتنافسة. لنفترض وجود خيطين Th1 و Th2 يقومان بإدراج في نفس الوقت. هناك ما مجموعه أربعة أوامر SQL يجب إصدارها. لنفترض أنها تتم بالترتيب التالي:

  1. إدراج I1 من Th1
  2. إدراج I2 من Th2
  3. اختيار S1 من Th1
  4. select S2 من Th2

في الخطوة 3، يسترد Th1 المفتاح الأساسي الذي تم إنشاؤه عند آخر عملية إدراج، أي مفتاح Th2 وليس مفتاحه الخاص. لا أعرف ما إذا كانت الطريقة [insert] الخاصة بـ iBATIS محمية في هذه الحالة. سنفترض أنها تتعامل معها بشكل صحيح. إذا لم يكن الأمر كذلك، فسيتعين علينا اشتقاق فئة التنفيذ [DaoImplCommon] من الطبقة [dao] إلى فئة [DaoImplMySQL] حيث يتم مزامنة الطريقة [insertPersonne]. وهذا لن يحل المشكلة إلا بالنسبة لخيوط تطبيقنا. إذا كان Th1 و Th2 أعلاه خيوطين لتطبيقين مختلفين، فسيكون من الضروري حل المشكلة باستخدام المعاملات ومستوى عزل مناسب بين المعاملات. سيكون المستوى [serializable]، الذي يتم فيه تنفيذ المعاملات كما لو كانت تُنفذ بالتسلسل، مناسبًا.

تجدر الإشارة إلى أن هذه المشكلة لا توجد مع Firebird و Postgres اللذين يقومان بتنفيذ SELECT قبل INSERT. على سبيل المثال، إذا كان لدينا التسلسل التالي:

  1. select S1 من Th1
  2. select S2 من Th2
  3. إدراج I1 من Th1
  4. إدراج I2 من Th2

في الخطوتين 1 و 2، يسترد Th1 و Th2 قيم المفتاح الأساسي من نفس المولد. هذه العملية عادةً ما تكون ذرية، وسيسترد Th1 و Th2 قيمتين مختلفتين. إذا لم تكن العملية ذرية، واسترد Th1 و Th2 قيمتين متطابقتين، فإن الإدراج الذي يتم في الخطوة 4 بواسطة Th2 سيفشل بسبب تكرار المفتاح الأساسي. هذا خطأ يمكن استرداده تمامًا ويمكن لـ Th2 إعادة محاولة الإدراج.

سنترك العملية "Personne.insertOne" كما هي حاليًا في الملف [personnes-mysql.xml]، ولكن يجب أن يدرك القارئ أن هناك مشكلة محتملة هنا.

فئة التنفيذ [DaoImplCommon] للطبقة [dao] هي نفسها الموجودة في الإصدارين السابقين.

تم تكييف تكوين الطبقة [dao] مع SGBD [MySQL]. وبالتالي، فإن ملف التكوين [spring-config-test-dao-mysql.xml] هو التالي:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- مصدر البيانات DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>com.mysql.jdbc.Driver</value>
        </property>
        <property name="url">
            <value>jdbc:mysql://localhost/dbpersonnes</value>
        </property>
        <property name="username">
            <value>root</value>
        </property>
        <property name="password">
            <value></value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-mysql.xml</value>
        </property>
    </bean>
    <!-- فئة الوصول إلى الطبقة [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • الأسطر 5-19: يشير bean [dataSource] الآن إلى قاعدة [MySQL] [dbpersonnes] التي يكون مسؤولها هو [root] بدون كلمة مرور. سيقوم القارئ بتعديل هذا التكوين وفقًا لبيئته الخاصة.
  • السطر 31: الفئة [DaoImplCommon] هي فئة تنفيذ الطبقة [dao]

بعد إجراء هذه التعديلات، يمكننا الانتقال إلى الاختبارات.

19.4. اختبارات الطبقات [dao] و [service]

اختبارات الطبقات [dao] و [service] هي نفسها التي أجريت على الإصدار [Firebird]. النتائج التي تم الحصول عليها هي كما يلي:

يلاحظ أن الاختبارات قد نجحت مع التنفيذ [DaoImplCommon]. لن نضطر إلى اشتقاق هذه الفئة كما كان ضروريًا مع SGBD [Firebird].

19.5. اختبارات تطبيق [web]

لاختبار تطبيق الويب باستخدام SGBD و [MySQL]، نقوم بإنشاء مشروع Eclipse [mvc-personnes-05B] بطريقة مشابهة لتلك المستخدمة لإنشاء مشروع [mvc-personnes-03B] باستخدام قاعدة بيانات Firebird (انظر الفقرة 17.7). ومع ذلك، كما هو الحال مع Postgres، لا يتعين علينا إعادة إنشاء الأرشيفات [personnes-dao.jar] و [personnes-service.jar] لأننا لم نقم بتعديل أي فئة.

نقوم بنشر مشروع الويب [mvc-personnes-05B] داخل Tomcat:

يتم تشغيل SGBD MySQL. ويكون محتوى الجدول [PERSONNES] كما يلي:

Image

يتم تشغيل Tomcat بدوره. باستخدام متصفح، نطلب عنوان URL [http://localhost:8080/mvc-personnes-05B]:

Image

نضيف شخصًا جديدًا عبر الرابط [Ajout]:

نتحقق من الإضافة في قاعدة البيانات:

Image

يُطلب من القارئ إجراء اختبارات أخرى [modification, suppression].