Skip to content

1. Вступ до ORM NHibernate

PDF цього документа доступний |TYT|.

Приклади з цього документа доступні за посиланням |TYT|.

Цей документ є коротким вступом до NHibernate — аналога фреймворка Java Hibernate для .NET. Для отримання повного вступу можна ознайомитися з:


Назва: «NHibernate in Action», Автор: П’єр-Анрі Куате, Видавництво: Manning, ISBN-13: 978-1932394924


ORM (об’єктно-реляційний мапер) — це набір бібліотек, що дозволяє програмі, яка працює з базою даних, використовувати її без видачі явних SQL-команд і без знання особливостей використовуваної SGBD.


Необхідні умови


У структурі [débutant-intermédiaire-avancé] цей документ знаходиться в розділі [intermédiaire]. Для його розуміння необхідні певні попередні знання, які можна знайти в деяких документах, написаних мною:

  1. Мова C# 2008: [Опанування мови C# версії 3.0 з використанням .NET Framework 3.5 (2008)]
  1. [Spring IoC для .NET (2005)], доступний за посиланням [Spring IoC pour .NET ]. У ньому викладено основи інверсії контролю (Inversion of Control) або ін’єкції залежностей (Dependency Injection) фреймворку Spring.Net [Spring.NET | Homepage ].

На початку деяких абзаців цього документа іноді наводяться рекомендації щодо додаткового читання. Вони посилаються на попередні документи.


Інструменти


