Skip to content

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

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

في هذا الإصدار، سنقوم بتخزين قائمة الأشخاص في جدول قاعدة بيانات MySQL 4.x. استخدمنا حزمة [Apache – MySQL – PHP] المتوفرة على الرابط [http://www.easyphp.org]. وفيما يلي، لقطات الشاشة مأخوذة من عميل MySQL Manager Lite [http://www.sqlmanager.net/fr/products/mysql/manager]، وهو عميل إدارة مجاني لنظام إدارة قواعد البيانات 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 أقل قدرة من نظامي إدارة قواعد البيانات السابقين. لم أتمكن من إضافة قيود (فحوصات) إلى الجدول.

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

يمكن أن يحتوي الجدول [PERSONNES] على المحتوى التالي:

Image

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

عبارة SQL

SELECT LAST_INSERT_ID()

يسمح لنا بتحديد آخر قيمة تم إدخالها في حقل ID بالجدول. يجب تنفيذه بعد الإدخال. وهذا يختلف عن أنظمة إدارة قواعد البيانات [Firebird] و[Postgres]، حيث طلبنا قيمة المفتاح الأساسي للشخص المضاف قبل الإدخال. سنستخدمه في ملف [people-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
# ---------------------------------------
# Host     : localhost
# Port     : 3306
# Database : dbpersonnes


SET FOREIGN_KEY_CHECKS=0;

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

USE `dbpersonnes`;

#
# Structure for the `personnes` table : 
#

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;

#
# Data for the `personnes` table  (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 لنظام إدارة قواعد البيانات MySQL. جميع هذه الملفات هي جزء من مسار فئات مشروع Eclipse.

19.3. طبقة [dao]

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

Image

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

ملف التعيين [person-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>
    <!-- alias class [Person] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- mapping table [PERSONNES] - object [Person] -->
    <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>
    <!-- list of all persons -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
    <!-- get a specific person -->
        <select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#value#</select>
    <!-- add a person -->
    <insert id="Personne.insertOne" parameterClass="Personne.classe">
        insert into 
        PERSONNES(VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#version#, #nom#, #prenom#, #dateNaissance#, #marie#, 
        #nbEnfants#) 
        <selectKey keyProperty="id">
            select LAST_INSERT_ID() as value
        </selectKey>         
    </insert>
    <!-- update a person -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#version#+1, NOM=#nom#, PRENOM=#prenom#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# and 
        VERSION=#version#</update>
    <!-- delete a person -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#value# </delete>
    <!-- obtain the value of the primary key [id] of the last person inserted -->
    <select id="Personne.getNextId" resultClass="int">select 
        LAST_INSERT_ID()</select>
</sqlMap>

هذا هو نفس محتوى [people-firebird.xml] مع الاختلافات الطفيفة التالية:

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

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

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

  1. إدراج I1 بواسطة Th1
  2. insert I2 بواسطة Th2
  3. select 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 from Th1
  2. SELECT S2 من Th2
  3. insert I1 from Th1
  4. insert I2 from Th2

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

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

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

تم تكييف تكوين طبقة [dao] مع نظام إدارة قواعد البيانات [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>
    <!-- data source 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>
    <!-- the [dao] layer access classes -->
    <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]. لن نحتاج إلى اشتقاق هذه الفئة كما كان ضروريًا مع نظام إدارة قواعد البيانات [Firebird].

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

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

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

يتم تشغيل نظام إدارة قواعد البيانات MySQL. ويكون محتوى جدول [PERSONNES] كما يلي:

Image

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

Image

نقوم بإضافة شخصًا جديدًا باستخدام رابط [Add]:

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

Image

ندعو القارئ إلى إجراء اختبارات أخرى [تحرير، حذف].