Skip to content

19. برنامه وب MVC در معماری سه‌لایه – مثال ۵، MySQL

19.1. پایگاه داده MySQL

در این نسخه، فهرست افراد را در یک جدول پایگاه داده MySQL 4.x ذخیره خواهیم کرد. ما از بسته [Apache – MySQL – PHP] موجود در آدرس URL [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 جامعیت کمتری دارد. من نتوانستم هیچ قید (بررسی) را به جدول اعمال کنم.

  • خط ۱۰: جدول باید از نوع [InnoDB] باشد و نه از نوع [MyISAM] که از تراکنش‌ها پشتیبانی نمی‌کند.
  • خط ۲: کلید اصلی از نوع 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

این پروژه یک پروژه ساده جاوا است، نه یک پروژه وب تامکت.


پوشه [src]


این پوشه حاوی کد منبع لایه‌های [dao] و [service] و همچنین فایل‌های پیکربندی این دو لایه است:

Image

هر فایلی که در نامش «[mysql]» باشد ممکن است برای نسخه‌های Firebird و Postgres تغییر کرده باشد یا نکرده باشد. در ادامه، آن‌هایی را که تغییر کرده‌اند توضیح می‌دهیم.


پوشه [database]


این پوشه حاوی اسکریپت ایجاد پایگاه داده داده‌های شخصی MySQL است:

Image

# EMS MySQL مدیر لایت ۳.۲.۰.۱
# ---------------------------------------
# میزبا    ن : 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` (L IMIT 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 موجود است. تمام این آرشیوها بخشی از پروژه Eclipse با شناسه Classpath را تشکیل می‌دهند.

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 QZXW2HTMLCSuqZQX=#id# و 
        VERSION=#نسخه#</update>
    <!-- حذف یک شخص -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#مقدار# </حذف>
    <!-- بازیابی مقدار کلید اصلی [id] آخرین شخص واردشده -->
    <select id="Personne.getNextId" resultClass="int">select 
        LAST_INSERT_ID()</select>
</sqlMap>

این فایل دارای همان محتوا با [personnes-firebird.xml] است، با تفاوت‌های جزئی زیر:

  • دستور SQL " Personne.insertOne " خطوط ۲۹–۳۷ تغییر کرده است:
  • دستور درج SQL قبل از دستور SELECT اجرا می‌شود که مقدار کلید اصلی ردیف درج‌شده را بازیابی می‌کند
  • دستور درج SQL برای ستون ID در جدول [PERSONNES] هیچ ارزشی ندارد

این بازتاب‌دهنده مثال درج است که در بند 19.1 مورد بحث قرار گرفت.

باید توجه داشت که این می‌تواند منشأ بالقوه‌ای از مشکلات بین نخ‌های همزمان باشد. بیایید دو نخ Th1 و Th2 را تصور کنیم که هم‌زمان عملیات درج را انجام می‌دهند. در مجموع چهار دستور SQL باید صادر شوند. فرض کنید آن‌ها به ترتیب زیر صادر شوند:

  1. توسط Th1 درج I1
  2. توسط Th2 درج I2
  3. انتخاب S1 توسط Th1
  4. انتخاب S2 از Th2

در مرحله ۳، Th1 کلید اصلی تولید شده در آخرین درج را بازیابی می‌کند، یعنی کلید Th2 را به جای کلید خود. نمی‌دانم که آیا متد [insert] از کلاس iBATIS در برابر این سناریو محافظت شده است یا خیر. فرض می‌کنیم که این متد به درستی با آن برخورد می‌کند. اگر این‌طور نبود، لازم بود کلاس پیاده‌سازی [DaoImplCommon] را از لایه [dao] مشتق کنیم و آن را به کلاسی به نام [DaoImplMySQL] تبدیل کنیم که در آن متد [insertPersonne] همگام‌سازی شود. این تنها مشکل رشته‌ها در برنامه ما را حل می‌کند. اگر، همانطور که در بالا ذکر شد، Th1 و Th2 رشته‌هایی از دو برنامه مختلف باشند، در این صورت مشکل باید با استفاده از هر دو تراکنش و یک سطح جداسازی مناسب بین تراکنش‌ها حل شود. سطح ایزولاسیون [serializable]، که در آن تراکنش‌ها گویی به‌صورت متوالی اجرا می‌شوند، مناسب خواهد بود.

شایان ذکر است که این مشکل در Firebird و Postgres وجود ندارد، که SELECT را قبل از INSERT اجرا می‌کنند. برای مثال، اگر ما دنباله زیر را داشته باشیم:

  1. select S1 from Th1
  2. select S2 from Th2
  3. وارد کردن I1 از Th1
  4. وارد کردن I2 از Th2

در مراحل ۱ و ۲، Th1 و Th2 مقادیر کلید اصلی را از یک تولیدکننده یکسان بازیابی می‌کنند. این عملیات معمولاً اتمی است و Th1 و Th2 دو مقدار متفاوت بازیابی خواهند کرد. اگر این عملیات اتمی نبود و Th1 و Th2 دو مقدار یکسان را دریافت می‌کردند، درج انجام‌شده در مرحله ۴ توسط 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>
  • خطوط ۵–۱۹: بیون [dataSource] اکنون به پایگاه داده [MySQL] [dbpersonnes] اشاره می‌کند که مدیر آن [root] با رمز عبور خالی است. خواننده باید این پیکربندی را برای مطابقت با محیط خود تغییر دهد.
  • خط ۳۱: کلاس [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].