Інструменти, використані в цьому прикладі, є у вільному доступі в Інтернеті. Це такі (грудень 2011 року):

  • Nhibernate 3.2, доступний за адресою [http://nhforge.org/Default.aspx]
  • Spring.net 1.3.2 доступний за адресою [http://www.springframework.net]. Фреймворк Spring.net має дуже широкі можливості. Тут ми використовуватимемо лише бібліотеку, яку він надає для спрощення роботи з фреймворком Nhibernate.
  • Log4net 1.2.10 доступний за посиланням [http://logging.apache.org/log4net]. Цей фреймворк для ведення журналів використовується Nhibernate.
  • Nunit 2.5 доступний за адресою [http://www.nunit.org/]. Цей фреймворк для модульних тестів є еквівалентом для .NET фреймворку JUnit для платформи Java.
  • Драйвер ADO.NET 6.4.4 для SGBD MySQL 5 доступний за адресою [http://dev.mysql.com/downloads/connector/net]

Усі файли DLL, необхідні для проектів Visual Studio 2010, зібрано в папці [libnet4]:

 

1.1. Місце NHIBERNATE у багаторівневій архітектурі .NET

Додаток .NET, що використовує базу даних, може бути побудований за багаторівневою архітектурою наступним чином:

Рівень [dao] взаємодіє з SGBD через API та ADO.NET. Нагадаємо основні методи цього API.

У режимі підключення додаток:

  1. відкриває з’єднання з джерелом даних
  2. працює з джерелом даних у режимі читання/запису
  3. закриває з’єднання

Ці операції переважно стосуються трьох інтерфейсів ADO.NET:

  • IDbConnection, що інкапсулює властивості та методи з’єднання.
  • IDbCommand, що інкапсулює властивості та методи виконаного запиту SQL.
  • IDataReader, що інкапсулює властивості та методи результату запиту SQL Select.

Інтерфейс IDbConnection

використовується для управління з’єднанням із базою даних. Серед методів M та властивостей P цього інтерфейсу є такі:

Назва
Тип
Роль
ConnectionString
P
рядок підключення до бази даних. Він визначає всі параметри, необхідні для встановлення з'єднання з конкретною базою даних.
Open
M
відкриває з’єднання з базою даних, визначеною параметром ConnectionString
Close
M
закриває з’єднання
BeginTransaction
M
починає транзакцію.
State
P
стан з'єднання: ConnectionState.Closed, ConnectionState.Open, ConnectionState.Connecting, ConnectionState.Executing, ConnectionState.Fetching, ConnectionState.Broken

Якщо Connection є класом, що реалізує інтерфейс IDbConnection, відкриття з'єднання можна здійснити наступним чином:

1
2
3
IDbConnection connexion=new Connection();
connexion.ConnectionString=...;
connexion.Open();

Інтерфейс IDbCommand

використовується для виконання команди SQL або збереженої процедури. Серед методів M та властивостей P цього інтерфейсу є такі:

Назва
Тип
Роль
CommandType
P
вказує, що потрібно виконати — значення береться з переліку:
- CommandType.Text: виконує команду SQL, визначену у властивості CommandText. Це значення за замовчуванням.
- CommandType.StoredProcedure: виконує процедуру, збережену в базі даних
CommandText
P
- текст команди SQL, яку слід виконати, якщо CommandType = CommandType.Text
- ім’я збереженої процедури, яку слід виконати, якщо CommandType = CommandType.StoredProcedure
Connection
P
з'єднання IDbConnection, яке слід використовувати для виконання команди SQL
Transaction
P
транзакція IDbTransaction, в якій слід виконати команду SQL
Parameters
P
список параметрів налаштованого запиту SQL. Запит update articles set price=price*1.1 where id=@id має параметр @id.
ExecuteReader
M
для виконання запиту SQL Select. Отримуємо об’єкт IDataReader, що представляє результат запиту Select.
ExecuteNonQuery
M
для виконання команди SQL «Оновити, Вставити, Видалити». Отримується кількість рядків, на які вплинула операція (оновлені, вставлені, видалені).
ExecuteScalar
M
для виконання команди SQL. Команда Select повертає лише один результат, як у запиті: select count(*) from articles.
CreateParameter
M
для створення параметрів IDbParameter для налаштованого запиту SQL.
Prepare
M
дозволяє оптимізувати виконання параметризованого запиту, коли він виконується багато разів з різними параметрами.

Якщо Command є класом, що реалізує інтерфейс IDbCommand, виконання команди SQL без транзакції матиме такий вигляд:

// вхід у систему 
IDbConnection connexion=...
connexion.Open();
// підготовка запиту
IDbCommand commande=new Command();
commande.Connection=connexion;
// виконання команди select
commande.CommandText="select ...";
IDbDataReader reader=commande.ExecuteReader();
...
// виконання команди update, insert, delete
commande.CommandText="insert ...";
int nbLignesInsérées=commande.ExecuteNonQuery();
...
// закриття з'єднання
connexion.Close();

Інтерфейс IDataReader

використовується для інкапсуляції результатів виконання команди SQL Select. Об’єкт IDataReader представляє таблицю зі рядками та стовпцями, які обробляються послідовно: спочатку перший рядок, потім другий, ... Серед методів M та властивостей P цього інтерфейсу є такі:

Назва
Тип
Роль
FieldCount
P
кількість стовпців у таблиці IDataReader
GetName
M
GetName(i) повертає назву стовпця № i таблиці IDataReader.
Item
P
Item[i] представляє стовпець № i поточного рядка таблиці IDataReader.
Read
M
переходить до наступного рядка таблиці IDataReader. Повертає логічне значення True, якщо зчитування відбулося успішно, і False в іншому випадку.
Close
M
закриває таблицю IDataReader.
GetBoolean
M
GetBoolean(i): повертає булеве значення стовпця № i поточного рядка таблиці IDataReader. Інші аналогічні методи такі: GetDateTime, GetDecimal, GetDouble, GetFloat, GetInt16, GetInt32, GetInt64, GetString.
Getvalue
M
Getvalue(i): повертає значення стовпця № i поточного рядка таблиці IDataReader у вигляді типу object.
IsDBNull
M
IsDBNull(i) повертає True, якщо стовпець № i поточного рядка таблиці IDataReader не має значення, що позначається значенням SQL NULL.

Використання об’єкта IDataReader часто виглядає наступним чином:

// відкриття з'єднання 
IDbConnection connexion=...
connexion.Open();
// підготовка команди
IDbCommand commande=new Command();
commande.Connection=connexion;
// виконання команди select
commande.CommandText="select ...";
IDataReader reader=commande.ExecuteReader();
// обробка результатів
while(reader.Read()){
     // обробка поточного рядка
        ...
}
// закриття зчитувача
reader.Close();
// закриття з'єднання
connexion.Close();

У попередній архітектурі,

коннектор [ADO.NET] пов'язаний із SGBD. Отже, клас, що реалізує інтерфейс [IDbConnection], — це:

  • клас [MySQLConnection] для SGBD MySQL
  • клас [SQLConnection] для SGBD та SQLServer

Таким чином, шар [dao] залежить від використовуваного SGBD. Деякі фреймворки (Linq, Ibatis.net, NHibernate) усувають це обмеження, додаючи додатковий рівень між рівнем [dao] та коннектором [ADO.NET] використовуваного SGBD. Тут ми будемо використовувати фреймворк [NHibernate].

У наведеному вище прикладі рівень [dao] більше не звертається до з’єднувача [ADO.NET], а до фреймворку NHibernate, який надасть йому інтерфейс, незалежний від використовуваного з’єднувача [ADO.NET]. Така архітектура дозволяє замінити SGBD без зміни шару [dao]. У цьому випадку потрібно змінити лише з’єднувач [ADO.NET].

1.2. Приклад бази даних

Щоб продемонструвати, як працювати з NHibernate, ми використаємо таку базу даних MySQL [dbpam_nhibernate]:

  • У [1] база даних містить три таблиці:
    • [employes]: таблиця, що містить дані про працівниць дитячого садка
    • [cotisations]: таблиця, що містить ставки соціальних внесків
    • [indemnites]: таблиця, що містить інформацію для розрахунку заробітної плати працівниць

Таблиця [employes]

  • у [2] — таблиця співробітників, а в [3] — значення її полів

Вміст таблиці може бути таким:

 

Таблиця [cotisations]

  • у [4] — таблиця внесків, а в [5] — значення її полів

Зміст таблиці може бути таким:

 

Таблиця [indemnites]

  • у [6] — таблиця виплат, а в [7] — значення її полів

Зміст таблиці може бути таким:

 

Експорт структури бази даних у файл SQL дає такий результат:

#
# Структура таблиці `cotisations`: 
#

CREATE TABLE `cotisations` (
  `ID` bigint(20) NOT NULL auto_increment,
  `SECU` double NOT NULL,
  `RETRAITE` double NOT NULL,
  `CSGD` double NOT NULL,
  `CSGRDS` double NOT NULL,
  `VERSION` int(11) NOT NULL,
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;

#
# Структура таблиці `indemnites`: 
#

CREATE TABLE `indemnites` (
  `ID` bigint(20) NOT NULL auto_increment,
  `ENTRETIEN_JOUR` double NOT NULL,
  `REPAS_JOUR` double NOT NULL,
  `INDICE` int(11) NOT NULL,
  `INDEMNITES_CP` double NOT NULL,
  `BASE_HEURE` double NOT NULL,
  `VERSION` int(11) NOT NULL,
  PRIMARY KEY  (`ID`),
  UNIQUE KEY `INDICE` (`INDICE`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;

#
# Структура таблиці `employes`: 
#

CREATE TABLE `employes` (
  `ID` bigint(20) NOT NULL auto_increment,
  `PRENOM` varchar(20) NOT NULL,
  `SS` varchar(15) NOT NULL,
  `ADRESSE` varchar(50) NOT NULL,
  `CP` varchar(5) NOT NULL,
  `VILLE` varchar(30) NOT NULL,
  `NOM` varchar(30) NOT NULL,
  `VERSION` int(11) NOT NULL,
  `INDEMNITE_ID` bigint(20) NOT NULL,
  PRIMARY KEY  (`ID`),
  UNIQUE KEY `SS` (`SS`),
  KEY `FK_EMPLOYES_INDEMNITE_ID` (`INDEMNITE_ID`),
  CONSTRAINT `FK_EMPLOYES_INDEMNITE_ID` FOREIGN KEY (`INDEMNITE_ID`) REFERENCES `indemnites` (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=latin1;

Зверніть увагу, що в рядках 6, 20 та 36 первинні ключі ID мають атрибут autoincrement. Це означає, що MySQL автоматично генеруватиме значення первинних ключів під час кожного додавання запису. Розробнику не потрібно про це турбуватися.

1.3. Демонстраційний проект на C#

Для ознайомлення з налаштуванням та використанням NHibernate ми використаємо таку архітектуру:

Консольна програма [1] оброблятиме дані з попередньої бази даних [2] за допомогою фреймворку [NHibernate] [3]. Це дасть нам змогу представити:

  • конфігураційні файли NHibernate
  • API на основі NHibernate

Проект на C# матиме такий вигляд:

Необхідні для проекту елементи такі:

  • у [1] — файли DLL, необхідні для проекту:
    • [NHibernate]: DLL фреймворку NHibernate
    • [MySql.Data]: DLL з'єднувача ADO.NET фреймворку SGBD MySQL
    • [log4net]: DLL фреймворку Log4net, що дозволяє генерувати журнали
  • у форматі [2], класи зображень таблиць бази даних
  • у форматі [3], файл [App.config], який налаштовує весь додаток, включаючи фреймворк [NHibernate]
  • у [4], а також консольні тестові додатки

1.3.1. Налаштування з’єднання з базою даних

Повернемося до архітектури тестування:

Як показано вище, [NHibernate] повинен мати доступ до бази даних. Для цього йому потрібна певна інформація:

  • SGBD, який управляє базою даних (MySQL, SQLServer, Postgres, Oracle тощо). Більшість SGBD додали до мови SQL власні розширення. Знаючи SGBD, NHibernate може адаптувати команди SQL, які він надсилає до цього SGBD. NHibernate використовує поняття діалекту SQL.
  • параметри підключення до бази даних (назва бази, ім’я користувача, що володіє підключенням, його пароль)

Цю інформацію можна розмістити у файлі конфігурації [App.config]. Ось приклад файлу, який буде використовуватися з базою даних MySQL 5:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <!-- розділи конфігурації -->
    <configSections>
        <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
        <section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
    </configSections>


    <!-- конфігурація NHibernate -->
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
        <session-factory>
            <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
            <!--
            <property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
            -->
            <property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
            <property name="connection.connection_string">
                Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
            </property>
            <property name="show_sql">false</property>
            <mapping assembly="pam-nhibernate-demos"/>
        </session-factory>
    </hibernate-configuration>

    <!-- Цей розділ містить параметри конфігурації log4net -->
    <!-- NOTE IMPORTANTE: журнали за замовчуванням неактивні. Їх потрібно активувати програмно за допомогою команди log4net.Config.XmlConfigurator.Configure();
    ! -->
    <log4net>
        <!-- Визначте модуль виведення (куди будуть надходити журнали) -->
        <appender name="LogFileAppender" type="log4net.Appender.FileAppender, log4net">
            <param name="File" value="log.txt" />
            <param name="AppendToFile" value="false" />
            <layout type="log4net.Layout.PatternLayout, log4net">
                <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] &lt;%X{auth}&gt; - %m%n" />
            </layout>
        </appender>
        <appender name="LogDebugAppender" type="log4net.Appender.DebugAppender, log4net">
            <layout type="log4net.Layout.PatternLayout, log4net">
                <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] &lt;%X{auth}&gt; - %m%n"/>
            </layout>
        </appender>
        <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender, log4net">
            <layout type="log4net.Layout.PatternLayout, log4net">
                <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] &lt;%X{auth}&gt; - %m%n"/>
            </layout>
        </appender>

        <!-- Налаштування кореневої категорії, встановлення рівня пріоритету за замовчуванням та додавання модулів виведення (куди будуть надходити журнали) -->
        <root>
            <priority value="INFO" />
            <!--
            <appender-ref ref="LogFileAppender" />
            <appender-ref ref="LogDebugAppender"/>
            -->
            <appender-ref ref="ConsoleAppender"/>
        </root>

        <!-- Вказати рівень для деяких конкретних просторів імен -->
        <!-- Рівень може бути: ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF -->
        <logger name="NHibernate">
            <level value="INFO" />
        </logger>
    </log4net>
</configuration>
  • рядки 4–7: визначають розділи конфігурації у файлі [App.config]. Розглянемо рядок 6:

<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />

Цей рядок визначає розділ конфігурації NHibernate у файлі [App.config]. Він має два атрибути: name та type.

  • Атрибут [name] позначає розділ конфігурації. Цей розділ має бути обмежений тегами <name>...</name>, а саме <hibernate-configuration>...</hibernate-configuration> у рядках 11–24.
  • Атрибут [type=classe,DLL] вказує ім’я класу, відповідального за обробку розділу, визначеного атрибутом [name], а також DLL, що містить цей клас. У даному випадку клас називається [NHibernate.Cfg.ConfigurationSectionHandler] і знаходиться в DLL [NHibernate.dll]. Нагадаємо, що цей DLL входить до списку посилань досліджуваного проєкту.

Тепер розглянемо розділ конфігурації NHibernate:


    <!-- конфігурація NHibernate -->
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
        <session-factory>
            <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
            <!--
            <property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
            -->
            <property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
            <property name="connection.connection_string">
                Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
            </property>
            <property name="show_sql">false</property>
            <mapping assembly="pam-nhibernate-demos"/>
        </session-factory>
</hibernate-configuration>
  • рядок 2: конфігурація NHibernate знаходиться всередині тегу <hibernate-configuration>. Атрибут xmlns (Xml NameSpace) визначає версію, яка використовується для конфігурації NHibernate. Дійсно, з часом спосіб налаштування NHibernate змінився. Тут використовується версія 2.2.
  • рядок 3: конфігурація NHibernate повністю міститься в тезі <session-factory> (рядки 3 та 14). Сесія NHibernate — це інструмент, що використовується для роботи з базою даних за схемою:
    • відкриття сесії
    • робота з базою даних за допомогою методів API NHibernate
    • закриття сесії

Сесія створюється за допомогою factory — загального терміна, що позначає клас, здатний створювати об’єкти. Рядки 3–14 налаштовують цей factory.

  • рядки 4, 6, 8, 9: налаштовують з’єднання з цільовою базою даних. Основна інформація — це ім’я використовуваного SGBD, ім’я бази даних, ідентифікатор користувача та його пароль.
  • рядок 4: визначає постачальника з’єднання — того, до якого надсилається запит на з’єднання з базою даних. Значенням властивості [connection.provider] є ім’я класу NHibernate. Ця властивість не залежить від використовуваного SGBD.
  • рядок 6: драйвер ADO.NET, який слід використовувати. Це ім’я класу NHibernate, спеціалізованого для певного SGBD, у даному випадку MySQL. Рядок 6 винесено в коментарі, оскільки він не є обов’язковим.
  • рядок 8: властивість [dialect] визначає діалект SQL, який слід використовувати з SGBD. Тут це діалект SGBD MySQL.

Якщо змінити SGBD, як знайти діалект NHibernate для нього? Повернімося до попереднього проєкту на C# і двічі клацнімо на DLL [NHibernate] у вкладці [References]:

  • у [1], на вкладці [Explorateur d'objets] відображається певна кількість DLL, серед яких є ті, на які посилається проект.
  • у [2], DLL [NHibernate]
  • у [3] — DLL та [NHibernate], що були розроблені. У них містяться різні простори імен (namespace), які там визначені.
  • у [4] — простір імен [NHibernate.Dialect], де містяться класи, що визначають різні діалекти SQL, які можна використовувати.
  • у [5] — клас діалекту SGBD MySQL 5.
  • у [6] — простір імен класу [MySqlDataDriver], що використовується у рядку 6 нижче:

    <!-- конфігурація NHibernate -->
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
        <session-factory>
            <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
            <!--
            <property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
            -->
            <property name="dialect">NHibernate.Dialect.MySQLDialect</property>
            <property name="connection.connection_string">
                Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
            </property>
            <property name="show_sql">false</property>
            <mapping assembly="pam-nhibernate-demos"/>
        </session-factory>
</hibernate-configuration>
  • рядки 9–11: рядок підключення до бази даних. Цей рядок має вигляд «param1=val1;param2=val2; ...». Набір параметрів, визначених таким чином, дозволяє драйверу SGBD встановити з’єднання. Формат цього рядка підключення залежить від використовуваного драйвера SGBD. Рядки підключення до основних драйверів SGBD можна знайти на сайті [http://www.connectionstrings.com/]. У даному випадку рядок «Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;» є рядком підключення для SGBD MySQL. Він вказує, що:
    • Server=localhost;: SGBD знаходиться на тій самій машині, що й клієнт, який намагається встановити з’єднання
    • Database=dbpam_nhibernate; : цільова база даних MySQL
    • Uid=root; : користувач, який відкриває з’єднання, — це користувач root
    • Pwd=;: цей користувач не має пароля (особливий випадок у цьому прикладі)
  • рядок 12: властивість [show_sql] вказує, чи повинен NHibernate відображати у своїх журналах команди SQL, які він надсилає до бази даних. На етапі розробки корисно встановити для цього об’єкта значення [true], щоб точно знати, що саме робить NHibernate.
  • рядок 13: щоб зрозуміти тег <mapping>, повернемося до архітектури додатка:

Якби консольна програма була прямим клієнтом коннектора ADO.NET і хотіла отримати список співробітників, вона б змусила коннектор виконати команду SQL Select, і отримала б у відповідь об’єкт типу IDataReader, який їй довелося б обробити, щоб отримати список співробітників, який спочатку був потрібний.

У наведеному вище прикладі консольна програма є клієнтом NHibernate, а NHibernate є клієнтом коннектора ADO.NET. Пізніше ми побачимо, що API від NHibernate дозволить консольній програмі запросити список співробітників. NHibernate перетворить цей запит на команду SQL Select, яку він передасть для виконання коннектору ADO.NET. Останній поверне об’єкт типу IDataReader. На основі цього об’єкта Nhibernate має змогу сформувати запитуваний список співробітників. Це стає можливим завдяки відповідній конфігурації. Кожній таблиці бази даних відповідає клас C#. Таким чином, на основі рядків таблиці [employes], повернутих IDataReader, NHibernate зможе сформувати список об’єктів, що представляють співробітників, і передати його консольній програмі. Ці зв’язки «таблиці <--> класи» створюються у файлах конфігурації. NHibernate використовує термін «mapping» для визначення цих зв’язків.

Повернемося до рядка 13 нижче:


    <!-- конфігурація NHibernate -->
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
        <session-factory>
            <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
            <!--
            <property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
            -->
            <property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
            <property name="connection.connection_string">
                Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
            </property>
            <property name="show_sql">false</property>
            <mapping assembly="pam-nhibernate-demos"/>
        </session-factory>
</hibernate-configuration>

Рядок 13 вказує, що конфігураційні файли «таблиці <--> класи» будуть розміщені в збірці [pam-nhibernate-demos]. Збірка — це виконуваний файл або DLL, отриманий у результаті компіляції проєкту. У цьому випадку файли зіставлення будуть розміщені в збірці прикладу проекту. Щоб дізнатися назву цієї збірки, потрібно переглянути властивості проекту:

  • у [1], властивості проєкту
  • на вкладці [Application] [2] — назва збірки [3], яка буде згенерована.
  • оскільки тип виводу — [Application console] [4], файл, що генерується під час компіляції проєкту, матиме назву [pam-nhibernate-demos.exe]. Якби тип виводу був [Bibliothèque de classes] [5], файл, згенерований під час компіляції проєкту, називався б [pam-nhibernate-demos.dll]
  • асемблерний файл генерується у папці [bin/Release] проекту [6].

З попереднього пояснення слід винести, що файли таблиць відповідності <--> класів повинні міститися у файлі [pam-nhibernate-demos.exe] [6].

1.3.2. Налаштування файлу « » для відповідності таблиць <--> класів

Повернемося до архітектури розглянутого проєкту:

  • у [1] консольна програма використовує методи з API фреймворку NHibernate. Ці два блоки обмінюються об’єктами.
  • У [2], NHibernate використовує API з'єднувача .NET. Він надсилає команди SQL до цільового SGBD.

Консольна програма буде працювати з об’єктами, що відображають таблиці бази даних. У цьому проєкті ці об’єкти та зв’язки, що поєднують їх із таблицями бази даних, розміщено в папці [Entites] нижче:

 
  • кожна таблиця бази даних відповідає одному класу та файлу, що встановлює зв’язок між ними
Таблиця
Клас
Зв’язок
внески
Cotisations.cs
Cotisations.hbm.xml
працівники
Employe.cs
Employe.hbm.xml
виплати
Indemnites.cs
Indemnites.hbm.xml

1.3.2.1. Зв'язки таблиці [cotisations]

Розглянемо таблицю [cotisations]:

ID
первинний ключ типу autoincrement
VERSION
номер версії запису
SECU
ставка (у відсотках) страхового внеску на соціальне страхування
RETRAITE
ставка внеску на пенсійне забезпечення
CSGD
ставка відрахувань на загальний соціальний внесок, що підлягає вирахуванню
CSGRDS
ставка внеску на загальний соціальний внесок та внесок на погашення соціального боргу

Один рядок цієї таблиці може бути інкапсульований в об’єкт типу [Cotisations.cs] наступним чином:


namespace PamNHibernateDemos {
    public class Cotisations {
        // автоматичні властивості
        public virtual int Id { get; set; }
        public virtual int Version { get; set; }
        public virtual double CsgRds { get; set; }
        public virtual double Csgd { get; set; }
        public virtual double Secu { get; set; }
        public virtual double Retraite { get; set; }

        // виробники
        public Cotisations() {
        }
        // ToString
        public override string ToString() {
            return string.Format("[{0}|{1}|{2}|{3}]", CsgRds, Csgd, Secu, Retraite);
        }
    }

}

Для кожного стовпця таблиці [cotisations] створено автоматичне властивість. Кожна з цих властивостей має бути оголошена віртуальною (virtual), оскільки NHibernate буде похідним від цього класу і перевизначатиме (override) його властивості. Тому ці властивості мають бути віртуальними.

Зверніть увагу, що у рядку 1 зазначено, що клас належить до простору імен [PamNHibernateDemos].

Файл маппінгу [Cotisations.hbm.xml], що пов’язує таблицю [cotisations] із класом [Cotisations], має такий вигляд:


<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="PamNHibernateDemos" assembly="pam-nhibernate-demos">
    <class name="Cotisations" table="COTISATIONS">
        <id name="Id" column="ID" unsaved-value="0">
            <generator class="native" />
        </id>
        <version name="Version" column="VERSION"/>
        <property name="CsgRds" column="CSGRDS"/>
        <property name="Csgd" column="CSGD"/>
        <property name="Retraite" column="RETRAITE"/>
        <property name="Secu" column="SECU"/>
    </class>
</hibernate-mapping>
  • файл маппінгу — це XML-файл, визначений у тезі <hibernate-mapping> (рядки 2 та 14)
  • рядок 4: тег <class> встановлює зв’язок між таблицею бази даних і класом. У цьому випадку — між таблицею [COTISATIONS] (атрибут table) і класом [Cotisations] (атрибут name). У файлі .NET клас має бути визначений за його повним іменем (включно з простором імен) та за збіркою, яка його містить. Ці дві інформації наводяться у рядку 3. Першу (простір імен) можна знайти у визначенні класу. Друга (збірка) — це назва збірки проекту. Ми вже вказували, як знайти цю назву.
  • рядки 5–7: тег <id> використовується для визначення відповідності первинного ключа таблиці [cotisations].
    • рядок 5: атрибут name позначає поле класу [Cotisations], яке прийматиме первинний ключ таблиці [cotisations]. Атрибут column позначає стовпець таблиці [cotisations], який слугує первинним ключем. Атрибут unsaved-value слугує для визначення первинного ключа, який ще не згенеровано. Це значення дозволяє NHibernate визначити, як зберегти об’єкт [Cotisations] у таблиці [cotisations]. Якщо цей об’єкт має поле Id=0, він виконає операцію SQL INSERT, інакше — операцію SQL UPDATE. Значення unsaved-value залежить від типу поля Id класу [Cotisations]. У даному випадку його тип — int, а значення за замовчуванням для типу int дорівнює 0. Отже, об’єкт [Cotisations], який ще не збережено (а отже, не має первинного ключа), матиме поле Id=0. Якби поле Id було типу Object або похідним від нього, ми б написали unsaved-value=null.
    • рядок 6: коли NHibernate має зберегти об’єкт [Cotisations] з полем Id=0, він повинен виконати в базі даних операцію INSERT, під час якої має отримати значення первинного ключа запису. Більшість SGBD мають власний метод автоматичного генерування цього значення. Тег <generator> слугує для визначення механізму, який слід використовувати для генерування первинного ключа. Тег <generator class="native"> вказує, що слід використовувати механізм за замовчуванням, який застосовується у SGBD. У розділі 1.2 ми бачили, що первинні ключі наших трьох таблиць MySQL мали атрибут autoincrement. Під час виконання операцій INSERT та NHibernate не надаватимуть значення стовпцю ID доданого запису, залишаючи MySQL генерувати це значення.
  • рядок 8: тег <version> використовується для визначення стовпця таблиці (а також відповідного поля класу), який дозволяє «версіювати» записи. Спочатку значення версії дорівнює 1. Воно збільшується під час кожної операції UPDATE. З іншого боку, будь-яка операція UPDATE або DELETE виконується з фільтром WHERE ID= id AND VERSION=v1. Отже, користувач може змінити або видалити об’єкт лише в тому випадку, якщо він має правильну версію цього об’єкта. Якщо це не так, NHibernate генерує виняток.
  • рядок 9: тег <property> використовується для визначення звичайного зіставлення стовпців (ані первинного ключа, ані стовпця версії). Таким чином, рядок 9 вказує, що стовпець CSGRDS таблиці [COTISATIONS] пов’язаний із властивістю CsgRds класу [Cotisations].

1.3.2.2. Зв’язки таблиці [indemnites]

Розглянемо таблицю [indemnites]:

ID
первинний ключ типу autoincrement
VERSION
номер версії запису
BASE_HEURE
вартість однієї години чергування в євро
ENTRETIEN_JOUR
добова надбавка за чергування в євро
REPAS_JOUR
добова надбавка на харчування в євро за день чергування
INDEMNITES_CP
допомога за оплачувану відпустку. Це відсоток, який застосовується до базової заробітної плати.

Один рядок цієї таблиці може бути інкапсульований в об’єкт типу [Indemnites] наступним чином:


namespace PamNHibernateDemos {
    public class Indemnites {

        // автоматичні властивості
        public virtual int Id { get; set; }
        public virtual int Version { get; set; }
        public virtual int Indice { get; set; }
        public virtual double BaseHeure { get; set; }
        public virtual double EntretienJour { get; set; }
        public virtual double RepasJour { get; set; }
        public virtual double IndemnitesCp { get; set; }

        // конструктори
        public Indemnites() {
        }

        // ідентичність
        public override string ToString() {
            return string.Format("[{0}|{1}|{2}|{3}|{4}]", Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
        }

    }
}

Файл відповідності таблиці [indemnites] <--> клас [Indemnites] може мати такий вигляд (Indemnites.hbm.xml):


<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="PamNHibernateDemos" assembly="pam-nhibernate-demos">
    <class name="Indemnites" table="INDEMNITES">
        <id name="Id" column="ID" unsaved-value="0">
            <generator class="native" />
        </id>
        <version name="Version" column="VERSION"/>
        <property name="Indice" column="INDICE" unique="true"/>
        <property name="BaseHeure" column="BASE_HEURE" />
        <property name="EntretienJour" column="ENTRETIEN_JOUR" />
        <property name="RepasJour" column="REPAS_JOUR" />
        <property name="IndemnitesCp" column="INDEMNITES_CP" />
    </class>
</hibernate-mapping>

Тут немає нічого нового порівняно з файлом маппінгу, описаним раніше. Єдина відмінність знаходиться у рядку 9. Атрибут unique="true" вказує, що в таблиці [indemnites] існує обмеження унікальності для стовпця [INDICE]: не може бути двох рядків з однаковим значенням у стовпці [INDICE].

1.3.2.3. Маппінг таблиці [employes]

Розглянемо таблицю [employes]:

ID
первинний ключ типу autoincrement
VERSION
номер версії запису
PRENOM
ім'я працівника
NOM
його прізвище
ADRESSE
його адреса
CP
його поштовий індекс
VILLE
його місто
INDEMNITE_ID
зовнішній ключ на INDEMNITES(ID)

Відмінність від попередніх таблиць полягає у наявності зовнішнього ключа: стовпець [INDEMNITE_ID] є зовнішнім ключем до стовпця [ID] таблиці [INDEMNITES]. Це поле посилається на рядок таблиці [INDEMNITES], який використовується для розрахунку виплат працівнику.

Клас [Employe] , що відображає таблицю [employes], може мати такий вигляд:


namespace PamNHibernateDemos {
    public class Employe {
        // автоматичні властивості
        public virtual int Id { get; set; }
        public virtual int Version { get; set; }
        public virtual string SS { get; set; }
        public virtual string Nom { get; set; }
        public virtual string Prenom { get; set; }
        public virtual string Adresse { get; set; }
        public virtual string Ville { get; set; }
        public virtual string CodePostal { get; set; }
        public virtual Indemnites Indemnites { get; set; }

        // конструктори
        public Employe() {
        }

        // ToString
        public override string ToString() {
            return string.Format("[{0}|{1}|{2}|{3}|{4}|{5}|{6}]", SS, Nom, Prenom, Adresse, Ville, CodePostal, Indemnites);
        }
    }
}

Файл маппінгу [Employe.hbm.xml] може мати такий вигляд:


<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="PamNHibernateDemos" assembly="pam-nhibernate-demos">
    <class name="Employe" table="EMPLOYES">
        <id name="Id" column="ID" unsaved-value="0">
            <generator class="native" />
        </id>
        <version name="Version" column="VERSION"/>
        <property name="SS" column="SS"/>
        <property name="Nom" column="NOM"/>
        <property name="Prenom" column="PRENOM"/>
        <property name="Adresse" column="ADRESSE"/>
        <property name="Ville" column="VILLE"/>
        <property name="CodePostal" column="CP"/>
        <many-to-one name="Indemnites" column="INDEMNITE_ID" cascade="save-update" lazy="false"/>
    </class>
</hibernate-mapping>

Нововведення полягає у рядку 15, де з’являється новий тег: <many-to-one>. Цей тег використовується для відображення стовпця зовнішнього ключа [INDEMNITE_ID] з таблиці [EMPLOYES] на властивість [Indemnites] класу [Employe]:


namespace PamNHibernateDemos {
    public class Employe {
        // автоматичні властивості
..
        public virtual Indemnites Indemnites { get; set; }

...
    }
}

Таблиця [EMPLOYES] має зовнішній ключ [INDEMNITE_ID], який посилається на стовпець [ID] таблиці [INDEMNITES]. Кілька (many) записів таблиці [EMPLOYES] можуть посилатися на один (one) запис таблиці [INDEMNITES]. Звідси й походить назва тегу <many-to-one>. Цей тег має тут такі атрибути:

  • column: вказує ім’я стовпця таблиці [EMPLOYES], який є зовнішнім ключем у таблиці [INDEMNITES]
  • name: вказує властивість класу [Employe], пов’язану з цим стовпцем. Тип цієї властивості обов’язково є класом, пов’язаним із цільовою таблицею зовнішнього ключа, у даному випадку — таблицею [INDEMNITES]. Відомо, що цим класом є вже описаний клас [Indemnites]. Саме це відображено у рядку 5 вище. Це означає, що коли NHibernate поверне з бази даних об’єкт [Employe], він також поверне об’єкт [Indemnites], який до нього належить.
  • каскад: цей атрибут може мати різні значення:
    • save-update: операція вставки (save) або оновлення (update) об’єкта [Employe] має бути поширена на об’єкт [Indemnites], який він містить.
    • delete: видалення об’єкта [Employe] має бути поширене на об’єкт [Indemnites], який він містить.
    • all: поширює операції вставки (save), оновлення (update) та видалення (delete).
    • none: нічого не поширює

Наостанок нагадаємо конфігурацію NHibernate у файлі [App.config]:


    <!-- конфігурація NHibernate -->
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
        <session-factory>
            <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
            <!--
            <property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
            -->
            <property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
            <property name="connection.connection_string">
                Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
            </property>
            <property name="show_sql">false</property>
            <mapping assembly="pam-nhibernate-demos"/>
        </session-factory>
</hibernate-configuration>

У рядку 13 вказано, що файли маппінгу *.hbm.xml будуть знаходитися в збірці [pam-nhibernate-demos]. Це не робиться за замовчуванням. Це потрібно налаштувати в проєкті C#:

  • у [1] вибираємо властивості файлу маппінгу
  • в [2], дія генерації повинна бути [Ressource incorporée] [3]. Це означає, що під час генерації проєкту файл маппінгу має бути включений до згенерованої збірки.

1.4. API з NHibernate

Повернемося до архітектури нашого прикладу проекту:

У попередніх параграфах ми налаштували NHibernate двома способами:

  • у [App.config] ми налаштували з’єднання з базою даних
  • для кожної таблиці бази даних ми вказали відповідний клас та файл маппінгу, який дозволяє перетворювати клас у таблицю і навпаки.

Нам залишається ознайомитися з методами, які пропонує NHibernate для роботи з даними бази: вставка, оновлення, видалення, перелік.

1.4.1. Об’єкт SessionFactory

Усі операції NHibernate виконуються в межах сеансу. Типова послідовність операцій NHibernate виглядає так:

  • відкриття сеансу NHibernate
  • початок транзакції в сесії
  • виконання операцій збереження даних у сесії (Load, Get, Find, CreateQuery, Save, SaveOrUpdate, Delete)
  • зафіксувати (commit) або скасувати (rollback) транзакцію
  • закрити сесію NHibernate

Сесію отримують із фабрики типу [SessionFactory]. Ця фабрика налаштовується за допомогою тегу <session-factory> у файлі конфігурації [App.config]:


    <!-- налаштування NHibernate -->
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
        <session-factory>
            <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
            <!--
            <property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
            -->
            <property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
            <property name="connection.connection_string">
                Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
            </property>
            <property name="show_sql">false</property>
            <mapping assembly="pam-nhibernate-demos"/>
        </session-factory>
</hibernate-configuration>

У коді C# SessionFactory можна отримати таким чином:


ISessionFactory sessionFactory = new Configuration().Configure().BuildSessionFactory();

Клас Configuration є класом фреймворку NHibernate. Попередня інструкція використовує розділ конфігурації NHibernate у [App.config]. Отриманий об’єкт [ISessionFactory] містить:

  • інформацію для встановлення з’єднання з цільовою базою даних
  • файли зіставлення між таблицями бази даних та постійними класами, з якими працює NHibernate.

1.4.2. Сесія NHibernate

Після створення SessionFactory (це робиться лише один раз) можна отримати сесії, що дозволяють виконувати операції збереження даних NHibernate. Типовий код виглядає так:


try{
      // вхід у систему 
      using (ISession session = sessionFactory.OpenSession())
      {
        // початок транзакції
        using (ITransaction transaction = session.BeginTransaction())
        {
........................ opérations de persistance
          // підтвердження транзакції
          transaction.Commit();
        }
      }
}catch (Exception ex){
....
}
  • рядок 3: сесія створюється на основі SessionFactory у блоці using. Після виходу з блоку using сесія буде автоматично закрита. Без блоку using сесію потрібно було б закрити явно (session.Close()).
  • рядок 6: операції збереження даних будуть виконуватися в межах транзакції. Або всі вони завершаться успішно, або жодна з них не завершиться успішно. Усередині блоку using, транзакція підтверджується за допомогою Commit (рядок 10). Якщо в транзакції операція збереження даних викликає виняток, транзакція автоматично буде скасована за допомогою Rollback після виходу з using.
  • Блок try/catch у рядках 1 і 13 дозволяє перехопити можливе виключення, згенероване кодом усередині блоку try (сесія, транзакція, збереження даних).

1.4.3. Інтерфейс ISession

Тепер розглянемо деякі методи інтерфейсу ISession, реалізованого сесією NHibernate:

ITransaction BeginTransaction()
запускає транзакцію в сесії
ITransaction tx=session.BeginTransaction();
void Clear()
очищає сесію. Об’єкти, що містилися в ній, стають від’єднаними.
session.Clear();
void Close()
закриває сесію. Об’єкти, що містилися в ній, синхронізуються з базою даних. Ця операція синхронізації також виконується наприкінці транзакції. Останній випадок є найпоширенішим.
session.Close();
IQuery CreateQuery(string queryString)
створює запит HQL (Hibernate Query Language) для подальшого виконання.
IQuery query=session.createQuery("select e from Employe e);
void Delete(object obj)
видаляє об’єкт. Цей об’єкт може належати до сеансу (прив’язаний) або не належати (від’єднаний). Під час синхронізації сеансу з базою даних щодо цього об’єкта буде виконано операцію SQL DELETE.
// завантажуємо співробітника з BD
Співробітник e = session.Get<Співробітник>(143);
// видаляємо його
session.Delete(e);
void Flush()
примусово синхронізує сесію з базою даних. Вміст сесії не змінюється.
session.Flush();
T Get<T>(object id)
виконує пошук у базі даних об’єкта T з первинним ключем id. Якщо цей об’єкт не існує, повертає покажчик null.
// завантажуємо співробітника за допомогою BD
Співробітник e = session.Get<Співробітник>(143);
object Save(object obj)
додає об’єкт obj до сесії. Цей об’єкт не має первинного ключа до виконання Save. Після Save він його отримує. Під час синхронізації сесії в базі даних буде виконано операцію SQL INSERT.
// створюємо співробітника
Співробітник e = new Співробітник(){...};
// зберігаємо його
e = session.Save(e);
SaveOrUpdate(object obj)
виконується операція Save, якщо об’єкт не має первинного ключа, або операція Update, якщо він уже має такий ключ.
void Update(object obj)
оновлює об’єкт obj у базі даних. Після цього у базі виконується операція SQL UPDATE.
// завантажуємо співробітника з BD
Співробітник e = session.Get<Співробітник>(143);
// змінюємо його ім’я
e.Nom = ...;
// оновлюємо його в базі даних
session.Update(e);

1.4.4. Інтерфейс IQuery

Інтерфейс IQuery дозволяє виконувати запити до бази даних для вилучення даних. Ми вже бачили, як створити його екземпляр:

IQuery query=session.createQuery("select e from Employe e);

Параметром методу createQuery є запит HQL (Hibernate Query Language) — мова, аналогічна мові SQL, але яка звертається до класів, а не до таблиць. Наведений вище запит вимагає переліку всіх співробітників. Ось кілька прикладів запитів HQL:

select e from Employe e where e.Nom like 'A%'
select e from Employe order by e.Nom asc
select e from Employe e where e.Indemnites.Indice=2

Тепер розглянемо деякі методи інтерфейсу IQuery:

IList<T> List<T>()
повертає результат запиту у вигляді списку об’єктів T
IList<Співробітник> співробітники = session.createQuery("select e from Співробітник e order by e.Nom asc").List<Співробітник>();
IList List()
повертає результат запиту у вигляді списку, де кожен елемент списку представляє рядок результату запиту Select у вигляді масиву об’єктів.
IList рядки = session.createQuery("select e.Nom, e.Prenom, e.SS from Співробітник").List();
lignes[i][j] представляє стовпець j рядка i у типі object. Таким чином, lignes[10][1] — це тип object, що представляє ім’я особи. Зазвичай для отримання даних у їхньому точному типі необхідні перетворення типів.
T UniqueResult<T>()
повертає перший об’єкт із результату запиту
Employe e=session.createQuery("select e from Employe e where e.Nom='MARTIN'").UniqueResult<Employe>();

Запит HQL можна налаштувати:

1
2
3
string numSecu;
...
Employe e=session.createQuery("select e from Employe e where e.SS=:num").SetString("num",numSecu).UniqueResult<Employe>();

У запиті HQL у рядку 3 :num є параметром, якому потрібно присвоїти значення перед виконанням запиту. У наведеному вище прикладі для цього використовується метод SetString. Інтерфейс IQuery має різні методи Set для присвоєння значення параметру:

  • - SetBoolean(string name, bool value)
  • - SetSingle(рядок name, числове значення)
  • - SetDouble(рядок name, значення типу double)
  • - SetInt32(ім'я-рядок, значення типу int32)
  • ..

1.5. Декілька прикладів коду

Наведені нижче приклади базуються на архітектурі, яку ми розглядали раніше та нагадуємо нижче. База даних — це база даних MySQL [dbpam_nhibernate], яка також була представлена. Приклади — це консольні програми [1], що використовують фреймворк NHibernate [3] для роботи з базою даних [2].

Проект на C#, до якого входять наведені нижче приклади, — це той самий, що вже був представлений:

  • у [1], DLL, необхідні для проекту:
    • [NHibernate]: DLL фреймворку NHibernate
    • [MySql.Data]: DLL для коннектора ADO.NET фреймворку SGBD MySQL 5
    • [log4net]: DLL інструменту, що дозволяє генерувати журнали
  • у форматі [2], класи зображень таблиць бази даних
  • у форматі [3], файл [App.config], який налаштовує весь додаток, включаючи фреймворк [NHibernate]
  • у форматі [4], а також консольні тестові програми. Саме їх ми частково й розглянемо.

1.5.1. Отримання вмісту бази даних

Програма [ShowDataBase.cs] дозволяє відобразити вміст бази даних:


using System;
using System.Collections;
using System.Collections.Generic;
using NHibernate;
using NHibernate.Cfg;


namespace PamNHibernateDemos
{
  public class ShowDataBase
  {

    private static ISessionFactory sessionFactory = null;

    // головна програма
    static void Main(string[] args)
    {
      // ініціалізація фабрики NHibernate
      sessionFactory = new Configuration().Configure().BuildSessionFactory();
      try
      {
        // виведення вмісту бази даних
        Console.WriteLine("Affichage base -------------------------------------");
        ShowDataBase1();
      }
      catch (Exception ex)
      {
        // виводиться повідомлення про виняток 
        Console.WriteLine(string.Format("L'erreur suivante s'est produite : [{0}]", ex.ToString()));
      }
      finally
      {
        if (sessionFactory != null)
        {
          sessionFactory.Close();
        }
      }
      // очікування введення з клавіатури
      Console.ReadLine();
    }

    // тест1
    static void ShowDataBase1()
    {
      // вхід у систему 
      using (ISession session = sessionFactory.OpenSession())
      {
        // початок транзакції
        using (ITransaction transaction = session.BeginTransaction())
        {
          // отримано список співробітників
          IList<Employe> employes = session.CreateQuery(@"select e from Employe e order by e.Nom asc").List<Employe>();
          // виводимо його
          Console.WriteLine("--------------- liste des employés");
          foreach (Employe e in employes)
          {
            Console.WriteLine(e);
          }
          // отримано список надбавок
          IList<Indemnites> indemnites = session.CreateQuery(@"select i from Indemnites i order by i.Indice asc").List<Indemnites>();
          // виведення на екран
          Console.WriteLine("--------------- liste des indemnités");
          foreach (Indemnites i in indemnites)
          {
            Console.WriteLine(i);
          }
          // отримуємо список внесків
          Cotisations cotisations = session.CreateQuery(@"select c from Cotisations c").UniqueResult<Cotisations>();
          Console.WriteLine("--------------- tableau des taux de cotisations");
          Console.WriteLine(cotisations);
          // фіксація транзакції
          transaction.Commit();
        }
      }
    }
  }
}

Пояснення:

  • рядок 19: створюється об’єкт SessionFactory. Саме він дозволить нам отримати об’єкти Session.
  • рядок 24: відображається вміст бази даних
  • рядки 31–37: об’єкт SessionFactory закривається у клаузулі finally об’єкта try.
  • рядок 43: метод, що виводить вміст бази
  • рядок 46: отримуємо Session з SessionFactory.
  • рядок 49: запускається транзакція
  • рядок 52: запит HQL для отримання списку співробітників. Завдяки зовнішньому ключу, що пов'язує суть Employe із суттю Indemnite, для кожного співробітника буде вказано його компенсацію.
  • рядок 60: запит HQL для отримання списку виплат.
  • рядок 68: запит HQL для отримання єдиного рядка з таблиці внесків.
  • рядок 72: кінець транзакції
  • рядок 73: кінець блоку using Itransaction з рядка 49 — транзакція автоматично закривається
  • рядок 74: кінець блоку using Isession з рядка 46 — сесія автоматично закривається.

Отримане зображення на екрані:

Affichage base -------------------------------------
--------------- liste des employés
[254104940426058|Jouveinal|Marie|5 rue des oiseaux|St Corentin|49203|[1|1,93|2|3|12]]
[260124402111742|Laverti|Justine|La Brûlerie|St Marcel|49014|[2|2,1|2,1|3,1|15]]

--------------- liste des indemnités
[1|1,93|2|3|12]
[2|2,1|2,1|3,1|15]
--------------- tableau des taux de cotisations
[3,49|6,15|9,39|7,88]

Зверніть увагу на рядки 3 і 4: при запиті про співробітника також було отримано інформацію про його компенсацію.

1.5.2. Введення даних у базу

Програма [FillDataBase.cs] дозволяє вводити дані в базу:


using System;
using System.Collections;
using System.Collections.Generic;
using NHibernate;
using NHibernate.Cfg;


namespace PamNHibernateDemos
{
  public class FillDataBase
  {

    private static ISessionFactory sessionFactory = null;

    // головна програма
    static void Main(string[] args)
    {
      // ініціалізація фабрики NHibernate
      sessionFactory = new Configuration().Configure().BuildSessionFactory();
      try
      {
        // видалення вмісту бази даних
        Console.WriteLine("Effacement base -------------------------------------");
        ClearDataBase1();
        Console.WriteLine("Affichage base -------------------------------------");
        ShowDataBase();
        Console.WriteLine("Remplissage base -------------------------------------");
        FillDataBase1();
        Console.WriteLine("Affichage base -------------------------------------");
        ShowDataBase();
      }
      catch (Exception ex)
      {
        // виведення повідомлення про виняток 
        Console.WriteLine(string.Format("L'erreur suivante s'est produite : [{0}]", ex.ToString()));
      }
      finally
      {
        if (sessionFactory != null)
        {
          sessionFactory.Close();
        }
      }
      // очікування введення з клавіатури
      Console.ReadLine();
    }

    // test1
    static void ShowDataBase()
    {
             // див. попередній приклад
    }

    // ClearDataBase1
    static void ClearDataBase1()
    {
      // вхід у систему 
      using (ISession session = sessionFactory.OpenSession())
      {
        // початок транзакції
        using (ITransaction transaction = session.BeginTransaction())
        {
          // отримання списку співробітників
          IList<Employe> employes = session.CreateQuery(@"select e from Employe e").List<Employe>();
          // видалення всіх співробітників
          Console.WriteLine("--------------- suppression des employés associés");
          foreach (Employe e in employes)
          {
            session.Delete(e);
          }
          // отримання списку виплат
          IList<Indemnites> indemnites = session.CreateQuery(@"select i from Indemnites i").List<Indemnites>();
          // видалення виплат
          Console.WriteLine("--------------- suppression des indemnités");
          foreach (Indemnites i in indemnites)
          {
            session.Delete(i);
          }
          // отримуємо список внесків
          Cotisations cotisations = session.CreateQuery(@"select c from Cotisations c").UniqueResult<Cotisations>();
          Console.WriteLine("--------------- suppression des taux de cotisations");
          if (cotisations != null)
          {
            session.Delete(cotisations);
          }
          // фіксація транзакції
          transaction.Commit();
        }
      }
    }

    // FillDataBase
    static void FillDataBase1()
    {
      // вхід у систему 
      using (ISession session = sessionFactory.OpenSession())
      {
        // початок транзакції
        using (ITransaction transaction = session.BeginTransaction())
        {
          // створено дві компенсації
          Indemnites i1 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
          Indemnites i2 = new Indemnites() { Id = 0, Indice = 2, BaseHeure = 2.1, EntretienJour = 2.1, RepasJour = 3.1, IndemnitesCp = 15 };
          // створено двох працівників
          Employe e1 = new Employe() { Id = 0, SS = "254104940426058", Nom = "Jouveinal", Prenom = "Marie", Adresse = "5 rue des oiseaux", Ville = "St Corentin", CodePostal = "49203", Indemnites = i1 };
          Employe e2 = new Employe() { Id = 0, SS = "260124402111742", Nom = "Laverti", Prenom = "Justine", Adresse = "La Brûlerie", Ville = "St Marcel", CodePostal = "49014", Indemnites = i2 };
          // створено ставки внесків
          Cotisations cotisations = new Cotisations() { Id = 0, CsgRds = 3.49, Csgd = 6.15, Secu = 9.39, Retraite = 7.88 };
          // збереження всіх даних
          session.Save(e1);
          session.Save(e2);
          session.Save(cotisations);
          // фіксація транзакції
          transaction.Commit();
        }
      }
    }

  }
}

Пояснення

  • рядок 19: створюється SessionFactory
  • рядки 37–43: вона закривається у клаузулі finally програми try
  • рядок 55: метод ClearDataBase1, який очищає базу даних. Принцип такий:
    • витягуються всі співробітники (рядок 64) у список
    • видаляємо їх по одному (рядки 67–70)
  • рядок 93: метод FillDataBase1 вставляє деякі дані в базу даних
  • створюються два об’єкти Indemnites (рядки 102, 103)
  • створюються два співробітники з цими надбавками (рядки 105, 106)
  • у рядку 108 створюється об’єкт Cotisations.
  • рядки 110, 111: обидва об’єкти «Співробітник» зберігаються в базі даних
  • рядок 112: об’єкт «Внески» також зберігається
  • Може викликати подив, що сутності Indemnités у рядках 102 та 103 не були збережені. Насправді вони були збережені одночасно із сутностями Employe. Щоб це зрозуміти, слід повернутися до маппінгу елемента Employe:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="PamNHibernateDemos" assembly="pam-nhibernate-demos">
    <class name="Employe" table="EMPLOYES">
        <id name="Id" column="ID" unsaved-value="0">
            <generator class="native" />
        </id>
        <version name="Version" column="VERSION"/>
        <property name="SS" column="SS"/>
        <property name="Nom" column="NOM"/>
        <property name="Prenom" column="PRENOM"/>
        <property name="Adresse" column="ADRESSE"/>
        <property name="Ville" column="VILLE"/>
        <property name="CodePostal" column="CP"/>
        <many-to-one name="Indemnites" column="INDEMNITE_ID" cascade="save-update" lazy="false"/>
    </class>
</hibernate-mapping>

У рядку 15, який відображає зв’язок із зовнішнім ключем між сутностями Employe та Indemnites, вказано атрибут cascade= "save-update", що призводить до того, що операції "save » та «update» сутності Employe поширюються на внутрішню сутність Indemnites.

Отримане зображення екрана:

Effacement base -------------------------------------
--------------- suppression des employés et des indemnités associées
--------------- suppression des indemnités restantes
--------------- suppression des taux de cotisations
Affichage base -------------------------------------
--------------- liste des employés
--------------- liste des indemnités
--------------- tableau des taux de cotisations

Remplissage base -------------------------------------
Affichage base -------------------------------------
--------------- liste des employés
[254104940426058|Jouveinal|Marie|5 rue des oiseaux|St Corentin|49203|[2|2,1|2,1|3,1|15]]
[260124402111742|Laverti|Justine|La Brûlerie|St Marcel|49014|[1|1,93|2|3|12]]
--------------- liste des indemnités
[1|1,93|2|3|12]
[2|2,1|2,1|3,1|15]
--------------- tableau des taux de cotisations
[3,49|6,15|9,39|7,88]

1.5.3. Пошук співробітника

Програма [Program.cs] містить різні методи, що ілюструють доступ до даних бази та роботу з ними. Ми наведемо декілька з них.

Метод [FindEmployee] дозволяє знайти працівника за номером соціального страхування:


// FindEmployee
    static void FindEmployee() {
      try {
        // вхід у систему 
        using (ISession session = sessionFactory.OpenSession()) {
          // початок транзакції
          using (ITransaction transaction = session.BeginTransaction()) {
            // пошук співробітника за номером SS
            String numSecu = "254104940426058";
            IQuery query = session.CreateQuery(@"select e from Employe e where e.SS=:numSecu");
            Employe employe = query.SetString("numSecu", numSecu).UniqueResult<Employe>();
            if (employe != null) {
              Console.WriteLine("Employe[" + numSecu + "]=" + employe);
            } else {
              Console.WriteLine("Employe[" + numSecu + "] non trouvé...");
            }

            numSecu = "xx";
            employe = query.SetString("numSecu", numSecu).UniqueResult<Employe>();
            if (employe != null) {
              Console.WriteLine("Employe[" + numSecu + "]=" + employe);
            } else {
              Console.WriteLine("Employe[" + numSecu + "] non trouvé...");
            }

            // фіксація транзакції
            transaction.Commit();
          }
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Пояснення

  • рядок 10: запит Select, налаштований за допомогою numSecu, який потрібно виконати
  • рядок 11: присвоєння значення параметру numSecu та виконання методу UniqueResult для отримання одного результату.

Отримане відображення на екрані:

Recherche d'un employé -------------------------------------
Employe[254104940426058]=[254104940426058|Jouveinal|Marie|5 rue des oiseaux|St Corentin|49203|[2|2,1|2,1|3,1|15]]
Employe[xx] non trouvé...

1.5.4. Вставлення недійсних об’єктів

Наступний метод намагається зберегти неініціалізований об’єкт [Employe].


// SaveEmptyEmployee
    static void SaveEmptyEmployee() {
      try {
        // вхід у систему 
        using (ISession session = sessionFactory.OpenSession()) {
          // початок транзакції
          using (ITransaction transaction = session.BeginTransaction()) {
            // створення порожнього запису про співробітника
            Employe e = new Employe();
            // створення неіснуючої надбавки
            Indemnites i = new Indemnites() { Id = 0, Indice = 3, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
            // яку пов'язують із працівником
            e.Indemnites = i;
            // зберігаємо запис про працівника, залишивши інші поля порожніми
            session.Save(e);
            // фіксуємо транзакцію
            transaction.Commit();
          }
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Пояснення

Нагадаємо код класу [Employe]:


namespace PamNHibernateDemos {
    public class Employe {
        // автоматичні властивості
        public virtual int Id { get; set; }
        public virtual int Version { get; set; }
        public virtual string SS { get; set; }
        public virtual string Nom { get; set; }
        public virtual string Prenom { get; set; }
        public virtual string Adresse { get; set; }
        public virtual string Ville { get; set; }
        public virtual string CodePostal { get; set; }
        public virtual Indemnites Indemnites { get; set; }

        // конструктори
        public Employe() {
        }

        // ToString
        public override string ToString() {
            return string.Format("[{0}|{1}|{2}|{3}|{4}|{5}|{6}]", SS, Nom, Prenom, Adresse, Ville, CodePostal, Indemnites);
        }
    }
}

Неініціалізований об’єкт [Employe] матиме значення null для всіх своїх полів типу string. Під час вставки запису в таблицю [employes] об’єкт NHibernate залишить порожніми стовпці, що відповідають цим полям. Однак у таблиці [employes] усі стовпці мають атрибут not null, що забороняє наявність стовпців без значень. Тому драйвер ADO.NET викличе виняток:

sauvegarde d'un employé vide -------------------------------------
L'exception suivante s'est produite : could not insert: [PamNHibernateDemos.Employe][SQL: INSERT INTO EMPLOYES (VERSION, SS, NOM, PRENOM, ADRESSE, VILLE, CP, INDEMNITE_ID) VALUES (?, ?, ?, ?, ?, ?, ?, ?)]

1.5.5. Створення двох надбавок з однаковим індексом у межах однієї транзакції

У таблиці [indemnites] стовпець [indice] було оголошено з атрибутом «unique», що забороняє наявність двох рядків з однаковим індексом. Наступний метод створює дві компенсації з однаковим індексом у межах однієї транзакції:


// CreateIndemnites1
    static void CreateIndemnites1() {
      try {
        // вхід у систему 
        using (ISession session = sessionFactory.OpenSession()) {
          // початок транзакції
          using (ITransaction transaction = session.BeginTransaction()) {
            // створюються дві компенсації з однаковим індексом
            Indemnites i1 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
            Indemnites i2 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
            // зберігання
            session.Save(i1);
            session.Save(i2);
            // фіксація транзакції
            transaction.Commit();
          }
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Пояснення

  • у рядках 9 і 10 створюються дві сутності Indemnites з однаковим індексом. Однак у базі даних стовпець INDICE має атрибут UNIQUE.
  • У рядках 12 та 13 обидва об’єкти Indemnites поміщаються в контекст збереження. Цей контекст синхронізується з базою даних під час підтвердження транзакції в рядку 15. Ця синхронізація призведе до створення двох об’єктів INSERT. Другий з них спричинить виняток через унікальність стовпця INDICE. Оскільки ми перебуваємо всередині транзакції, перший INSERT буде скасовано.

Отримано такий результат:

Effacement base -------------------------------------
--------------- видалення співробітників
--------------- видалення виплат
--------------- видалення ставок внесків
Création de deux indemnités de même indice dans une transaction --------------
L'exception suivante s'est produite : could not insert: [PamNHibernateDemos.Indemnites][SQL: INSERT INTO INDEMNITES (VERSION, INDICE, BASE_HEURE, ENTRETIEN_JOUR, REPAS_JOUR, INDEMNITES_CP) VALUES (?, ?, ?, ?, ?, ?)]
Affichage base -------------------------------------
--------------- список співробітників
--------------- перелік виплат
--------------- таблиця ставок внесків

У рядку 9 видно, що таблиця [indemnites] порожня. Вставлення не відбулося.

1.5.6. Створення двох виплат з однаковим індексом поза транзакцією

Наступний метод створює дві виплати з однаковим індексом без використання транзакції:


// CreateIndemnites2
    static void CreateIndemnites2() {
      try {
        // вхід у систему 
        using (ISession session = sessionFactory.OpenSession()) {

          // створюються дві виплати з однаковим індексом
          Indemnites i1 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
          Indemnites i2 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.94, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
          // зберігаємо їх
          session.Save(i1);
          session.Save(i2);
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Пояснення

  • код такий самий, як і раніше, але без транзакції.
  • Синхронізація контексту збереження з базою даних відбудеться під час закриття цього контексту, рядок 13 (закриття Session). Синхронізація спричинить два INSERT. Друга операція завершиться невдачею через унікальність стовпця INDICE. Але оскільки ми не перебуваємо в транзакції, перша операція INSERT не буде скасована.

Отримано такий результат:

1
2
3
4
5
6
7
Création de deux indemnités de même indice sans transaction --------------
L'exception suivante s'est produite : could not insert: [PamNHibernateDemos.Indemnites][SQL: INSERT INTO INDEMNITES (VERSION, INDICE, BASE_HEURE, ENTRETIEN_JOUR, REPAS_JOUR, INDEMNITES_CP) VALUES (?, ?, ?, ?, ?, ?)]
Affichage base -------------------------------------
--------------- список співробітників
--------------- список надбавок
[1|1,93|2|3|12]
--------------- таблиця ставок внесків

Перед виконанням методу база даних була порожньою. У рядку 6 видно, що таблиця [indemnites] містить один запис.