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. Мені не вдалося встановити обмеження (checks) для таблиці.

  • рядок 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` (LIMI T 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=#значення# </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. вибір S2 з Th2

У кроці 3 Th1 отримує первинний ключ, згенерований під час останнього вставлення, тобто ключ Th2, а не свій власний. Я не знаю, чи метод [insert] класу iBATIS захищений від такого випадку. Припустимо, що він коректно обробляє цю ситуацію. Якщо це не так, нам доведеться похідним класом реалізації [DaoImplCommon] від шару [dao] створити клас [DaoImplMySQL], у якому метод [insertPersonne] буде синхронізовано. Це вирішило б проблему лише для потоків нашої програми. Якщо вищезазначені Th1 і Th2 є потоками двох різних програм, то проблему слід вирішувати одночасно за допомогою транзакцій та відповідного рівня ізоляції (isolation level) між транзакціями. Відповідним був би рівень [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].