Skip to content

3. Приклад із використанням SQL Server Express 2012

3.1. Introduction

Приклади для Entity Framework, знайдені в Інтернеті, здебільшого стосуються сервера SQL. Це цілком нормально. Ймовірно, саме SGBD є найпоширенішим у світі серед корпоративних серверів .NET. Ми дотримаємося цієї тенденції. Згодом приклади будуть розширені на всі бази даних, зазначені в пункті 1.2.

3.2. Встановлення інструментів

Ми не описуватимемо процес встановлення інструментів. Адже для цього знадобилося б дуже багато знімків екрана, які досить швидко втрачають актуальність. Це завдання (що, правда, не завжди є простим) ми залишаємо читачеві.

Нам потрібно встановити такі інструменти:

  • SGBD SQL Server Express 2012: [http://www.microsoft.com/fr-fr/download/details.aspx?id=29062]. Завантажте версію «With Tools», яка разом із SGBD містить інструмент адміністрування:
 

Після встановлення SGBD запускаємо його:

  • [1]: у меню «Пуск» запускаємо «Диспетчер конфігурації SQL Server»;
  • [2]: у цьому диспетчері запустіть сервер;
  • [3]: сервер запущено.

Тепер запускаємо інструмент адміністрування сервера SQL:

  • [1]: у меню «Пуск» запустіть «SQL Server Management Studio»;
  • [2]: інструмент адміністрування.

Підключимося до сервера:

  • у [1] підключаємо об’єктний оглядач;
  • у [2] вводимо параметри підключення:
  • [3]: сервер (локальний) (зверніть увагу на необхідні дужки) позначає сервер, встановлений на комп’ютері,
  • [4]: вибираємо аутентифікацію Windows. Щоб підключення відбулося успішно, потрібно мати права адміністратора на своєму комп’ютері,
  • [5]: здійснюється підключення;
  • [7]: потрібно змінити певні властивості сервера;
  • [8]: запитуємо, щоб було два режими автентифікації:
  • аутентифікація Windows, як це щойно було використано. Користувач Windows із відповідними правами може тоді увійти в систему,
  • аутентифікація на сервері SQL. Користувач повинен входити до списку зареєстрованих користувачів у SGBD;

Після цього можна підтвердити властивості сервера;

  • [9]: редагуємо властивості користувача «system administrator»;
  • у [10] встановлюємо для нього пароль. У подальшому тексті документа цей пароль — sqlserver2012;
  • у [10] надаємо йому дозвіл на підключення;
  • у [11] підключення активовано. На цьому роботу майстра можна завершити;
  • у [12] відбувається відключення від сервера.

Тепер ми знову підключаємося з логіном sa/sqlserver2012:

  • у [1] відбувається повторне підключення;
  • у [2], під час аутентифікації SQL Server;
  • у [3] користувач — sa;
  • у [4], його пароль — sqlserver2012;
  • у [5] — відбувається вхід;
  • у 6 — ми увійшли в систему.

Тепер ми створимо демонстраційну базу даних:

  • у [1] створюємо нову BD;
  • у [2], вона називатиметься demo;
  • у [3] підтверджуємо;
  • у [4] — база створена;
  • у [5] створюється нова таблиця в базі demo;
  • у 6 визначаємо таблицю з двома стовпцями ID та NOM;
  • у [7] стовпець [ID] визначено як первинний ключ;
  • у [8] первинний ключ позначається символом ключа;
  • у [9] таблицю зберігають;
  • у [10] їй присвоюють ім’я;
  • у [11], щоб таблиця з’явилася в базі [demo], потрібно оновити базу;
  • у [12] таблиця [PERSONNES] була успішно створена.

Наразі ми знаємо достатньо про використання інструменту адміністрування сервера SQL.

3.3. Вбудований сервер (localdb)\v11.0

VS Express 2012 постачається з вбудованим сервером SQL. Тут припускається, що VS Express 2012 було встановлено [http://www.microsoft.com/visualstudio/fra/downloads]. Запускаємо VS 2012 [1]:

Запускаємо інструмент адміністрування SQL Server 2012 [2] і входимо в систему [3].

  • у [4] підключаємося до сервера (localdb)\v11.0;
  • у [5] — з автентифікацією Windows;
  • у 6 після успішного підключення відображаються бази даних сервера. Як і раніше, можна створити нову базу даних.

Ми не будемо використовувати цей вбудований сервер у VS 2012.

3.4. Створення бази даних на основі сутностей

Entity Framework 5 Code First дозволяє створювати базу даних на основі сутностей. Саме це ми зараз і розглянемо. У VS Express 2012 ми створюємо перший консольний проєкт на C#:

  • у [1] — визначення проекту;
  • у [2] — створений проєкт.

Усі наші проекти потребуватимуть DLL з Entity Framework 5. Додаємо його:

  • у [1] інструмент NuGet дозволяє завантажити залежності;
  • в [2] завантажуємо залежність Entity Framework;
  • у [3] посилання було додано до проєкту.

Більше інформації можна дізнатися, переглянувши властивості доданого посилання:

  • у [1] вказано версію DLL. Потрібна версія 5;
  • у [2], її розташування у файловій системі: <solution>\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll, де <solution> — це папка рішення VS. Усі пакети, додані NuGet, потраплять до папки <solution>/packages;
  • у [3] було створено файл [packages.config]. Його вміст такий:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="EntityFramework" version="5.0.0" targetFramework="net45" />
</packages>

У ньому перелічено пакети, імпортовані NuGet.

Повернемося до проєкту VS і створимо в ньому папку [Models]:

  • у [1], додавання папки до проекту;
  • у [2], він називатиметься [Models].

Надалі ми будемо дотримуватися цієї практики — розміщувати визначення наших сутностей у папці [Models].

Для створення наших сутностей ми скористаємося визначенням бази даних MySQL 5, що використовується в проєкті NHibernate. Нагадаємо про роль сутностей EF:

Ентітети повинні відображати таблиці бази даних. Рівень доступу до даних використовує ці ентітети замість того, щоб працювати безпосередньо з таблицями. Почнемо з таблиці [MEDECINS]:

3.4.1. Ентітет [Medecin]

Вона містить інформацію про лікарів, якими керує додаток [RdvMedecins].

  • ID: номер, що ідентифікує лікаря — первинний ключ таблиці
  • VERSION: номер, що ідентифікує версію рядка в таблиці. Це число збільшується на 1 щоразу, коли до рядка вносяться зміни.
  • NOM: прізвище лікаря
  • PRENOM: його ім’я
  • TITRE: його титул (пані, пані, пан)

Ми можемо взяти за основу такий клас [Medecin]:


using System;

[Table("MEDECINS", Schema = "dbo")]
  namespace RdvMedecins.Entites
{
  public class Medecin
  {
    // дані
    public int Id { get; set; }
    public string Titre { get; set; }
    public string Nom { get; set; }
    public string Prenom { get; set; }
}
  • рядок 3: клас [Medecin] пов'язаний із таблицею [MEDECINS] у базі даних. Ця таблиця знаходиться у схемі з назвою «dbo».

Ми розміщуємо цей клас у файлі [Entites.cs] [1]. Саме туди ми розмістимо всі наші сутності.

Також у папці [Models] ми створюємо такий файл: [Context.cs]:


using System.Data.Entity;
using RdvMedecins.Entites;

namespace RdvMedecins.Models
{

  // контекст
  public class RdvMedecinsContext : DbContext
  {
    // лікарі
    public DbSet<Medecin> Medecins { get; set; }
  }

  // ініціалізація бази даних
  public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
  {
  }
}
  • рядок 8: клас [RdvMedecinsContext] представлятиме контекст збереження, c.-à-d. сукупність сутностей, що керуються ORM. Він повинен походити від класу [System.Data.Entity.DbContext];
  • рядок 11: поле [Medecins] представлятиме сутності типу [Medecin] з контексту персистентності. Він має тип DbSet<Medecin>. Зазвичай кількість [DbSet] дорівнює кількості таблиць у базі даних — по одному на таблицю;
  • рядок 15: визначається клас [RdvMedecinsInitializer] для ініціалізації створеної бази даних. Тут він походить від класу [DropCreateDataBaseAlways], який, як вказує його назва, видаляє базу даних, якщо вона вже існує, а потім створює її заново. Це зручно на етапі розробки BD. Параметром класу [DropCreateDataBaseAlways] є тип контексту збереження, пов’язаного з базою даних. Для класу ініціалізації можна використовувати інші батьківські класи, крім [DropCreateDataBaseAlways]:
  • [DropCreateDatabaseIfModelChanges]: перестворює базу даних, якщо сутності змінилися,
  • [CreateDatabaseIfNotExists]: створює базу, якщо її ще немає;

Залишається створити головну програму. Вона матиме такий вигляд: [CreateDB_01.cs]:


using System;
using System.Data.Entity;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class CreateDB_01
  {
    static void Main(string[] args)
    {
      // створення бази даних
      Database.SetInitializer(new RdvMedecinsInitializer());
      using (var context = new RdvMedecinsContext())
      {
        context.Database.Initialize(false);
      }
    }
  }
}
  • рядок 12: [System.Data.Entity.DataBase] — це клас, що надає статичні методи для управління базою даних, пов’язаною з контекстом збереження. Статичний метод [SetInitializer] дозволяє вказати клас ініціалізації бази даних. Це не запускає процес ініціалізації;
  • рядок 13: щоб працювати з контекстом збереження даних, його потрібно інстанціювати. Саме це й робиться тут. Використовується оператор using, щоб контекст автоматично закривався після виходу з цього оператора. Отже, у рядку 17 контекст закривається;
  • рядок 15: явно запускається створення бази даних, пов’язаної з контекстом збереження [RdvMedecinsContext]. Параметр false вказує, що ця операція не повинна виконуватися, якщо вона вже була виконана для цього контексту. Тут можна було б так само вказати true.

Під час роботи з базою даних параметри підключення зазвичай записуються у файл [App.config]. Зауважимо, що наразі їх там немає:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Докладнішу інформацію про налаштування Entity Framework можна знайти на сайті http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
  </entityFramework>
</configuration>

Вищезазначені елементи були записані у файл [App.config] під час додавання залежності Entity Framework до посилань проекту.

Запустимо проект (Ctrl-F5) після запуску SQL Server Express (це важливо):

Виконання має завершитися без помилок. Тепер відкриємо інструмент адміністрування SQL Server і оновимо відображення:

Ми бачимо, що було створено базу даних із повною назвою класу [RdvMedecinsContext], яка містить таблицю [dbo.MEDECINS] (саме таку назву ми їй дали) зі стовпцями, назви яких відповідають назвам полів сутності [Medecin]. Якщо код виконався успішно, але зазначена база даних не відображається, слід перевірити вбудований сервер (localdb)\v11.0 (див. стор. 19). У версії VS 2012 Pro цей сервер використовується, якщо сервер SQL не активний під час виконання коду. У версії VS 2012 Express — ні.

Розглянемо структуру таблиці [MEDECINS]:

  • вона містить імена полів сутності [Medecin];
  • стовпець [Id] є первинним ключем. Це умовно встановлене правило для EF: якщо суть E має поле Id або Eid (MedecinId), то цей стовпець є первинним ключем у відповідній таблиці;
  • типи стовпців таблиці відповідають типам полів сутності;
  • для стовпців «Титул», «Прізвище», «Ім’я» було використано тип [nvarchar(max)]. Можна було б бути точнішими: 5 символів для титулу, 30 — для прізвища та імені;
  • стовпці «Титул», «Прізвище», «Ім’я» можуть мати значення NULL. Ми це змінимо.

Розглянемо властивості первинного ключа [Id]:

У [1] бачимо, що первинний ключ має тип [Identité], що означає, що його значення автоматично генерується сервером SQL. Ми застосуємо цю стратегію до всіх SGBD.

Ми зменшимо залежність від угод EF, використовуючи анотації. Код сутності в [Entites.cs] виглядатиме так:


using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RdvMedecins.Entites
{
  [Table("MEDECINS", Schema = "dbo")]
  public class Medecin
  {
    // дані
    [Key]
    [Column("ID")]
    public int Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    [Required]
    [Column("VERSION")]
    public int Version { get; set; }
  }
}
  • рядки 2 і 3: анотації знаходяться в просторах імен [System.ComponentModel.DataAnnotations] (Key, Required, MaxLength) та [System.ComponentModel.DataAnnotations.Schema] (Column). Інші анотації можна знайти в просторах імен URL та [http://msdn.microsoft.com/en-us/data/gg193958.aspx];
  • рядок 11: [Key] позначає первинний ключ;
  • рядок 12: [Column] визначає назву стовпця, що відповідає полю;
  • рядок 14: [Required] вказує, що поле є обов’язковим (SQL, NOT, NULL);
  • рядок 15: [MaxLength] визначає максимальний розмір рядка, [MinLength] — мінімальний;

Запустимо проект із цим новим визначенням сутності [Medecin]. Створена база даних матиме такий вигляд:

 
  • стовпці мають імена, які ми їм присвоїли;
  • анотація [Required] була перетворена на SQL NOT NULL;
  • анотація [MaxLength(N)] була перетворена на тип SQL nvarchar(N).

У додатку NHibernate стовпець [VERSION] слугував для запобігання паралельному доступу до одного й того самого рядка таблиці. Принцип такий:

  • процес P1 зчитує рядок L з таблиці [MEDECINS] у момент часу T1. Рядок має версію V1;
  • процес P2 зчитує той самий рядок L з таблиці [MEDECINS] у момент часу T2. Рядок має версію V1, оскільки процес P1 ще не підтвердив свою зміну;
  • процес P1 підтверджує свою зміну рядка L. Версія рядка L тоді змінюється на V2=V1+1;
  • процес P2 підтверджує зміну рядка L. Тоді ORM генерує виняток, оскільки процес P2 має версію рядка L — V1 — яка відрізняється від версії V2, знайденої в базі даних.

Це називається оптимістичним управлінням паралельних звернень. У EF 5 поле, яке виконує цю роль, повинно мати один із двох атрибутів: [Timestamp] або [ConcurrencyCheck]. SQL Server має тип [timestamp]. Значення стовпця цього типу автоматично генерується SQL Server під час кожного вставлення або зміни рядка. Такий стовпець може використовуватися для управління конкуренцією доступу. Повертаючись до попереднього прикладу, процес P2 знайде timestamp, відмінний від того, який він прочитав, оскільки тим часом процес P1 вніс до нього зміни.

Наш об’єкт [Medecin] змінюється наступним чином:


using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
  {
    // дані
    [Key]
    [Column("ID")]
    public int Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
  }
}
  • рядки 26–28: новий стовпець з атрибутом [Timestamp] у рядку 27. Тип поля має бути byte[] (рядок 28). Ім’я поля може бути будь-яким. Атрибут [Required] не вказується, оскільки це значення надаватиме не додаток, а сам SGBD.

Якщо запустити проект із цією новою сутністю, база даних змінюється наступним чином:

Нам залишилося вирішити останнє питання. Контекст персистентності «знає», що об’єкт має бути вставлений у базу даних, оскільки в цей момент його первинний ключ дорівнює null. Саме вставлення в базу даних надасть значення первинному ключу. У цьому випадку тип int, присвоєний первинному ключу [Id], не підходить, оскільки цей тип не приймає значення null. Тож йому присвоюють тип int?, який приймає значення int та покажчик null. Отже, суть [Medecin], що використовується, буде такою:


public class Medecin
  {
    // дані
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    ...

Залишається з’ясувати, як у сутності представити поняття зовнішнього ключа між таблицями.

3.4.2. Ентітет [Creneau]

Таблиця [CRENEAUX] містить перелік часових проміжків, у яких можливі RV:

  • ID: номер, що ідентифікує часовий проміжок — первинний ключ таблиці
  • VERSION: номер, що ідентифікує версію рядка в таблиці. Це число збільшується на 1 кожного разу, коли до рядка вносяться зміни.
  • ID_MEDECIN: номер, що ідентифікує лікаря, якому належить цей часовий проміжок — зовнішній ключ у стовпці MEDECINS(ID).
  • HDEBUT: час початку часового проміжку
  • MDEBUT: хвилини початку часового проміжку
  • HFIN: година закінчення часового проміжку
  • MFIN: хвилини закінчення інтервалу

Другий рядок таблиці [CRENEAUX] (див. [1] вище) вказує, наприклад, що слот № 2 починається о 8:20 і закінчується о 8:40 та належить лікарю № 1 (пані Марі PELISSIER).

Виходячи з того, що нам відомо, ми можемо визначити суть [Creneau] у [Entites.cs] наступним чином:


[Table("CRENEAUX", Schema = "dbo")]
  public class Creneau
  {
    // дані
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [Column("HDEBUT")]
    public int Hdebut { get; set; }
    [Required]
    [Column("MDEBUT")]
    public int Mdebut { get; set; }
    [Required]
    [Column("HFIN")]
    public int Hfin { get; set; }
    [Required]
    [Column("MFIN")]
    public int Mfin { get; set; }
    [Required]
    public virtual Medecin Medecin { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
}

Єдина зміна стосується рядків 20–21. Те, що таблиця [CRENEAUX] має зовнішній ключ до таблиці [MEDECINS], відображається в сутності [Creneau] через наявність посилання на сутність [Medecin], рядок 21. Назва поля не має значення, важливий лише тип. Властивість має бути оголошена віртуальною за допомогою ключового слова virtual. Адже EF має перевизначити всі так звані навігаційні властивості, тобто ті, що відповідають зовнішньому ключу та дозволяють переходити з однієї таблиці до іншої.

Щоб протестувати нову суть, нам потрібно внести кілька змін у [Context.cs]:


using System.Data.Entity;
using RdvMedecins.Entites;

namespace RdvMedecins.Models
{

  // контекст
  public class RdvMedecinsContext : DbContext
  {
    // сутності
    public DbSet<Medecin> Medecins { get; set; }
    public DbSet<Creneau> Creneaux { get; set; }
  }

  // ініціалізація бази
  public class RdvMedecinsInitializer :  DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
  {
  }
}

Рядок 12 відображає те, що контекст має ще одну суть, яку потрібно обробляти. Коли ми запускаємо проект, отримуємо таку нову базу даних:

Таблиця [CRENEAUX] була успішно створена, а новим елементом є наявність зовнішніх ключів [1] та [2]. Його назва була згенерована на основі назви відповідного поля в сутності (Medecin) з додаванням суфікса «_Id». Щоб дізнатися про властивості цього зовнішнього ключа, спробуємо змінити його на [3].

На знімку екрана вище показано, що [Medecin_Id] є зовнішнім ключем таблиці [CRENEAUX] і посилається на первинний ключ [ID] таблиці [MEDECINS].

Якщо створювати сутності для існуючої бази даних, стовпець із зовнішнім ключем не обов’язково матиме назву [Medecin_Id]. Щодо інших стовпців ми вже бачили, що анотація [Column] вирішує цю проблему. Дивно, але у випадку із зовнішнім ключем все складніше. Потрібно діяти наступним чином:


public class Creneau
  {
    // дані
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    ...
}
  • рядки 5–7: створюємо поле типу зовнішнього ключа (int). За допомогою атрибута [Column] вказуємо ім’я стовпця, який буде зовнішнім ключем у таблиці, пов’язаній з сутністю;
  • рядок 9: додаємо анотацію [ForeignKey] до поля типу [Medecin]. Аргументом цієї анотації є ім’я поля (а не стовпця), яке пов’язане зі стовпцем зовнішнього ключа таблиці.

Під час виконання проєкту цього разу створюється така таблиця:

Як бачимо вище, стовпець із зовнішнім ключем має саме те ім’я, яке ми йому дали. Слід зауважити, що поля:


    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }

породили лише один стовпець — [MEDECIN_ID]. Проте наявність поля [MedecinId] є важливою. Під час зчитування рядка з таблиці [CRENEAUX] він отримає значення стовпця [MEDECIN_ID], тобто значення зовнішнього ключа з таблиці [MEDECINS]. Це часто буває корисним.

Поле [Medecin], наведене вище, відображає відношення «багато до одного», яке пов’язує суть [Creneau] із суттю [Medecin]. Кілька об’єктів [Creneau] пов’язані з одним і тим самим об’єктом [Medecin]. Зворотний зв’язок, коли один об’єкт [Medecin] пов’язаний з кількома об’єктами [Creneau], можна змоделювати за допомогою додаткового поля в сутності [Medecin]:


public class Medecin
  {
    // дані
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    ...
    public ICollection<Creneau> Creneaux { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

У рядку 8 додано поле [Creneaux], яке є колекцією об’єктів [Creneau]. Це поле надасть нам доступ до всіх часових проміжків лікаря.

При повторному запуску проєкту можна помітити, що таблиця [MEDECINS] не змінилася:

 

Жодного стовпця не було додано. Зв’язку через зовнішній ключ, що існує між таблицею [CRENEAUX] та таблицею [MEDECINS], достатньо для того, щоб EF могла згенерувати пов’язані з нею поля:


  public class Medecin
  {
    ...
    public ICollection<Creneau> Creneaux { get; set; }
    ...
  }

  public class Creneau
  {
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    ...
  }

Ми знаємо найголовніше. Тепер можемо завершити створення двох інших об'єктів.

3.4.3. Ентітети [Client] та [Rv]

Використовуючи отримані знання, ми можемо написати сутності [Client] та [Rv]. Сутність [Client] містить інформацію про клієнтів, якими керує додаток [RdvMedecins].

  • ID: номер, що ідентифікує клієнта — первинний ключ таблиці
  • VERSION: номер, що ідентифікує версію рядка в таблиці. Це число збільшується на 1 щоразу, коли до рядка вносяться зміни.
  • NOM: ім’я клієнта
  • PRENOM: його ім’я
  • TITRE: його/її титул (пані, пані, пан)

Елемент [Client] може мати такий вигляд:


  [Table("CLIENTS", Schema = "dbo")]
  public class Client
  {
    // дані
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    // Rvs клієнта
    public ICollection<Rv> Rvs { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
}

Клас [Client] майже ідентичний класу [Medecin]. Їх можна було б вивести з одного й того самого батьківського класу. Новим є рядок 21. Він відображає той факт, що клієнт може мати кілька зустрічей, і випливає з наявності зовнішнього ключа з таблиці [RVS] до таблиці [CLIENTS].

Ентітет [Rv] представляє зустріч:

  • ID: номер, що однозначно ідентифікує RV — первинний ключ
  • JOUR: день RV
  • ID_CRENEAU: часовий проміжок запису RV — зовнішній ключ у стовпці [ID] таблиці [CRENEAUX] — визначає як часовий проміжок, так і відповідного лікаря.
  • ID_CLIENT: номер клієнта, для якого зроблено бронювання — зовнішній ключ у стовпці [ID] таблиці [CLIENTS]

Елемент [Rv] може мати такий вигляд:


[Table("MEDECINS", Schema = "dbo")]
  public class Rv
  {
    // дані
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [Column("JOUR")]
    public DateTime Jour { get; set; }
    [Column("CLIENT_ID")]
    public int ClientId { get; set; }
    [ForeignKey("ClientId")]
    [Required]
    public virtual Client Client { get; set; }
    [Column("CRENEAU_ID")]
    public int CreneauId { get; set; }
    [ForeignKey("CreneauId")]
    [Required]
    public virtual Creneau Creneau { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
}
  • рядки 5–7: первинний ключ;
  • рядки 8–10: дата зустрічі;
  • рядки 11–12: зовнішній ключ з таблиці [RVS] до таблиці [CLIENTS];
  • рядки 13–15: клієнт, у якого призначено зустріч;
  • рядки 16–17: зовнішній ключ таблиці [RVS] до таблиці [CRENEAUX];
  • рядки 18–20: часовий проміжок зустрічі;
  • рядки 21–23: поле управління паралельним доступом.

У рядку 17 бачимо відношення «багато до одного»: одному часовому проміжку може відповідати кілька зустрічей (не в один і той самий день). Зворотне відношення може бути відображене в сутності [Creneau]:


public class Creneau
  {
    // Rvs часового проміжку
    public ICollection<Rv> Rvs { get; set; }
    ...
}

У рядку 4 — набір зустрічей, призначених у цьому часовому проміжку.

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

 

Таблиці [MEDECINS] та [CRENEAUX] не зазнали змін. Таблиці [CLIENTS] та [RVS] мають такий вигляд:

Це саме те, чого ми очікували. Залишилося вирішити кілька дрібниць:

  • обробити ім’я бази даних. Тут воно було згенеровано за допомогою EF;
  • заповнення бази даних.

3.4.4. Встановлення імені бази даних

Щоб встановити ім’я бази даних, згенерованої EF, ми використаємо рядок підключення, визначений у [App.config]. Цей конфігураційний файл змінюється наступним чином:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Докладнішу інформацію про налаштування Entity Framework можна знайти на сайті http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
  </entityFramework>

  <!-- ланцюг підключення до бази даних -->
  <connectionStrings>
    <add name="RdvMedecinsContext"
         connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <!-- фабричний провайдер -->
  <system.data>
    <DbProviderFactories>
      <add name="SqlClient Data Provider"
       invariant="System.Data.SqlClient"
       description=".Net Framework Data Provider for SqlServer"
       type="System.Data.SqlClient.SqlClientFactory, System.Data,
     Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
    />
    </DbProviderFactories>
  </system.data>

</configuration>
  • рядки 15–19: рядок підключення до бази даних;
  • рядок 16: атрибут [name] переймає ім’я класу [RdvMedecinsContext], що використовується для контексту збереження даних. Важливо про це пам’ятати. Це обмеження можна обійти у конструкторі контексту:

    // конструктор
    public RdvMedecinsContext()
      : base("monContexte")
    {
    }

У цьому випадку ми можемо отримати name= "monContexte". Саме це ми побачимо далі в документі.

  • рядок 17: рядок підключення. [Data Source]: ім’я сервера, на якому знаходиться SGBD, [Initial Catalog]: ім’я бази даних, тобто тут [rdvmedecins-ef], [User Id]: власник з’єднання, [Password]: його пароль. Читач повинен адаптувати цей рядок до свого середовища;
  • рядки 21–29: визначають [DbProviderFactory]. Я не знаю, що це таке. Судячи з назви, це може бути клас, що дозволяє генерувати шар [ADO.NET], який відокремлює EF від SGBD:

Насправді ці рядки непотрібні для SQL Server, але мені довелося додати їх для інших SGBD. Тож я розміщую їх тут для довідки. Вони не заважають. Єдиним важливим моментом є версія рядка 27. Це версія для DLL та [System.Data], що містяться у посиланнях проекту:

Ось і все. Ми готові. Запускаємо проект і отримуємо таку базу [rdvmedecins-ef]:

 

Це буде наша остаточна база даних. Залишилося лише внести в неї дані.

3.4.5. Заповнення бази даних

Для введення даних у базу можна використати клас ініціалізації бази:


public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
  {
    // ініціалізація бази даних
    public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
    {
      protected override void Seed(RdvMedecinsContext context)
      {
        base.Seed(context);
        // база даних ініціалізується
        // клієнти
        Client[] clients ={
        new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
        new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
        new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
        new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
     };
        foreach (Client client in clients)
        {
          context.Clients.Add(client);
        }
        // лікарі
        Medecin[] medecins ={
        new Medecin { Titre = "Mme", Nom = "Pelissier", Prenom = "Marie" },
        new Medecin { Titre = "Mr", Nom = "Bromard", Prenom = "Jacques" },
        new Medecin { Titre = "Mr", Nom = "Jandot", Prenom = "Philippe" },
        new Medecin { Titre = "Melle", Nom = "Jacquemot", Prenom = "Justine" }
     };
        foreach (Medecin medecin in medecins)
        {
          context.Medecins.Add(medecin);
        }
        // часові слоти
        Creneau[] creneaux ={
        new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=0,Hfin=14,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=20,Hfin=14,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=40,Hfin=15,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=0,Hfin=15,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=20,Hfin=15,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=40,Hfin=16,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=0,Hfin=16,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=20,Hfin=16,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=40,Hfin=17,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=0,Hfin=17,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=20,Hfin=17,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=40,Hfin=18,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[1]},
      };
        foreach (Creneau creneau in creneaux)
        {
          context.Creneaux.Add(creneau);
        }
        // прийоми
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
      }

    }
  }
  • рядок 6: ініціалізація відбувається в методі [Seed]. Цей метод існує в батьківському класі. Тут він перевизначений. Аргументом є контекст персистентності [RdvMedecinsContext] додатка;
  • рядок 8: аргумент передається до батьківського класу; ймовірно, що цей клас відкриває переданий йому контекст збереження, оскільки подальше відкриття вже не потрібне;
  • рядки 11–16: створення 4 клієнтів;
  • рядки 17–20: ці клієнти додаються до контексту персистентності, а точніше — до його лікарів. Зверніть увагу на метод [Add], який це дозволяє. Тут слід згадати визначення контексту:

  public class RdvMedecinsContext : DbContext
  {
    // організації
    public DbSet<Medecin> Medecins { get; set; }
    public DbSet<Creneau> Creneaux { get; set; }
    public DbSet<Client> Clients { get; set; }
public DbSet<Rv> Rvs { get; set; }
...

Також зазначається, що клієнти були прив’язані до контексту, тобто тепер ними керує EF. Раніше вони були від’єднані від нього. Вони існували як об’єкти, але не керувалися EF;

  • рядки 21–27: створення 4 лікарів;
  • рядки 28–31: їх поміщають у контекст збереження;
  • рядки 33–70: створення часових слотів. Рядки 34–57 — для лікаря medecins[0], рядки 58–69 — для лікаря medecins[1]. Інші лікарі не мають часових слотів;
  • рядки 71–74: ці часові слоти додаються до контексту збереження;
  • рядок 76: створення зустрічі для першого клієнта з першим часовим проміжком та його додавання до контексту збереження.

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

Вище наведено заповнену таблицю [CLIENTS].

3.4.6. Зміна сутностей

Наразі класи [Medecin] та [Client] є практично ідентичними. Насправді, якщо вилучити поля, додані для управління збереженням даних у EF 5, вони стають ідентичними. Ми зробимо їх похідними від класу [Personne]. Тоді ці дві сутності стануть такими:


// особа
  public abstract class Personne
  {
    // дані
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

    // підпис
    public override string ToString()
    {
      return String.Format("[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // короткий підпис
    public string ShortIdentity()
    {
      ...
    }

    // утиліта
    private string dump(byte[] timestamp)
    {
      ...
    }

  }

  [Table("MEDECINS", Schema = "dbo")]
  public class Medecin : Personne
  {
    // графік прийому лікаря
    public ICollection<Creneau> Creneaux { get; set; }
    // підпис
    public override string ToString()
    {
      return String.Format("Medecin {0}", base.ToString());
    }
  }

[Table("CLIENTS", Schema = "dbo")]
    public class Client : Personne
  {
    // записи пацієнта
    public ICollection<Rv> Rvs { get; set; }
    // підпис
    public override string ToString()
    {
      return String.Format("Client {0}", base.ToString());
    }
  }

Під час виконання проєкту ми отримуємо ту саму базу даних. EF 5 зіставив найнижчі класи спадковості, кожен із яких — з однією таблицею. Насправді EF 5 має різні стратегії генерації таблиць для представлення спадковості сутностей. Ми не будемо їх тут розглядати. Можна, наприклад, ознайомитися зі статтею « : Entity Framework Code First Inheritance — Table Per Hierarchy and Table Per Type» на URL [http://www.codeproject.com/Articles/393228/Entity-Framework-Code-First-Inheritance-Table-Per].

Відтепер ми будемо використовувати саме цю версію сутностей.

3.4.7. Додавання обмежень до бази даних

Залишилося вирішити ще одну деталь. Таблиця [RVS], що містить зустрічі, має такий вигляд:

 

Ця таблиця повинна мати обмеження унікальності: для певного дня часовий проміжок лікаря може бути заброньований лише один раз для прийому. З точки зору таблиці це означає, що пара (JOUR, CRENEAU_ID) має бути унікальною. Я не знаю, чи можна це обмеження виразити безпосередньо в коді — або на рівнях сутностей, або на рівні контексту. Це ймовірно, але я не перевіряв. Ми оберемо інший підхід. Ми використаємо клієнтське програмне забезпечення для адміністрування сервера SQL, щоб додати це обмеження.

У «SQL Server Management Studio» я не знайшов простого способу додати це обмеження, окрім виконання команди SQL, яка його створює:

  • у [1] створюється запит SQL для бази даних [rdvmedecins-ef];
  • у [2] — запит SQL, який створює обмеження унікальності;
  • у запиті [3] виконання цього запиту створило новий індекс у таблиці [RVS].

Існують й інші інструменти адміністрування сервера SQL. Тут ми будемо використовувати інструмент EMS SQL Manager for SQL Server Freeware [http://www.sqlmanager.net/fr/products/mssql/manager/download]. Після встановлення запускаємо його:

  • у [1] створюємо базу даних;
  • у [2] підключаємося до (локального) сервера;
  • у [3] — з автентифікацією SQL Server;
  • у файлі [4] — під ім'ям користувача sa;
  • у [5] — з паролем sqlserver2012;
  • у 6 переходимо до наступного етапу;
  • у [7] вибираємо базу даних [rdvmedecins-ef];
  • у [8] завершуємо роботу майстра;
  • у [9] база з’являється у дереві баз. Підключаємося до неї [10];
  • у [11] — підключення встановлено.

«SQL Manager Lite для сервера SQL» дозволяє створити обмеження унікальності для таблиці [RVS].

  • у [1] бачимо обмеження унікальності, яке ми створили раніше;
  • у [2] ми її видаляємо;
  • у [3] індекс, що відповідає цьому обмеженню унікальності, зник.

Відтворюємо видалене обмеження:

  • у [1] створюємо новий індекс для таблиці [RVS];
  • в [2] присвоюємо йому ім’я;
  • в [3] — це обмеження унікальності;
  • у [4] — для стовпців JOUR та CRENEAU_ID;

Вкладка DDL надає нам код SQL, який буде виконано:

  • у 6 компілюється команда SQL;
  • в [7], підтверджуємо;
  • у [8] з’явився новий індекс.

Інтерфейс, який пропонує «SQL Manager Lite for SQL server», аналогічний тому, що пропонує «SQL Server Management Studio». Аналогічні інтерфейси можна знайти для SGBD Oracle, PostgreSQL, Firebird, MySQL. Тому надалі ми будемо продовжувати роботу з цією серією інструментів адміністрування SGBD.

Щоб отримати доступ до інформації з таблиці, достатньо двічі клацнути по ній:

Інформація про вибрану таблицю доступна у вкладках. Вище показано вкладку [Fields] таблиці [CLIENTS]. Вкладка [Data] відображає вміст таблиці:

Image

3.4.8. Остаточна база даних

Ми маємо нашу остаточну базу даних. Експортуємо її скрипт SQL, щоб мати змогу відтворити її за потреби.

  • у [1], початок майстра;
  • у [2] — сервер;
  • у [3] — база даних, яку буде експортовано;
  • у [4] вкажіть ім’я файлу, у якому буде збережено скрипт SQL;
  • у полі [5] вкажіть його кодування;
  • у полі 6 вкажіть, що саме ви хочете витягти (таблиці, обмеження, дані);
  • у [7] ви можете уточнити параметри скрипта, що буде згенеровано;
  • у [8] завершіть роботу майстра.

Скрипт було згенеровано та завантажено в редактор скриптів. Ви можете переглянути згенерований код SQL. Ми відновимо базу даних за допомогою цього скрипта.

  • у [1] видаляємо базу даних;
  • у [2] та [3] її відтворюємо;
  • у [4] — проводиться аутентифікація;
  • у [5] виконується скрипт SQL для створення бази даних;
  • у 6 її зберігають у «SQL Manager»;
  • у [7] підключаємося до щойно створеної бази даних;
  • у [8] база даних поки що не містить таблиць;
  • у [9a] відкривається редактор скриптів SQL;
  • у [9b] відкривається раніше створений скрипт SQL;
  • у [10] його виконують;
  • у [11] таблиці створено;
  • у [12] — вони заповнені;
  • у [14] ми бачимо обмеження унікальності, яке ми створили для таблиці [RVS].

Відтепер ми будемо працювати з цією існуючою базою даних. Якщо вона буде знищена або пошкоджена, ми знаємо, як її відновити.

3.5. Використання бази даних за допомогою Entity Framework

Ми будемо:

  • додавати, видаляти, змінювати елементи бази даних;
  • виконувати запити до бази даних за допомогою LINQ to Entities;
  • управляти паралельним доступом до одного й того самого елемента бази даних;
  • розібратися з поняттями Lazy Loading / Eager Loading;
  • дізнаємося, що оновлення бази даних контекстом персистентності відбувається в рамках транзакції.

3.5.1. Видалення елементів з контексту збереження

Ми маємо заповнену базу даних. Ми збираємося її очистити. Створюємо новий клас [Erase.cs] у поточному проєкті [1]:

Клас [Erase] має такий вигляд:


using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class Erase
  {
    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // очищення поточної бази даних
        // клієнти
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        // лікарі
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // збереження контексту персистентності
        context.SaveChanges();
      }
    }
  }
}
  • рядок 9: операції з контекстом персистентності завжди виконуються у клаузулі [using]. Це гарантує, що при виході з [using] контекст буде закрито;
  • рядок 13: відбувається обхід контексту клієнтів [context.Clients]. Усі клієнти бази даних будуть поміщені в контекст персистентності;
  • рядок 15: для кожного з них виконується операція [Remove], яка видаляє їх із контексту. Насправді вони все ще перебувають у контексті, але у стані «видалено»;
  • рядки 18–21: те саме робимо для лікарів;
  • рядок 23: контекст збереження записується в базу даних.

Під час збереження контексту в базі даних сутності контексту, які:

  • мають нульовий первинний ключ, стають об’єктом операції SQL INSERT;
  • перебувають у стані «видалено», підлягають операції SQL DELETE;
  • перебувають у стані «змінено» — підлягають операції SQL UPDATE;

Як ми побачимо пізніше, ці операції SQL виконуються в рамках транзакції. Якщо одна з них завершиться невдало, все, що було зроблено раніше, буде скасовано.

Зробимо програму [Erase] новим об’єктом запуску проекту [1], а потім запустимо проект.

Перевіримо базу даних. Ми побачимо, що всі таблиці [2] порожні. Це дивно, адже ми просили лише видалити лікарів і клієнтів. Завдяки дії зовнішніх ключів інші таблиці були послідовно очищені.

Визначення зовнішнього ключа з таблиці [CRENEAUX] до таблиці [MEDECINS] було задано провайдером EF 5 наступним чином:

  • у [1] вибираємо таблицю [CRENEAUX];
  • у [2] вибирається вкладка «Зовнішні ключі»;
  • у [3] редагується єдиний зовнішній ключ;
  • у [4], на вкладці DDL, визначення SQL обмеження зовнішнього ключа;
  • У таблиці [5] умова ON DELETE CASCADE передбачає, що видалення лікаря призводить до видалення пов’язаних з ним часових слотів.

Обмеження зовнішніх ключів таблиці [RVS] визначено аналогічно:

1
2
3
4
5
6
ALTER TABLE [dbo].[RVS]
ADD CONSTRAINT [FK_dbo.RVS_dbo.CLIENTS_CLIENT_ID] FOREIGN KEY ([CLIENT_ID]) 
  REFERENCES [dbo].[CLIENTS] ([ID]) 
  ON UPDATE NO ACTION
  ON DELETE CASCADE
GO
  • рядки 1–6: видалення клієнта призведе до видалення пов’язаних з ним прийомів;
1
2
3
4
5
6
ALTER TABLE [dbo].[RVS]
ADD CONSTRAINT [FK_dbo.RVS_dbo.CRENEAUX_CRENEAU_ID] FOREIGN KEY ([CRENEAU_ID]) 
  REFERENCES [dbo].[CRENEAUX] ([ID]) 
  ON UPDATE NO ACTION
  ON DELETE CASCADE
GO
  • рядки 1–6: видалення часового проміжку призведе до видалення всіх зустрічей, пов’язаних із ним.

3.5.2. Додавання елементів до контексту збереження

Тепер, коли ми очистили базу даних, ми знову її заповнимо. Додаємо до проєкту програму [Fill.cs] [1].

Програма [Fill.cs] має такий вигляд:


using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class Fill
  {
    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // очищення поточної бази даних
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // її скидають
        // клієнти
        Client[] clients ={
        new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
        new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
        new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
        new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
     };
        foreach (Client client in clients)
        {
          context.Clients.Add(client);
        }
        // лікарі
        Medecin[] medecins ={
        new Medecin { Titre = "Mme", Nom = "Pelissier", Prenom = "Marie" },
        new Medecin { Titre = "Mr", Nom = "Bromard", Prenom = "Jacques" },
        new Medecin { Titre = "Mr", Nom = "Jandot", Prenom = "Philippe" },
        new Medecin { Titre = "Melle", Nom = "Jacquemot", Prenom = "Justine" }
     };
        foreach (Medecin medecin in medecins)
        {
          context.Medecins.Add(medecin);
        }
        // часові слоти
        Creneau[] creneaux ={
        new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=0,Hfin=14,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=20,Hfin=14,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=40,Hfin=15,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=0,Hfin=15,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=20,Hfin=15,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=40,Hfin=16,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=0,Hfin=16,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=20,Hfin=16,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=40,Hfin=17,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=0,Hfin=17,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=20,Hfin=17,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=40,Hfin=18,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[1]},
      };
        foreach (Creneau creneau in creneaux)
        {
          context.Creneaux.Add(creneau);
        }
        // зустрічі
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
        // зберігаємо контекст персистентності
        context.SaveChanges();
      }
    }
  }
}
  • рядок 10: відкриваємо контекст збереження даних;
  • рядки 13–20: рядки таблиць [CLIENTS] та [MEDECINS] вносяться до контексту, а потім видаляються з нього. Ми щойно побачили, що це повністю очищає базу даних;
  • рядки 22–88: до контексту збереження додаються елементи. Усі вони мають нульове значення первинного ключа. Отже, вони будуть вставлені в базу даних;
  • рядок 90: зміни, внесені в контекст, синхронізуються з базою даних. З нею буде виконано серію операцій SQL DELETE, за якими слідуватиме серія операцій SQL INSERT;

Програму [Fill] робимо новим об’єктом запуску проекту [1], а потім виконуємо останній.

У [2] бачимо, що таблиці заповнені.

3.5.3. Відображення вмісту бази даних

Тепер ми відобразимо вміст бази даних за допомогою запитів LINQ to Entity. LINQ (Language INtegrated Query) з’явився разом із фреймворком .NET 3.5 у 2007 році. Він є розширенням мов .NET та c.a.d, інтегрованим у мову, а його синтаксис перевіряється компілятором. Він дозволяє виконувати запити до різних колекцій за допомогою синтаксису, що має схожість із мовою SQL (Structured Query Language) для запитів до баз даних. Існують різні варіанти LINQ:

  • LINQ to Object — для запитів до колекцій у пам’яті;
  • LINQ to XML — для виконання запитів до XML;
  • LINQ до Entity, для запитів до баз даних;

Щоб існувати, LINQ спирається на численні розширення, зроблені до мов .NET. Їх можна використовувати поза межами LINQ. Ми не будемо їх тут описувати, а лише наведемо два джерела, де читач знайде детальний опис LINQ:

  • «LINQ in Action», Фабріс Маргері, Стів Ейхерт, Джим Вулі, видавництво Manning;
  • «LINQ pocket reference», Джозеф та Бен Альбахарі, видавництво O'Reilly.

Я прочитав першу книгу і вважаю її чудовою. Другу я не читав, але читав книгу тих самих авторів «C# 3.0 in a nutshell», яка вийшла одночасно з LINQ. Ця книга, на мою думку, значно вища за середній рівень книг, які я зазвичай читаю. Схоже, що інші книги цих двох авторів також відповідають цьому рівню. Крім того, ми будемо використовувати LINQPad — навчальний інструмент для LINQ, написаний Джозефом Альбахарі.

Ми відобразимо сутності, що містяться в базі даних. Для цього додамо до їхніх класів два методи відображення. Почнемо з сутності [Medecin]:


// лікар
  public class Medecin
  {
    // дані
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    // часові слоти лікаря
    public ICollection<Creneau> Creneaux { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

    // підпис
    public override string ToString()
    {
      return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // скорочений підпис
    public string ShortIdentity()
    {
      return ToString();
    }

    // додаткова інформація
    private string dump(byte[] timestamp){
      string str = "";
      foreach (byte b in timestamp)
      {
        str += b;
      }
      return str;
    }
  }
  • рядки 27–30: метод ToString класу. Зверніть увагу, що він не відображає колекцію з рядка 21;
  • рядки 32–37: метод ShortIdentity, який робить те саме.

Тут нам слід пояснити поняття «Lazy» та «Eager Loading», щоб оцінити вплив двох попередніх методів. Ми бачили, що суть може мати залежності від іншої суті. Вони бувають двох видів:

  • «один до багатьох», як наведено вище, де лікар пов’язаний із кількома часовими слотами;
  • від декількох до одного, як у сутності [Creneau] нижче, де кілька часових слотів пов’язані з одним лікарем;

public class Creneau
  {
    // дані
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    ...
  }

Коли залежності завантажуються одночасно з об’єктами, до яких вони прив’язані, це називається Eager Loading. В іншому випадку це називається «Lazy Loading»: залежності завантажуються лише тоді, коли на них вперше з’являється посилання. За замовчуванням EF 5 використовує «Lazy Loading»: залежності не завантажуються одночасно з сутністю.

Розглянемо наш метод [ToString], наведений вище:


    // графік прийому лікаря
    public ICollection<Creneau> Creneaux { get; set; }
    
    // підпис
    public override string ToString()
    {
      return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // скорочений підпис
    public string ShortIdentity()
    {
      return ToString();
}

Метод [ToString] не відображає залежність [Creneaux] у рядку 2. Якби він це зробив, то змусив би завантажити всі слоти лікаря ще до свого виконання. Саме для уникнення цього ресурсоємного завантаження залежність не була включена до сигнатури сутності. Загалом, ми будемо включати дві сигнатури в кожну сутність:

  • метод ToString, який відображатиме суть та її можливі залежності «кілька до одного». Як щойно було пояснено, це спричинить завантаження залежності;
  • метод ShortIdentity, який не буде посилатися на жодні залежності. Отже, завантаження залежностей не відбуватиметься;

Методи відображення інших сутностей будуть такими:

Елемент [Client]:


  public class Client
  {
    // дані
    ...
    // записи на прийом у клієнта
    public ICollection<Rv> Rvs { get; set; }
    
    // підпис
    public override string ToString()
    {
      return String.Format("Client[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // скорочений підпис
    public string ShortIdentity()
    {
      return ToString();
    }

}
  • рядки 9–12: метод [ToString] не відображає залежність із рядка 6;

Об’єкт [Creneau]:


public class Creneau
  {
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    // дату та час слота
    public ICollection<Rv> Rvs { get; set; }
    
    // підпис
    public override string ToString()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
    }
    // короткий підпис
    public string ShortIdentity()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Timestamp, MedecinId, dump(Timestamp));
    }
  }
  • рядок 16: метод [ToString] посилається на залежність у рядку 9. Це змусить її завантажитися;
  • рядок 11: на залежність [Rvs] немає посилання. Вона не буде завантажена;
  • рядки 21–22: метод [ShortIdentity] більше не посилається на посилання [Medecin] з рядка 9. Отже, воно не буде завантажено.

Об’єкт [Rv]:


public class Rv
  {
    // дані
    ...
    [Column("CLIENT_ID")]
    public int ClientId { get; set; }
    [ForeignKey("ClientId")]
    [Required]
    public virtual Client Client { get; set; }
    [Column("CRENEAU_ID")]
    public int CreneauId { get; set; }
    [ForeignKey("CreneauId")]
    [Required]
    public virtual Creneau Creneau { get; set; }

    // підпис
    public override string ToString()
    {
      return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, Client, Creneau, dump(Timestamp));
    }
    // короткий підпис
    public string ShortIdentity()
    {
      return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, ClientId, CreneauId, dump(Timestamp));
    }

  }
  • рядки 17–20: метод [ToString] посилається на залежності з рядків 9 та 14. Це змусить їх завантажитися;
  • рядки 17–20: метод [ShortIdentity] цього уникає, тому залежності не завантажуватимуться.

Отже, слід звертати увагу на методи [ToString] об’єктів. Якщо не звертати на це уваги, відображення таблиці може призвести до завантаження половини бази даних, якщо таблиця має багато залежностей.

З огляду на це, ми пишемо такий новий код [Dump.cs]:


using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;

namespace RdvMedecins_01
{
  class Dump
  {
    static void Main(string[] args)
    {
      // дамп бази даних
      using (var context = new RdvMedecinsContext())
      {
        // клієнти
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
        }
        // лікарі
        Console.WriteLine("Médecins--------------------------------------");
        var medecins = from medecin in context.Medecins select medecin;
        foreach (Medecin medecin in medecins)
        {
          Console.WriteLine(medecin);
        }
        // часові слоти
        Console.WriteLine("Créneaux horaires--------------------------------------");
        var creneaux = from creneau in context.Creneaux select creneau;
        foreach (Creneau creneau in creneaux)
        {
          Console.WriteLine(creneau);
        }
        // прийоми
        Console.WriteLine("Rendez-vous--------------------------------------");
        var rvs = from rv in context.Rvs select rv;
        foreach (Rv rv in rvs)
        {
          Console.WriteLine(rv);
        }
      }
    }
  }
}

Ми пояснимо рядки 17–21, які виводять сутності [Client]. Наведене пояснення стосуватиметься й інших сутностей.


        // клієнти
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
}
  • рядок 3: ключове слово var було введено в C# 3.0. Воно дозволяє уникнути вказівки конкретного типу змінної. Компілятор виводить його з типу виразу, що присвоюється змінній;
  • рядок 3: вираз, що присвоюється змінній clients, є запитом LINQ to Entity. У ньому можна впізнати ключові слова мови SQL, перенесені в LINQ. Синтаксис, що використовується тут, є таким:

from variable in DbSet select variable

Більш загальний синтаксис LINQ має вигляд


from variable in collection select variable

Колекція буде пройдена, і для кожного її елемента змінна буде обчислена. Це відбувається лише тоді, коли змінна [clients] із рядка 3 буде перелічена за допомогою циклу for / each у рядках 4–7. Доки це не відбудеться, змінна [clients] є лише невизначеним запитом;

  • рядок 4: запит [clients] перебирається. Це змусить обчислити запит. Рядки таблиці [CLIENTS] по черзі передаватимуться в контекст персистентності;
  • рядок 6: для відображення використовується метод [ToString] сутності [Client]. Завантаження залежностей не відбувається;

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

  • рядки 24–28: рядки таблиці [MEDECINS] передаються в контекст персистентності та відображаються. Завантаження залежностей не відбувається;
  • рядки 31–35: рядки таблиці [CRENEAUX] передаються в контекст персистентності та відображаються. Ми бачили, що метод [ToString] цієї сутності відображав залежність [Medecin]. Однак вона вже завантажена. Тому повторного завантаження не відбудеться;
  • рядки 38–42: рядки таблиці [RVS] передаються в контекст персистентності та відображаються. Ми бачили, що метод [ToString] цієї сутності відображав залежності [Client] та [Creneau]. Однак вони вже завантажені. Тому нових завантажень не відбудеться.

Слід зауважити, що порядок відображення має значення. Якби ми хотіли спочатку відобразити сутності [Rv], метод [ToString] цієї сутності спричинив би завантаження сутностей [Client] та [Creneau], пов’язаних із цими зустрічами. Решта об’єктів не завантажилися б. Вони завантажилися б пізніше під час іншого відображення. Це впливає на продуктивність. Попередній код потребує чотирьох запитів SQL, щоб відобразити всі об’єкти. Тепер припустимо, що спочатку обробляється таблиця зустрічей [RVS]. Для таблиці [RVS] необхідний перший запит SQL. Далі метод [ToString] сутності [Rv] може спричинити завантаження пов’язаних сутностей [Client] та [Creneau]. Для кожного з них потрібен запит SQL. Припустимо, що є N2 клієнтів та N3 часових проміжків, і що всі ці сутності зазначені в таблиці [RVS], для її відображення знадобиться 1 + N2 + N3 запитів SQL. Отже, продуктивність нижча, ніж у розглянутій версії. Щоб відобразити таблицю [RVS] разом із її залежностями, знадобиться з’єднання таблиць. Його можна здійснити за допомогою таблиці LINQ. Ми повернемося до цього на прикладі. А поки що нагадаємо, що слід звернути увагу на запити SQL, що лежать в основі нашого коду LINQ.

Налаштовуємо проєкт для виконання цього нового коду [1] та [2], а потім запускаємо його:

Вивід на консолі виглядає так:

Clients--------------------------------------
Client[9,Mr,Jules,Martin,000000844]
Client[10,Mme,Christine,German,000000845]
Client[11,Mr,Jules,Jacquard,000000846]
Client[12,Melle,Brigitte,Bistrou,000000847]
Médecins--------------------------------------
Medecin[9,Mme,Marie,Pelissier,000000848]
Medecin[10,Mr,Jacques,Bromard,000000873]
Medecin[11,Mr,Philippe,Jandot,000000886]
Medecin[12,Melle,Justine,Jacquemot,000000887]
Créneaux horaires--------------------------------------
Creneau[73,8,0,8,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000849]
Creneau[74,8,20,8,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000850]
Creneau[75,8,40,9,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000851]
Creneau[76,9,0,9,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000852]
Creneau[77,9,20,9,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000853]
Creneau[78,9,40,10,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000854]
Creneau[79,10,0,10,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000855]
Creneau[80,10,20,10,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000856]
Creneau[81,10,40,11,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000857]
Creneau[82,11,0,11,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000858]
Creneau[83,11,20,11,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000859]
Creneau[84,11,40,12,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000860]
Creneau[85,14,0,14,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000861]
Creneau[86,14,20,14,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000862]
Creneau[87,14,40,15,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000863]
Creneau[88,15,0,15,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000864]
Creneau[89,15,20,15,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000865]
Creneau[90,15,40,16,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000866]
Creneau[91,16,0,16,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000867]
Creneau[92,16,20,16,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000868]
Creneau[93,16,40,17,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000869]
Creneau[94,17,0,17,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000870]
Creneau[95,17,20,17,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000871]
Creneau[96,17,40,18,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000872]
Creneau[97,8,0,8,20, Medecin[10,Mr,Jacques,Bromard,000000873],000000874]
Creneau[98,8,20,8,40, Medecin[10,Mr,Jacques,Bromard,000000873],000000875]
Creneau[99,8,40,9,0, Medecin[10,Mr,Jacques,Bromard,000000873],000000876]
Creneau[100,9,0,9,20, Medecin[10,Mr,Jacques,Bromard,000000873],000000877]
Creneau[101,9,20,9,40, Medecin[10,Mr,Jacques,Bromard,000000873],000000878]
Creneau[102,9,40,10,0, Medecin[10,Mr,Jacques,Bromard,000000873],000000879]
Creneau[103,10,0,10,20, Medecin[10,Mr,Jacques,Bromard,000000873],000000880]
Creneau[104,10,20,10,40, Medecin[10,Mr,Jacques,Bromard,000000873],000000881]
Creneau[105,10,40,11,0, Medecin[10,Mr,Jacques,Bromard,000000873],000000882]
Creneau[106,11,0,11,20, Medecin[10,Mr,Jacques,Bromard,000000873],000000883]
Creneau[107,11,20,11,40, Medecin[10,Mr,Jacques,Bromard,000000873],000000884]
Creneau[108,11,40,12,0, Medecin[10,Mr,Jacques,Bromard,000000873],000000885]
Rendez-vous--------------------------------------
Rv[3,08/10/2012 00:00:00,Client[9,Mr,Jules,Martin,000000844],Creneau[73,8,0,8,20
, Medecin[9,Mme,Marie,Pelissier,000000848],000000849],000000888]
Appuyez sur une touche pour continuer...

3.5.4. Навчання LINQ за допомогою LINQPad

Вище ми використовували запити LINQ to Entity для відображення вмісту таблиць бази даних. Джозеф Альбахарі написав програму для навчання різних форм LINQ. Зараз ми її презентуємо.

LINQPad доступний за наступним посиланням: URL. Після встановлення ми запускаємо програму [1]:

Новачки можуть ознайомитися з програмою LINQ за допомогою прикладів на вкладках [Samples] та [2], де наведено безліч прикладів. Виберемо приклад [3], який потім відобразиться в іншому вікні [4]. Повний код прикладу такий:


// А тепер простий вираз запиту LINQ-to-objects (зверніть увагу, що крапки з комою немає):

from word in "The quick brown fox jumps over the lazy dog".Split()
orderby word.Length
select word


// Не соромтеся редагувати це... (ніхто не дивиться!) Вам буде запропоновано зберегти будь-які
// зміни в окремому файлі.
//
// Порад а: Ви можете виконати частину запиту, виділивши її, а потім натиснувши F5.

Рядки 3–5 є прикладом запиту LINQ to Object. Запит LINQ має такий синтаксис:


from variable in collection orderby élément1 select élément2
  • змінна позначає поточний елемент колекції. У нашому прикладі ця колекція — це список слів, отриманих у результаті розбиття рядка;
  • колекція впорядкована відповідно до параметра élément1 параметра orderby. У нашому прикладі колекція слів буде впорядкована за їхньою довжиною;
  • ключове слово select вказує, що саме потрібно витягти з поточного елемента variable колекції. У нашому прикладі це буде саме слово.

Виконаємо цей запит LINQ:

  • у [1]: вираз LINQ виконується за допомогою [F5] або через кнопку виконання;
  • у [2]: виведення на екран. Слова відображаються в порядку їхньої довжини. Цей простий приклад демонструє потужність LINQ;
  • у [3] можна завантажити інші приклади, зокрема ті, що містяться у книзі «LINQ in action» [4];
  • у [5] ми обираємо приклад із книги;

string[] words = { "hello", "wonderful", "linq", "beautiful", "world" };

// Згрупувати слова за довжиною
var groups =
  from word in words
  orderby word ascending
  group word by word.Length into lengthGroups
  orderby lengthGroups.Key descending
  select new { Length = lengthGroups.Key, Words = lengthGroups };

// Роздрукувати кожну групу
foreach (var group in groups)
{
  Console.WriteLine("Words of length " + group.Length);
  foreach (string word in group.Words)
    Console.WriteLine("  " + word);
}
  • рядок 4: новий запит LINQ з новими ключовими словами;
  • рядок 5: запитувана колекція — це масив слів із рядка 1;
  • рядок 6: колекція сортована в алфавітному порядку слів;
  • рядок 7: колекція об’єднана (ключове слово into) у нову колекцію lengthGroups. lengthGroups.Key позначає фактор об’єднання (ключове слово by), у даному випадку — довжину слів. lengthGroups об’єднує слова з однаковим фактором групування, тобто однаковою довжиною;
  • рядок 8: колекція lengthGroups впорядкована за ключем групування у спадному порядку, тобто тут — за зменшенням розміру слів;
  • рядок 9: з цієї колекції створюються нові об’єкти (анонімні класи) з двома полями:
    • Length: довжина слів,
    • Words: слова, що мають цю довжину;

Тут особливо помітно значення ключового слова var у рядку 4. Оскільки в рядку 9 використано анонімний клас, ми не можемо вказати тип змінної groups. Компілятор, у свою чергу, надасть анонімному класу внутрішнє ім’я та визначить тип змінної groups на його основі. Після цього він зможе визначити, чи правильно використовується змінна groups

  • рядок 12: обхід запиту з рядка 4. Лише в цей момент він обчислюється. Нагадаємо, що його виконання створить колекцію об’єктів, зазначених у рядку 9;
  • рядок 14: виводиться властивість Length поточного елемента, тобто довжина слів;
  • рядки 15–17: виводиться кожен елемент колекції властивості Words, тобто сукупність слів, що мають довжину, виведену раніше.

Коли ми виконуємо цей запит, отримуємо такий результат у LINQPad:

 

Тепер, коли ми розглянули кілька прикладів запитів [LINQ to Object], давайте розглянемо запити [LINQ to Entity], які дозволять нам здійснювати запити до баз даних. Спочатку ми під’єднаємося до бази даних SQL Server, яку ми створили та заповнили:

  • у [1] додаємо підключення до бази даних;
  • у [2] — засоби доступу до джерела даних. Для доступу до бази SQL Server ми будемо використовувати [LINQPad Driver];
  • у [3] також можна отримати контекст збереження [DbContext], визначений у файлі .exe або .dll assembly (варіант 3). На жаль, на сьогодні (8 жовтня 2012 року) Entity Framework 5 не підтримується;
  • у [4] можна завантажити драйвери для інших SGBD, окрім SQL Server;
  • у [5] можна завантажити драйвер для SGBD, MySQL та Oracle;
  • на 6 — завантажений драйвер;
  • у [7] ми підключаємося до сервера SQL;
  • у [8] база знаходиться на сервері імен (локальному);
  • у [9] ми підключаємося з автентифікацією sa / sqlserver2012;
  • у [10] — до бази даних [rdvmedecins-ef], яку ми створили;
  • у [11] можна перевірити з'єднання;
  • у [12] завершуємо роботу майстра;
  • у [13] з’являється з’єднання у LINQPad.

Об’єкти було створено на основі таблиці [rdvmedecins-ef]. Це такі об’єкти:

  • у [1], [CLIENTS] представляє сукупність об’єктів [Client]. Кожен об’єкт має:
    • властивості (ID, TITRE, NOM, PRENOM, TIMESTAMP),
    • відношення «1 до багатьох» з [CLIENTRVS];
  • де [2], [CRENEAUXes] представляють сукупність об’єктів [Creneau]. Кожен об’єкт має:
    • властивості (ID, HDEBUT, MDEBUT, HFIN, MFIN, MEDECIN_ID, TIMESTAMP),
    • відношення «1 до декількох» [CRENEAURVS],
  • відношення «багато до одного» [MEDECIN];
  • у [3] суть [MEDECINS] представляє сукупність сутностей [Medecin]. Кожна суть має:
    • властивості (ID, TITRE, NOM, PRENOM, TIMESTAMP),
    • відношення «1 до багатьох» [MEDECINCRENEAUXes];
  • у [4] суть [RVS] представляє сукупність сутностей [Rv]. Кожна суть має:
    • властивості (ID, JOUR, CLIET_ID, CRENEAU_ID, TIMESTAMP),
    • відношення «багато до одного» до [CLIENT],
    • відношення «багато до одного» [CRENEAU].

Зауважимо, що назви властивостей, наведені вище, відрізняються від тих, які ми використовували досі. Це не має значення. Ми просто хочемо вивчити основні принципи формування запитів до бази даних.

Давайте подивимося, як можна виконати запит до цієї бази сутностей. Наприклад, нам потрібен список лікарів, упорядкований за їхніми TITRE та NOM:

  • у [1] створюємо новий запит;
  • у [2] — текст запиту;
  • у [3] — результат запиту;
  • у [4] — той самий запит із лямбда-виразами. Запит із лямбда-виразами менш читабельний, ніж текстовий запит, і, можливо, ви захочете обійтися без нього. Однак іноді вони є незамінними, оскільки дозволяють робити те, чого не дозволяють текстові запити. Лямбда-вираз позначає функцію з одним вхідним параметром a та одним вихідним параметром b у формі a=>b. Наведений вище метод OrderBy приймає лямбда-функцію як єдиний параметр. Вона надає йому параметр, за яким має бути впорядковано колекцію. Таким чином, MEDECINS.OrderBy(m=>m.TITRE) — це список лікарів, впорядкований за званнями. Цю інструкцію слід розглядати як конвеєрну обробку колекції. Колекція лікарів передається як вхідні дані методу OrderBy. Цей метод оброблятиме об’єкти [Medecin] по одному. У лямбда-виразі m=>m.TITRE m позначає вхідні дані лямбда-функції. Їх можна назвати як завгодно. У даному випадку вхідними даними лямбда-функції буде суть [Medecin]. Функція m=>m.TITRE читається так: якщо я називаю m своїм вхідним значенням (об’єкт [Medecin]), то моїм вихідним значенням є m.TITRE, тобто ім’я лікаря. MEDECINS.OrderBy(m=>m.TITRE) — це, у свою чергу, колекція, колекція лікарів, упорядкована за званнями. Ця нова колекція може бути передана в інший метод, у даному прикладі — метод ThenBy. Він працює за тим самим принципом. Він слугує для вказання додаткових параметрів сортування колекції.

Читання лямбда-коду, еквівалентного текстовому коду, який ми зазвичай вводимо, — це хороший спосіб його засвоїти;

  • у [5], запит SQL, що надсилається до бази даних. І тут знову слід уважно прочитати цей код. Він дозволяє оцінити реальну вартість запиту LINQ.

Далі ми наводимо кілька прикладів запитів LINQ. У кожному випадку ми показуємо відображені результати та еквівалентні коди lambda і SQL. Щоб зрозуміти ці запити, слід згадати відношення «багато до одного», які пов’язують сутності між собою. Саме за їх допомогою ми переходимо від однієї сутності до іншої. Їх називають навігаційними властивостями.

// клієнти з титулом «Mr», відсортовані за спаданням імен

Результати:

 
LINQ

from client in CLIENTS where client.TITRE=="Mr" 
orderby client.NOM descending  select client
Lambda
CLIENTS
.Where (client => (client.TITRE == "Mr"))
.OrderByDescending (client => client.NOM)
SQL

-- Region Parameters
DECLARE @p0 NVarChar(1000) = 'Mr'
-- EndRegion
SELECT [t0].[ID], [t0].[TITRE], [t0].[NOM], [t0].[PRENOM],
 [t0].[TIMESTAMP]
FROM [CLIENTS] AS [t0]
WHERE [t0].[TITRE] = @p0
ORDER BY [t0].[NOM] DESC

// усі часові слоти з відповідним лікарем

Результати (часткові):

 
LINQ

from creneau in CRENEAUXes 
select new { hd=creneau.HDEBUT, md=creneau.MDEBUT, hf=creneau.HFIN,
 mf=creneau.MFIN, medecin=creneau.MEDECIN}
Lambda
SQL

SELECT [t0].[HDEBUT] AS [hd], [t0].[MDEBUT] AS [md], [t0].[HFIN] AS [hf],
 [t0].[MFIN] AS [mf], [t1].[ID], [t1].[TITRE], [t1].[NOM], [t1].[PRENOM],
 [t1].[TIMESTAMP]
FROM [CRENEAUX] AS [t0]
INNER JOIN [MEDECINS] AS [t1]
 ON [t1].[ID] = [t0].[MEDECIN_ID]

// усі зустрічі з клієнтом та лікарем

Результати:

 
LINQ

from rv in RVS select new { rv=rv.CLIENT, medecin=rv.CRENEAU.MEDECIN}
Lambda
SQL

SELECT [t1].[ID], [t1].[TITRE], [t1].[NOM], [t1].[PRENOM], [t1].[TIMESTAMP],
 [t3].[ID] AS [ID2], [t3].[TITRE] AS [TITRE2], [t3].[NOM] AS [NOM2],
 [t3].[PRENOM] AS [PRENOM2], [t3].[TIMESTAMP] AS [TIMESTAMP2]
FROM [RVS] AS [t0]
INNER JOIN [CLIENTS] AS [t1] ON [t1].[ID] = [t0].[CLIENT_ID]
INNER JOIN [CRENEAUX] AS [t2] ON [t2].[ID] = [t0].[CRENEAU_ID]
INNER JOIN [MEDECINS] AS [t3] ON [t3].[ID] = [t2].[MEDECIN_ID]

// лікарі, у яких немає призначених прийомів

Результати:

 
LINQ
 
Lambda
SQL

SELECT [t0].[ID], [t0].[TITRE], [t0].[NOM], [t0].[PRENOM], [t0].[TIMESTAMP]
FROM [MEDECINS] AS [t0]
WHERE NOT (EXISTS(
    SELECT NULL AS [EMPTY]
    FROM [RVS] AS [t1]
    INNER JOIN [CRENEAUX] AS [t2] ON [t2].[ID] = [t1].[CRENEAU_ID]
    INNER JOIN [MEDECINS] AS [t3] ON [t3].[ID] = [t2].[MEDECIN_ID]
    WHERE [t3].[ID] = [t0].[ID]
    ))

Для цього запиту немає запиту LINQ. Потрібно використовувати лямбда-вирази. Цей вираз читається наступним чином: я беру колекцію лікарів (MEDECINS) і залишаю (Where) лише тих лікарів (m), для яких я не можу знайти в колекції призначень (RVS) жодного призначення (rv) з цим лікарем (m).

// часові проміжки пані Пеліссьє

(Часткові) результати:

 
LINQ

from creneau in CRENEAUXes where creneau.MEDECIN.NOM=="Pelissier"
 select creneau
Lambda
SQL

-- Region Parameters
DECLARE @p0 NVarChar(1000) = 'Pelissier'
-- EndRegion
SELECT [t0].[ID], [t0].[HDEBUT], [t0].[MDEBUT], [t0].[HFIN], [t0].[MFIN],
 [t0].[MEDECIN_ID], [t0].[TIMESTAMP]
FROM [CRENEAUX] AS [t0]
INNER JOIN [MEDECINS] AS [t1] ON [t1].[ID] = [t0].[MEDECIN_ID]
WHERE [t1].[NOM] = @p0

// кількість зустрічей пані Пеліссьє 08.10.2012

Результати:

 
LINQ

(from rv in RVS where rv.CRENEAU.MEDECIN.NOM=="Pelissier"
 && rv.JOUR==new DateTime(2012,10,08)  select rv).Count()
Lambda
 
SQL

-- Region Parameters
DECLARE @p0 NVarChar(1000) = 'Pelissier'
DECLARE @p1 DateTime = '2012-10-08 00:00:00.000'
-- EndRegion
SELECT COUNT(*) AS [value]
FROM [RVS] AS [t0]
INNER JOIN [CRENEAUX] AS [t1] ON [t1].[ID] = [t0].[CRENEAU_ID]
INNER JOIN [MEDECINS] AS [t2] ON [t2].[ID] = [t1].[MEDECIN_ID]
WHERE ([t2].[NOM] = @p0) AND ([t0].[JOUR] = @p1)

// список клієнтів, які домовилися про зустріч з пані Пеліссьє 08.10.2012

Результати:

 
LINQ

from rv in RVS where (rv.JOUR==new DateTime(2012,10,08)
 && rv.CRENEAU.MEDECIN.NOM=="Pelissier") select rv.CLIENT
Lambda
SQL

-- Region Parameters
DECLARE @p0 DateTime = '2012-10-08 00:00:00.000'
DECLARE @p1 NVarChar(1000) = 'Pelissier'
-- EndRegion
SELECT [t3].[ID], [t3].[TITRE], [t3].[NOM], [t3].[PRENOM], [t3].[TIMESTAMP]
FROM [RVS] AS [t0]
INNER JOIN [CRENEAUX] AS [t1] ON [t1].[ID] = [t0].[CRENEAU_ID]
INNER JOIN [MEDECINS] AS [t2] ON [t2].[ID] = [t1].[MEDECIN_ID]
INNER JOIN [CLIENTS] AS [t3] ON [t3].[ID] = [t0].[CLIENT_ID]
WHERE ([t0].[JOUR] = @p0) AND ([t2].[NOM] = @p1)

// кількість прийомних годин на одного лікаря

Результати:

 
LINQ

from creneau in CRENEAUXes 
group creneau by creneau.MEDECIN into creneauxMedecin 
select new { nom=creneauxMedecin.Key.NOM,
 prenom=creneauxMedecin.Key.PRENOM,
 nbRv=creneauxMedecin.Count()}
Lambda
SQL

SELECT [t2].[NOM] AS [nom], [t2].[PRENOM] AS [prenom], [t1].[value] AS [nbRv]
FROM (
    SELECT COUNT(*) AS [value], [t0].[MEDECIN_ID]
    FROM [CRENEAUX] AS [t0]
    GROUP BY [t0].[MEDECIN_ID]
    ) AS [t1]
INNER JOIN [MEDECINS] AS [t2] ON [t2].[ID] = [t1].[MEDECIN_ID]

3.5.5. Зміна об'єкта, пов'язаного з контекстом збереження

Ми розглянули такі операції з контекстом збереження:

  • додавання елемента до контексту ([dbContext].[DbSet].Add);
  • видалення елемента з контексту ([dbContext].[DbSet].Remove);
  • запит до контексту за допомогою запитів LINQ.

Якщо потрібно синхронізувати контекст із базою даних, потрібно написати [dbContext].SaveChanges().

Код [ModifyAttachedEntity] ілюструє зміну об’єкта, пов’язаного з контекстом:


using System;
using System.Data;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class ModifyAttachedEntity
  {
    static void Main(string[] args)
    {
      Client client1, client2, client3;
      // 1-й контекст
      using (var context = new RdvMedecinsContext())
      {
        // очистити поточну базу даних
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // додаємо клієнта
        client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
        context.Clients.Add(client1);
        // подальші дії
        Console.WriteLine("client1--avant");
        Console.WriteLine(client1);
        // збереження контексту
        context.SaveChanges();
        // моніторинг
        Console.WriteLine("client1--après");
        Console.WriteLine(client1);
      }
      // другий контекст
      using (var context = new RdvMedecinsContext())
      {
        // витягуємо клієнта1 з клієнта2
        client2 = context.Clients.Find(client1.Id);
        // відстеження
        Console.WriteLine("client2");
        Console.WriteLine(client2);
        // змінюємо клієнта2
        client2.Nom = "yy";
        // збереження контексту
        context.SaveChanges();
      }
      // 3-й контекст
      using (var context = new RdvMedecinsContext())
      {
        // витягуємо клієнта2 у клієнта3
        client3 = context.Clients.Find(client2.Id);
        // відстеження
        Console.WriteLine("client3");
        Console.WriteLine(client3);
      }
    }
  }
}
  • рядок 15: відкриття контексту додатка;
  • рядки 18–25: контекст очищується. Точніше кажучи, усі об’єкти переносяться в контекст із бази даних, а потім переходять у стан «видалено». Зазначимо, що на цьому етапі база даних не змінилася. Доки контекст не синхронізовано з базою даних, остання не змінюється. Нагадаємо, що видалення сутностей [Medecin] та [Client] достатньо для очищення бази завдяки каскадному видаленню;
  • рядки 27–28: до бази додано нового клієнта;
  • рядки 30–31: його відображають перед збереженням у базі;
  • рядок 33: виконується синхронізація контексту з базою даних. Елементи, позначені як «видалені», стануть об’єктом операції SQL DELETE, а об’єкт, доданий за допомогою операції SQL INSERT;
  • рядки 35–36: клієнт відображається після синхронізації з базою даних;

Результат, отриманий у консолі, такий:

1
2
3
4
client1--avant
Client[,xx,xx,xx,]
client1--après
Client[16,xx,xx,xx,000000132209]

Слід звернути увагу на наступні моменти:

  • до синхронізації з базою даних клієнт не має ані первинного ключа, ані timestamp,
  • після синхронізації він їх має. Нагадаємо, що первинний ключ було налаштовано так, щоб його генерував сервер SQL. Так само цей SGBD автоматично генерує часовий штамп;
  • рядок 37: контекст збереження закрито. Ентітети, що містилися в ньому, стають «відокремленими». Вони існують як об’єкти, але не як ентітети, прив’язані до контексту збереження;
  • рядок 39: запускається новий порожній контекст;
  • рядок 42: клієнт отримується безпосередньо з бази даних за його первинним ключем. Потім він вноситься в контекст. Якщо його не знайдено, метод Find повертає покажчик null;
  • рядки 48–49: його виводимо на екран;

Це дає такий результат:

client2
Client[16,xx,xx,xx,000000132209]
  • рядок 47: його змінюють;
  • рядок 49: контекст синхронізується з базою. EF виявить, що деякі елементи контексту були змінені з моменту їх перенесення туди. Для цих елементів він згенерує команди SQL та UPDATE у базі даних. Отже, у цьому випадку синхронізація полягатиме в єдиній команді UPDATE;
  • рядок 50: другий контекст закрито. Об’єкт client2, який був приєднаний до контексту, тепер від’єднується від нього;
  • рядок 52: відкривається третій порожній контекст;
  • рядок 55: туди знову переноситься єдиний клієнт з бази. Ми хочемо перевірити, чи зміни, внесені до нього в попередньому контексті, були відображені в базі;
  • рядки 57–58: виводиться клієнт. Це дає такий результат:
client3
Client[16,xx,xx,yy,000000132210]

Ім’я клієнта дійсно було змінено в базі даних. Цікаво, що його timestamp було оновлено.

  • рядок 59: закриваємо контекст. До речі, слід зазначити, що, на відміну від двох попередніх випадків, раніше не було потреби синхронізувати контекст із базою даних (SaveChanges), оскільки контекст не зазнав змін.

3.5.6. Управління відокремленими сутностями

Повернемося до багаторівневої архітектури додатка, подібного до того, що розглядається в цьому прикладі:

Рівень [DAO] використовує ORM та EF5 для доступу до даних. Ми маємо основні компоненти цього шару. Кожен метод відкриватиме контекст збереження даних, виконуватиме в ньому необхідні операції (вставлення, зміна, видалення, запит), а потім закриватиме його. Ентітети, що управляються шаром [DAO], передаються до веб-шару ASP.NET. У цьому шарі вони знаходяться поза контекстом збереження даних, тобто є відокремленими. У веб-шарі користувач може змінювати ці ентітети (додавати, змінювати, видаляти). Коли вони повертаються до шару [DAO], вони все ще залишаються від’єднаними. Однак шар [DAO] повинен буде відобразити зміни, внесені користувачем, у базі даних. Отже, йому доведеться працювати з від’єднаними сутностями. Розглянемо три можливі випадки:

Додавання відокремленого об’єкта

Це звичайний випадок додавання. Достатньо додати (Add) відокремлений об’єкт до контексту, переконавшись, що його первинний ключ дорівнює null.

Зміна відокремленого об’єкта

Можна використати такий код:

[DbContext].Entry(entité-détachée).State=EntityState.Modified ;
  • метод [DbContext].Entry(відокремлений об’єкт) додасть об’єкт до контексту;
  • статус цього об’єкта встановлюється як «змінений», щоб на нього було застосовано команду SQL UPDATE.

Видалення відокремленого об’єкта

Можна використати такий код:

Entity e=[DbContext].[DbSet].Find(clé primaire de l'entité détachée) ;
[DbContext].[DbSet].Remove(e) ;
  • рядок 1: додаємо до контексту об’єкт із тим самим первинним ключем, що й відокремлений об’єкт;
  • рядок 2: видаляємо її:

Слід зауважити, що для цього спочатку потрібно вказати SELECT, а потім — DELETE, хоча зазвичай достатньо лише DELETE. Також можна скористатися прикладом редагування відокремленого об’єкта та написати:

[DbContext].Entry(entité-détachée).State=EntityState.Deleted ;

Оскільки мені не вдалося налаштувати журналювання операцій SQL, що виконуються в базі даних, я не знаю, чи можна рекомендувати один метод замість іншого.

Ось приклад:

Код програми [ModifyDetachedEntities] такий:


using System;
using System.Data;
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class ModifyDetachedEntities
  {
    static void Main(string[] args)
    {
      Client client1;

      // очищення поточної бази даних
      Erase();
      // додаємо клієнта
      using (var context = new RdvMedecinsContext())
      {
        // створення клієнта
        client1 = new Client { Titre = "x", Nom = "x", Prenom = "x" };
        // додавання клієнта до контексту
        context.Clients.Add(client1);
        // збереження контексту
        context.SaveChanges();
      }
      // перегляд бази даних
      Dump("1-----------------------------");
      // клієнт1 відсутній у контексті — його змінюють
      client1.Nom = "y";
      // новий контекст
      using (var context = new RdvMedecinsContext())
      {
        // тут контекст порожній
        // клієнт1 додається до контексту у зміненому стані
        context.Entry(client1).State = EntityState.Modified;
        // зберігаємо контекст
        context.SaveChanges();
      }
      // базовий вигляд
      Dump("2-----------------------------");
      // видалення об’єкта поза контекстом
      using (var context = new RdvMedecinsContext())
      {
        // тут ми маємо новий порожній контекст
        //  klєнт1 поміщається в контекст у стані «видалено»
        context.Entry(client1).State = EntityState.Deleted;
        // зберігаємо контекст
        context.SaveChanges();
      }
      // відображення бази даних
      Dump("3-----------------------------");
    }

    static void Erase()
    {
      // очищення бази даних
      using (var context = new RdvMedecinsContext())
      {
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // зберігаємо контекст
        context.SaveChanges();
      }
    }

    static void Dump(string str)
    {
      Console.WriteLine(str);
      // відображення бази
      using (var context = new RdvMedecinsContext())
      {
        foreach (var rv in context.Rvs)
        {
          Console.WriteLine(rv);
        }
        foreach (var creneau in context.Creneaux)
        {
          Console.WriteLine(creneau);
        }
        foreach (var client in context.Clients)
        {
          Console.WriteLine(client);
        }
        foreach (var medecin in context.Medecins)
        {
          Console.WriteLine(medecin);
        }
      }
    }
  }
}
  • рядок 15: база даних очищується;
  • рядки 17–25: до бази даних додається клієнт;
  • рядок 27: відображається вміст бази даних;
1-----------------------------
Client[20,x,x,x,0000011209]
  • після рядка 25 контекст збереження більше не існує. Отже, приєднаних сутностей більше немає. Сутність client1 перейшла у стан «від'єднана»;
  • рядок 29: змінюється ім’я від’єднаного об’єкта;
  • рядок 31: відкривається новий порожній контекст;
  • рядок 35: від'єднаний об'єкт client1 поміщається в контекст у стані «змінений»;
  • рядок 37: контекст синхронізується з базою даних;
  • рядок 38: контекст закривається;
  • рядок 40: база даних відображається;
2-----------------------------
Client[20,x,x,y,0000011210]

Ім'я клієнта було успішно змінено в базі даних. Зверніть увагу, що timestamp було оновлено;

  • рядок 42: відкрито новий порожній контекст;
  • рядок 46: відокремлений об’єкт client1 поміщається в контекст у стані «видалено»;
  • рядок 48: контекст синхронізовано з базою даних;
  • рядок 49: контекст закрито;
  • рядок 51: база даних відображається;
3-----------------------------

Ентітет було успішно видалено з бази даних.

Тепер ми розглянемо два режими завантаження залежностей об’єкта: Lazy та Eager Loading.

3.5.7. Lazy та Eager Loading

Повернемося до схеми залежностей «багато до одного» одного з наших чотирьох об’єктів:

На малюнку вище суть [Creneau] має навігаційну властивість [Creneau.Medecin], що вказує на суть [Medecin]. Це називається залежністю. Ми бачили, що існують також залежності типу «один до багатьох». Принцип, який буде пояснено, застосовується також і до них.

За замовчуванням EF 5 працює в режимі Lazy Loading: коли він завантажує суть із бази даних у контекст персистентності, він не завантажує її залежності. Вони будуть завантажені під час їхнього першого використання. Це цілком логічний підхід. Якби це було не так, то при внесенні зустрічей у контекст, відповідно до наведених вище залежностей, також були б внесені:

  • сутності [Creneau], пов’язані з зустрічами;
  • об’єкти [Medecin], пов’язані з цими часовими проміжками;
  • об’єкти [Clients], пов’язані з зустрічами.

Однак іноді потрібна як сама суть, так і її залежності. Ми проілюструємо обидва способи завантаження.

Код [LazyEagerLoading] такий:


using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;

namespace RdvMedecins_01
{
  class LazyEagerLoading
  {
    // об'єкти
    static Medecin[] medecins;
    static Client[] clients;
    static Creneau[] creneaux;

    static void Main(string[] args)
    {
      // ініціалізація бази      
      InitBase();
      Console.WriteLine("Initialisation terminée");
      // попереднє завантаження
      Creneau creneau;
      int idCreneau = (int)creneaux[0].Id;
      using (var context = new RdvMedecinsContext())
      {
        // слот № 0
        creneau = context.Creneaux.Include("Medecin").Single<Creneau>(c => c.Id == idCreneau);
        Console.WriteLine(creneau.ShortIdentity());
      }
      // відображення залежностей
      try
      {
        Console.WriteLine("Médecin={0}", creneau.Medecin);
      }
      catch (Exception e)
      {
        Console.WriteLine("L'erreur 1 suivante s'est produite : {0}", e);
      }
      // відкладене завантаження — режим за замовчуванням
      using (var context = new RdvMedecinsContext())
      {
        // слот № 0
        creneau = context.Creneaux.Single<Creneau>(c => c.Id == idCreneau);
        Console.WriteLine(creneau.ShortIdentity());
      }
      // відображення залежностей
      try
      {
        Console.WriteLine("Médecin={0}", creneau.Medecin);
      }
      catch (Exception e)
      {
        Console.WriteLine("L'erreur 2 suivante s'est produite : {0}", e);
      }

    }

    static void InitBase()
    {
      // ініціалізація бази даних
      using (var context = new RdvMedecinsContext())
      {
        // очищення поточної бази даних
        ...
        // ініціалізація бази даних
        // клієнти
        clients = new Client[] {
        new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
        new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
        new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
        new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
     };
...
        // зустрічі
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
        // збереження контексту персистентності
        context.SaveChanges();
      }
    }
  }
}
  • рядок 18: ми виходимо з відомої бази даних, яка використовувалася до цього моменту. Після цієї операції масиви в рядках 11–13 заповнюються відокремленими об’єктами;
  • рядки 21–22: розглядається перший часовий проміжок та відповідний лікар;
  • рядок 23: новий контекст;
  • рядок 26: ми поміщаємо слот у контекст разом із його залежністю (eager loading). Оскільки це не режим за замовчуванням, потрібно явно запитати цю залежність. Це дозволяє зробити метод Include. Його параметром є ім’я залежності в сутності, що вводиться в контекст. Запит, що вводить суть у контекст, використовує лямбда-вирази. Метод Single дозволяє вказати умову, за якою повертається лише одна суть. Тут ми шукаємо в базі суть [Creneau], яка має первинний ключ слота № 0;
  • рядок 27: виводиться отримана суть. Нагадаємо два методи запису, що використовуються в сутностях:

// підпис
    public override string ToString()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5},{6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
    }
 
   // короткий підпис
    public string ShortIdentity()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, MedecinId, dump(Timestamp));
    }
  • рядки 2–5: метод [ToString] відображає залежність [Medecin]. Якщо вона ще не присутня в контексті, її шукатимуть у базі даних, щоб додати туди;
  • рядки 8–11: метод [ShortIdentity] не відображає залежність [Medecin]. Отже, її не шукатимуть у базі даних, якщо вона відсутня в контексті;

На цьому етапі вивід у консолі виглядає так:

Initialisation terminée
Creneau[181,8,0,8,20, 21, 00000195150]
  • рядок 28: контекст закрито;
  • рядки 30–37: відбувається спроба записати залежність [Medecin] об’єкта. Нагадаємо, як працює Lazy Loading: залежність завантажується під час її першого використання, якщо її немає. У цьому випадку вона, зазвичай, є. Вивід на екрані такий:
Médecin=Medecin[21,Mme,Marie,Pelissier,00000195149]
  • рядки 39–44: у новому контексті слот № 0 знову шукається в базі даних і вноситься в контекст. У цьому випадку залежність [Medecin] явно не запитується. Отже, вона не буде завантажена (Lazy Loading);
  • рядок 43: короткий ідентифікатор слота відображається наступним чином:
Creneau[181,8,0,8,20, 21, 00000195150]

Тут важливо використовувати ShortIdentity замість ToString для відображення сутності. Якщо використати ToString, буде відображено залежність [Medecin], і для цього її шукатимуть у базі даних. А ми цього не хочемо.

  • рядок 44: контекст закрито;
  • рядки 46–53: робиться спроба відобразити залежність об’єкта. Важливо робити це поза контекстом, інакше її шукатимуть у базі даних і знайдуть. Тут ми перебуваємо поза контекстом. Ентітет [Creneau] відокремлений, а його залежність [Medecin] відсутня (Lazy Loading). Що відбудеться? На екрані відобразиться таке:
L'erreur 2 suivante s'est produite : System.ObjectDisposedException: L'instance ObjectContext a été supprimée et ne peut plus être utilisée pour les opérations qui requièrent une connexion.
   à System.Data.Objects.ObjectContext.EnsureConnection()
   à System.Data.Objects.ObjectQuery`1.GetResults(Nullable`1 forMergeOption)
   à System.Data.Objects.ObjectQuery`1.Execute(MergeOption mergeOption)
   à System.Data.Objects.DataClasses.EntityReference`1.Load(MergeOption mergeOption)
   à System.Data.Objects.DataClasses.RelatedEnd.Load()
   à System.Data.Objects.DataClasses.RelatedEnd.DeferredLoad()
   à System.Data.Objects.Internal.LazyLoadBehavior.LoadProperty[TItem](TItem propertyValue, String relationshipName, String targetRoleName, Boolean mustBeNull,Object wrapperObject)
   à System.Data.Objects.Internal.LazyLoadBehavior.<>c__DisplayClass7`2.<GetInterceptorDelegate>b__2(TProxy proxy, TItem item)
   à System.Data.Entity.DynamicProxies.Creneau_AF14A89855AD9B7E5ABA4A877B4989B2F8B3F7ECA154E3FEC02BA722002773E4.get_Medecin()
   à RdvMedecins_01.LazyEagerLoading.Main(String[] args) dans d:\data\istia-1213\c#\dvp\Entity Framework\RdvMedecins\RdvMedecins-SqlServer-01\LazyEagerLoading.cs:рядок 48

EF виявив відсутність залежності [Medecin]. Він намагався завантажити її, але оскільки контекст був закритий, ця операція стала неможливою. Запам’ятаємо це виключення [System.ObjectDisposedException], оскільки воно характерне для завантаження залежності поза відкритим контекстом.

Тепер розглянемо конкуренцію доступу до сутностей.

3.5.8. Конкуренція доступу до сутностей

Повернемося до визначення сутності [Client]:


public class Client
  {
    // дані
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    // Rvs клієнта
    public ICollection<Rv> Rvs { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

    // підпис
    ...
  }

Ми зосередимося на полі [Timestamp] у рядку 23. Ми знаємо, що його значення генерується SGBD. Ми також зазначали, що анотація [Timestamp] у рядку 22 змушувала EF 5 використовувати анотоване поле для управління конкуренцією доступу до сутностей. Нагадаємо, що таке управління конкуренцією доступу:

  • процес P1 зчитує рядок L з таблиці [MEDECINS] у момент часу T1. Рядок має timestamp TS1;
  • процес P2 зчитує той самий рядок L з таблиці [MEDECINS] у момент часу T2. Рядок має значення timestamp TS1, оскільки процес P1 ще не підтвердив свою зміну;
  • процес P1 підтверджує свої зміни до рядка L. Після цього значення timestamp для рядка L змінюється на TS2;
  • процес P2 підтверджує зміну рядка L.ORM генерує виняток, оскільки процес P2 має значення timestamp та TS1 для рядка L, що відрізняються від значень timestamp та TS2, знайденого в базі даних.

Це називається оптимістичним управлінням паралельних звернень. У EF 5 поле, яке виконує цю роль, повинно мати один із двох атрибутів: [Timestamp] або [ConcurrencyCheck]. Сервер SQL має тип [timestamp]. Значення стовпця цього типу автоматично генерується сервером SQL під час кожного вставлення або зміни рядка. Такий стовпець може використовуватися для управління конкурентним доступом.

Ми проілюструємо цю конкуренцію доступу за допомогою двох потоків, які одночасно змінюватимуть один і той самий об’єкт [Client] у базі даних. Проект розвивається наступним чином:

Код програми [AccèsConcurrents] такий:


using System;
using System.Data;
using System.Linq;
using System.Threading;
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{

  // об'єкт, що обмінюється з потоками
  class Data
  {
    public int Duree { get; set; }
    public string Nom { get; set; }
    public Client Client { get; set; }
  }

  // тестова програма
  class AccèsConcurrents
  {

    static void Main(string[] args)
    {
      Client client1;
      using (var context = new RdvMedecinsContext())
      {
        // головний потік
        Thread.CurrentThread.Name = "main";
        // очищення поточної бази даних
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // додаємо клієнта
        client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
        context.Clients.Add(client1);
        // відстеження
        Console.WriteLine("{0} client1--avant sauvegarde du contexte", Thread.CurrentThread.Name);
        Console.WriteLine(client1.ShortIdentity());
        // збереження
        context.SaveChanges();
        // відстеження
        Console.WriteLine("{0} client1--après sauvegarde du contexte", Thread.CurrentThread.Name);
        Console.WriteLine(client1.ShortIdentity());
      }
      // змінимо клієнта1 за допомогою двох потоків
      // потік t1
      Thread t1 = new Thread(Modifie);
      t1.Name = "t1";
      t1.Start(new Data { Duree = 5000, Nom = "yy", Client = client1 });
      // потік t2
      Thread t2 = new Thread(Modifie);
      t2.Name = "t2";
      t2.Start(new Data { Duree = 5000, Nom = "zz", Client = client1 });
      // очікуємо завершення роботи обох потоків
      Console.WriteLine("Thread {0} -- début attente fin des deux threads", Thread.CurrentThread.Name);
      t1.Join();
      t2.Join();
      Console.WriteLine("Thread {0} -- fin attente fin des deux threads", Thread.CurrentThread.Name);
      // виводимо зміни — лише одна з них мала бути успішною
      using (var context = new RdvMedecinsContext())
      {
        // отримуємо client1 у client2
        Client client2 = context.Clients.Find(client1.Id);
        Console.WriteLine("Thread {0} client2", Thread.CurrentThread.Name);
        Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, client2.ShortIdentity());
      }
    }

    // потік
    static void Modifie(object infos)
    {
 ...
}
  • рядок 26: запускається порожній контекст;
  • рядок 29: поточний потік отримує ім’я, щоб відрізнити його від двох потоків, які будуть створені пізніше;
  • рядки 31–38: об’єкти [Medecin] та [Client] переводяться у стан «видалено»;
  • рядки 40–41: додаємо клієнта до контексту;
  • рядки 43–44: його відображають перед синхронізацією контексту;
  • рядок 46: синхронізація контексту з базою даних: об’єкти у стані «видалено» будуть видалені з бази даних. Об’єкт [Client], доданий до контексту, буде вставлений у базу даних. Це буде єдиний елемент у базі даних;
  • рядки 47–49: клієнт відображається після синхронізації контексту. На цьому етапі екранні відображення мають такий вигляд:
1
2
3
4
main client1--avant sauvegarde du contexte
Client[,xx,xx,xx,]
main client1--après sauvegarde du contexte
Client[33,xx,xx,xx,000001126209]

Слід зауважити, що після синхронізації контексту клієнт має первинний ключ та timestamp;

  • рядок 50: контекст закрито;
  • рядок 53: потік t1 пов’язаний із методом [Modifie] у рядку 84. Це означає, що після запуску він виконає метод [Modifie];
  • рядок 54: потоку t1 присвоюється ім’я;
  • рядок 55: запускається потік t1. Йому передаються параметри у вигляді структури [Data], визначеної в рядках 12–17:
    • Тривалість: потік зупиниться за Durée секунд до завершення свого виконання,
    • Клієнт: посилання на клієнта, якого потрібно оновити в базі даних,
    • Ім'я: ім'я, яке слід присвоїти цьому клієнту;
  • рядки 57–59: те саме з другим потоком. У підсумку два потоки спробують змінити в базі ім’я одного й того самого клієнта;
  • рядки 60–63: після запуску обох потоків головний потік очікує на завершення їх виконання;
  • рядок 62: очікування завершення потоку t1;
  • рядок 63: очікування завершення потоку t2;
  • рядок 64: невідомо, в якому порядку завершаться обидва потоки. Безперечно лише те, що на рядку 64 вони вже завершилися;
  • рядки 66–72: у новому контексті ми звертаємося до бази даних, щоб перевірити стан клієнта.

Тепер давайте подивимося, що роблять обидва потоки t1 і t2. Вони виконують такий метод [Modifie]:


static void Modifie(object infos)
    {
      // отримуємо параметр
      Data data = (Data)infos;
      try
      {
        using (var context = new RdvMedecinsContext())
        {
          Console.WriteLine("Début Thread {0}", Thread.CurrentThread.Name);
          // отримуємо client1 у client2
          Client client2 = context.Clients.Find(data.Client.Id);
          Console.WriteLine("Thread {0} client2", Thread.CurrentThread.Name);
          Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, client2.ShortIdentity());
          // змінюємо «клієнт2»
          client2.Nom = data.Nom;
          // трохи зачекаємо
          Thread.Sleep(data.Duree);
          // зберігаємо зміни
          context.SaveChanges();
        }
      }
      catch (Exception e)
      {
        // виняток
        Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, e);
      }
      // кінець потоку
      Console.WriteLine("Fin Thread {0}", Thread.CurrentThread.Name);
    }
  • рядок 4: отримуємо параметри потоку (Тривалість, Ім’я, Клієнт);
  • рядок 7: новий контекст;
  • рядок 11: клієнт передається в контекст;
  • рядки 12–13: перевірка стану клієнта;
  • рядок 15: змінюємо його ім’я;
  • рядок 17: потік зупиняється на Duree мілісекунд. Це має цікавий ефект. Пот відпускає процесор, який його виконував, поступаючись місцем іншому потоку. У нашому прикладі ми маємо три потоки: main, t1, t2. Потік main зупинений, очікуючи завершення потоків t1 і t2. Припустимо, що потік t1 першим отримав доступ до процесора, після чого він передає його потоку t2. Це призведе до того, що потік t2 прочитає точно те саме, що й потік t1, — того самого клієнта з тим самим timestamp;
  • рядок 19: контекст синхронізовано з базою даних. Припустимо знову, що потік t1 прокидається першим. Він збереже клієнта з іменем «yy». Він зможе це зробити, оскільки має той самий timestamp, що й у базі даних. Через це оновлення SGBD змінить timestamp. Коли по черзі прокинеться потік t2, він матиме клієнта з timestamp, відмінним від того, що зараз є в базі. Його оновлення буде відхилено.

На екрані відображається наступне:

main client1--перед збереженням контексту
Client[,xx,xx,xx,]
main client1--після збереження контексту
Client[33,xx,xx,xx,000001126209]
Thread main -- початок очікування завершення обох потоків
Début Thread t1
Début Thread t2
Thread t2 client2
Thread t2 Client[33,xx,xx,xx,000001126209]
Thread t1 client2
Thread t1 Client[33,xx,xx,xx,000001126209]
Fin Thread t2
Thread t1 System.Data.Entity.Infrastructure.DbUpdateConcurrencyException: Une instruction de mise à jour, d'insertion ou de suppression dans le magasin a affecté un nombre inattendu de lignes (0). Des entités ont peut-être é modifiées ou supprimées depuis leur chargement. Actualisez les entrées ObjectStateManager. ---> System.Data.OptimisticConcurrencyException: Інструкція на оновлення, вставку або видалення в сховищі вплинула на несподівану кількість рядків (0). Можливо, сутності були змінені або видалені з моменту їх створення
gement. Actualisez les entrées ObjectStateManager.
   à System.Data.Mapping.Update.Internal.UpdateTranslator.ValidateRowsAffected(I
nt64 rowsAffected, UpdateCommand source)
   ...
   à RdvMedecins_01.AccèsConcurrents.Modifie(Object infos) dans d:\data\istia-12
13\c#\dvp\Entity Framework\RdvMedecins\RdvMedecins-SqlServer-01\AccèsConcurrents
.cs:ligne 102
Fin Thread t1
Thread main -- завершення очікування, завершення обох потоків
Thread main client2
Thread main Client[33,xx,xx,zz,000001126210]
  • рядок 4: клієнт у базі даних;
  • рядок 9: клієнт, як його зчитує потік t2;
  • рядок 11: клієнт, як його зчитує потік t1. Отже, обидва потоки зчитали одне й те саме;
  • рядок 12: потік t2 завершується першим. Отже, він зміг виконати оновлення. Ім’я мало змінитися на «zz»;
  • рядок 13: потік t1 генерує виняток типу [System.Data.OptimisticConcurrencyException]. EF виявив, що не має правильного timestamp;
  • рядок 21: потік t1, у свою чергу, завершується;
  • рядок 22: головний потік завершив очікування;
  • рядок 24: головний потік відображає клієнта в базі даних. Перемогу здобув саме потік t2. Ім’я — «zz». Зауважимо, що timestamp змінився.

Тепер розглянемо інший аспект: транзакцію, яка забезпечує синхронізацію контексту персистентності з базою даних.

3.5.9. Синхронізація в транзакції

Таблиця [CRENEAUX] має обмеження унікальності, яке ми додали вручну (див. параграф 2.2.4, сторінка 12):

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

Ми будемо діяти наступним чином: додамо одночасно два записи на прийом до одного й того самого лікаря, на один і той самий день та в один і той самий часовий проміжок. Подивимося, що відбудеться.

Проект розвивається наступним чином:

Код програми [SynchronisationTransaction] такий:


using System;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{

  // тестова програма
  class SynchronisationTransaction
  {

    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // очищення поточної бази даних
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        context.SaveChanges();
      }

      // створюється клієнт
      Client client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
      // створено лікаря
      Medecin medecin1 = new Medecin { Nom = "xx", Prenom = "xx", Titre = "xx" };
      // створено час прийому для цього лікаря
      Creneau creneau1 = new Creneau { Hdebut = 8, Mdebut = 20, Hfin = 8, Mfin = 40, Medecin = medecin1 };
      // створено два записи про прийом для цього лікаря та цього клієнта, того самого дня, у тому самому часовому проміжку
      Rv rv1 = new Rv { Client = client1, Creneau = creneau1, Jour = new DateTime(2012, 10, 18) };
      Rv rv2 = new Rv { Client = client1, Creneau = creneau1, Jour = new DateTime(2012, 10, 18) };
      try
      {
        // вносимо всі ці дані в контекст збереження
        using (var context = new RdvMedecinsContext())
        {
          context.Clients.Add(client1);
          context.Creneaux.Add(creneau1);
          context.Medecins.Add(medecin1);
          context.Rvs.Add(rv1);
          context.Rvs.Add(rv2);
          // зберігаємо контекст — має виникнути виняток
          //, оскільки базовий об’єкт BD має обмеження унікальності, яке не дозволяє
          // наявності двох RDV в один і той самий день, у той самий часовий проміжок
          context.SaveChanges();
        }
      }
      catch (Exception e)
      {
        Console.WriteLine("Erreur : {0}", e);
      }
      // якщо збереження відбувається в транзакції, то в базу даних не мало бути вставлено нічого
      // через попереднє виключення — перевіряємо

      using (var context = new RdvMedecinsContext())
      {
        // клієнтів
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
        }
        // лікарів
        Console.WriteLine("Médecins--------------------------------------");
        var medecins = from medecin in context.Medecins select medecin;
        foreach (Medecin medecin in medecins)
        {
          Console.WriteLine(medecin);
        }
        // часові слоти
        Console.WriteLine("Créneaux horaires--------------------------------------");
        var creneaux = from creneau in context.Creneaux select creneau;
        foreach (Creneau creneau in creneaux)
        {
          Console.WriteLine(creneau);
        }
        // записи на прийом
        Console.WriteLine("Rendez-vous--------------------------------------");
        var rvs = from rv in context.Rvs select rv;
        foreach (Rv rv in rvs)
        {
          Console.WriteLine(rv);
        }
      }
    }
  }
}
  • рядки 15–27: використовується контекст збереження даних для очищення бази;
  • рядок 30: створення об’єкта [Client];
  • рядок 32: створення об’єкта [Medecin];
  • рядок 34: створення об’єкта [Creneau];
  • рядок 36: створення об’єкта [Rv];
  • рядок 37: створення другого об’єкта [Rv], ідентичного попередньому;
  • рядок 41: відкриття нового контексту;
  • рядки 43–47: раніше створені об’єкти прив’язуються до нового контексту. Зауважте, що з урахуванням залежностей ми могли б мінімізувати кількість операцій Add. Але операція EF оптимізує послідовність операцій SQL та INSERT, які мають бути виконані в базі даних;
  • рядок 51: контекст синхронізовано з базою даних. Як зазначено в коментарі, вставка одного з двох зустрічей має завершитися невдачею через обмеження унікальності в таблиці [RVS]. Але крім того, якщо синхронізація відбувається в рамках транзакції, все має бути скасовано. Отже, жодної вставки не має відбутися. База даних має залишатися порожньою;
  • рядок 53: контекст закрито;
  • рядки 61–90: виведення вмісту бази даних. Вона має бути порожньою.

На екрані відображається наступне:

Erreur : System.Data.Entity.Infrastructure.DbUpdateException: Une erreur s'est produite lors de la mise à jour des entrées. Pour plus d'informations, consultezl'exception interne. ---> System.Data.UpdateException: Під час оновлення записів сталася помилка. Для отримання додаткової інформації перегляньте внутрішнє виключення. ---> System.Data.SqlClient.SqlException: Порушено обмеження UNIQUE KEY «RVS_uq». Неможливо вставити дублікат ключа в об’єкт «dbo.RVS». Дублікат значення ключа: (18 жовтня 2012 р. 12:00AM, 34).
L'instruction a é arrêtée.
   à System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   à System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)...
    --— Кінець стека внутрішніх винятків ---
   ...
   à System.Data.Entity.DbContext.SaveChanges()
   à RdvMedecins_01.SynchronisationTransaction.Main(String[] args) dans d:\data\istia-1213\c#\dvp\Entity Framework\RdvMedecins\RdvMedecins-SqlServer-01\SynchronisationTransaction.cs:рядок 59
Clients--------------------------------------
Médecins--------------------------------------
Créneaux horaires--------------------------------------
Rendez-vous--------------------------------------
  • рядок 1: виняток через порушення обмеження унікальності в таблиці [RVS];
  • рядки 9–12: база дійсно порожня. Отже, синхронізація контексту з базою відбулася в рамках транзакції.

Безсумнівно, у EF 5 є й інші аспекти, які варто дослідити. Але ми знаємо достатньо, щоб повернутися до нашого дослідження багатошарової архітектури. На початку цього документа читач знайде посилання на статті та книги, які допоможуть йому поглибити свої знання про EF 5.

3.6. Дослідження багаторівневої архітектури на основі EF 5

Повернемося до нашого прикладу, описаного в параграфі 2. Йдеться про веб-додаток ASP.NET, структурований наступним чином:

Почнемо з побудови рівня [DAO] доступу до даних. Цей рівень базуватиметься на EF5.

3.6.1. Новий проєкт

Ми створюємо новий консольний проект VS 2012 [RdvMedecins-SqlServer-02] у поточному рішенні [1]:

Ми додаємо чотири папки [2], у яких розмістимо наші коди. Папка [Entites] є копією папки [Entites] з попереднього проєкту. Після цього копіювання з’являються помилки, пов’язані з тим, що у нас відсутні потрібні посилання. Нам потрібно додати посилання на Entity Framework 5. Для цього ми скористаємося методом, описаним у розділі 3.4 на сторінці 21. Список посилань виглядає наступним чином: [3]:

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


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Докладнішу інформацію про налаштування Entity Framework можна знайти на сайті http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
  </entityFramework>

  <!-- рядок підключення до бази даних -->
  <connectionStrings>
    <add name="monContexte"
         connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <!-- фабричний провайдер -->
  <system.data>
    <DbProviderFactories>
      <add name="SqlClient Data Provider"
       invariant="System.Data.SqlClient"
       description=".Net Framework Data Provider for SqlServer"
       type="System.Data.SqlClient.SqlClientFactory, System.Data,
     Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
    />
    </DbProviderFactories>
  </system.data>

</configuration>

3.6.2. Клас Exception

Ми будемо використовувати власний клас винятків для цього проєкту. Саме він буде виводитися з шару [DAO]:

Рівень [DAO] перехопить усі винятки, що будуть передані до нього, та інкапсулює їх у виняток типу [RdvMedecinsException]. Цей виняток матиме такий вигляд:


using System;

namespace RdvMedecins.Exceptions
{
  public class RdvMedecinsException : Exception
  {

    // властивості
    public int Code { get; set; }

    // конструктори
    public RdvMedecinsException()
      : base()
    {
    }

    public RdvMedecinsException(string message)
      : base(message)
    {
    }

    public RdvMedecinsException(int code, string message)
      : base(message)
    {
      Code = code;
    }

    public RdvMedecinsException(int code, string message, Exception ex)
      : base(message, ex)
    {
      Code = code;
    }

    // ідентифікатор
    public override string ToString()
    {
      if (InnerException == null)
      {
        return string.Format("RdvMedecinsException[{0},{1}]", Code, base.Message);
      }
      else
      {
        return string.Format("RdvMedecinsException[{0},{1},{2}]", Code, base.Message, base.InnerException.Message);
      }
    }
  }
}
  • рядок 5: клас походить від класу [Exception];
  • рядок 9: він додає до свого базового класу код помилки;
  • рядки 12–32: різні конструктори враховують наявність поля [Code].

Проект розвивається наступним чином:

3.6.3. Рівень [DAO]

Рівень [DAO] надає інтерфейс для рівня [ASP.NET]. Щоб ідентифікувати цей рівень, слід переглянути веб-сторінки додатка:

  • у [1] вище випадаючий список заповнено переліком лікарів. Цей перелік надаватиме рівень [DAO];
  • у [2] цей список надаватиме шар [DAO];
  • список прийомів лікаря на цей день,
  • список часових інтервалів прийому лікаря,
  • додаткову інформацію про обраного лікаря;
  • у [3] список клієнтів, що відкривається, буде надано шаром [DAO];
  • у [4] користувач підтверджує запис на прийом. Рівень [DAO] повинен мати можливість додати його до бази даних. Він також повинен мати можливість надати додаткову інформацію про обраного клієнта;
  • у [5] користувач видаляє зустріч. Рівень [DAO] повинен це забезпечувати.

З урахуванням цієї інформації інтерфейс [IDao] шару [DAO] може виглядати наступним чином:


using System;
using System.Collections.Generic;
using RdvMedecins.Entites;

namespace RdvMedecins.Dao
{
  public interface IDao
  {
    // список клієнтів
    List<Client> GetAllClients();
    // список лікарів
    List<Medecin> GetAllMedecins();
    // перелік часових проміжків лікаря
    List<Creneau> GetCreneauxMedecin(int idMedecin);
    // список RV певного лікаря на певний день
    List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
    // додати RV
    int AjouterRv(DateTime jour, int idCreneau, int idClient);
    // видалити RV
    void SupprimerRv(int idRv);
    // знайти об’єкт T за його первинним ключем
    T Find<T>(int id) where T : class;
  }
}

Методи в рядках 10–20 випливають із щойно проведеного аналізу. Метод у рядку 22 призначений для усунення наслідків роботи в режимі Lazy Loading. Якщо в шарі [ASP.NET] потрібна залежність від об’єкта, її буде отримано з бази даних за допомогою цього методу.

Реалізація [Dao] цього інтерфейсу буде такою:


using System;
using System.Collections.Generic;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Exceptions;
using RdvMedecins.Models;

namespace RdvMedecins.Dao
{
  public class Dao : IDao
  {

    //список клієнтів
    public List<Client> GetAllClients()
    {
      // список клієнтів
      List<Client> clients = null;
      try
      {
        // відкрити контекст збереження даних
        using (var context = new RdvMedecinsContext())
        {
          // список клієнтів
          clients = context.Clients.ToList();
        }

      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(1, "GetAllClients", ex);
      }
      // повернення результату
      return clients;
    }

    // список лікарів
    public List<Medecin> GetAllMedecins()
    {
      // список лікарів
      List<Medecin> medecins = null;
      try
      {
        // відкриття контексту збереження
        using (var context = new RdvMedecinsContext())
        {
          // список лікарів
          medecins = context.Medecins.ToList();
        }

      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(2, "GetAllMedecins", ex);
      }
      // повертається результат
      return medecins;
    }

    // список часових інтервалів для певного лікаря
    public List<Creneau> GetCreneauxMedecin(int idMedecin)
    {
   ...
    }

    // список RV лікаря за певний день
    public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
    {
 ...
    }

    // додати RV
    public int AjouterRv(DateTime jour, int idCreneau, int idClient)
    {
 ...
    }

    // видалити RV
    public void SupprimerRv(int idRv)
    {
...
    }

    // знайти клієнта
    public Client FindClient(int id)
    {
...
    }

    // знайти вільний час
    public Creneau FindCreneau(int id)
    {
 ...
    }

    // знайти лікаря
    public Medecin FindMedecin(int id)
    {
....
    }

    // знайти час прийому
    public Rv FindRv(int id){
...
    }

  }
}

Розглянемо метод [GetAllClients], який повинен повернути список усіх клієнтів:

  • рядки 18–31: пошук клієнтів здійснюється у блоці try/catch. Так само буде і в усіх наступних методах;
  • рядок 21: відкриття нового контексту;
  • рядок 24: сутності [Client] завантажуються в контекст і додаються до списку.

Метод [GetAllMedecins], який повинен повернути список усіх лікарів, є аналогічним (рядки 37–57).

Метод [GetCreneauxMedecin] виглядає наступним чином:


// перелік часових слотів певного лікаря
    public List<Creneau> GetCreneauxMedecin(int idMedecin)
    {
      // перелік часових проміжків
      try
      {
        // відкриття контексту збереження даних
        using (var context = new RdvMedecinsContext())
        {
          // отримання даних про лікаря та його часові слоти
          Medecin medecin = context.Medecins.Include("Creneaux").Single(m => m.Id == idMedecin);
          // перелік часових проміжків лікаря
          return medecin.Creneaux.ToList<Creneau>();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(3, "GetCreneauxMedecin", ex);
      }
    }
  • рядок 9: відкриття нового контексту збереження даних;
  • рядок 11: здійснюється пошук лікаря, первинний ключ якого відомий. Вимагається включити до нього залежність [Creneaux], яка є колекцією часових проміжків лікаря. Якщо лікаря не існує, метод Single генерує виняток;
  • рядок 13: повертається список вільних часів.

Метод [GetRvMedecinJour] повинен повернути список прийомів лікаря на певний день. Його код може виглядати так:


// список RV лікаря за певний день
    public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
    {
      // список прийомів
      List<Rv> rvs = null;

      try
      {
        // відкриття контексту збереження даних
        using (var context = new RdvMedecinsContext())
        {
          // отримання даних про лікаря
          Medecin medecin = context.Medecins.Find(idMedecin);
          if (medecin == null)
          {
            throw new RdvMedecinsException(10, string.Format("Médecin [{0}] inexistant", idMedecin));
          }
          // список прийомів
          rvs = context.Rvs.Where(r => r.Creneau.Medecin.Id == idMedecin && r.Jour == jour).ToList();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(4, "GetRvMedecinJour", ex);
      }
      // повернення результату
      return rvs;
    }
  • рядок 13: в контекст передається лікар, первинний ключ якого відомий;
  • рядки 14–17: якщо його немає, генерується виняток;
  • рядок 19: запит LINQ для отримання призначень цього лікаря;

Метод [AjouterRv] повинен додати прийом до бази даних і повернути первинний ключ вставленого елемента. Його код може виглядати так:


// додати RV
    public int AjouterRv(DateTime jour, int idCreneau, int idClient)
    {
      // номер доданого прийому
      int idRv;
      try
      {
        // відкриття контексту збереження даних
        using (var context = new RdvMedecinsContext())
        {
          // отримуємо часовий проміжок
          Creneau creneau = context.Creneaux.Find(idCreneau);
          if (creneau == null)
          {
            throw new RdvMedecinsException(5, string.Format("Créneau [{0}] inexistant", idCreneau));
          }
          // отримано дані про клієнта
          Client client = context.Clients.Find(idClient);
          if (client == null)
          {
            throw new RdvMedecinsException(6, string.Format("Client [{0}] inexistant", idCreneau));
          }
          // створення часового проміжку
          Rv rv = new Rv { Jour = jour, Client = client, Creneau = creneau };
          // додавання до контексту
          context.Rvs.Add(rv);
          // збереження контексту
          context.SaveChanges();
          // отримано первинний ключ доданого часового проміжку
          idRv = (int)rv.Id;
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(7, "AjouterRv", ex);
      }
      // результат
      return idRv;
    }
  • рядок 12: шукаємо час прийому в базі даних;
  • рядки 13–16: якщо його не знайдено, генерується виняток;
  • рядок 18: шукаємо клієнта зустрічі в базі даних;
  • рядки 19–22: якщо його не знайдено, генерується виняток;
  • рядок 24: створюється об’єкт [Rv] з необхідною інформацією;
  • рядок 26: додаємо його до контексту збереження;
  • рядок 28: синхронізуємо контекст збереження з базою даних. Після цього зустріч буде занесена до бази;
  • рядок 30: відомо, що після синхронізації бази первинні ключі вставлених елементів стають доступними. Отримуємо первинний ключ доданої зустрічі;
  • рядок 31: закриваємо контекст збереження.

Метод [SupprimerRv] повинен видалити зустріч, первинний ключ якої йому передано.


// видалення RV
    public void SupprimerRv(int idRv)
    {
      try
      {
        // відкриття контексту збереження
        using (var context = new RdvMedecinsContext())
        {
          // отримано Rv
          Rv rv = context.Rvs.Find(idRv);
          if (rv == null)
          {
            throw new RdvMedecinsException(5, string.Format("Rv [{0}] inexistant", idRv));
          }
          // видалення Rv
          context.Rvs.Remove(rv);
          // збереження контексту
          context.SaveChanges();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(8, "SupprimerRv", ex);
      }
    }
  • рядок 7: новий контекст збереження даних;
  • рядок 10: в контекст передається зустріч, яку потрібно видалити;
  • рядки 11–15: якщо його немає, генерується виняток;
  • рядок 16: видаляється з контексту;
  • рядок 18: синхронізуємо контекст із базою даних;
  • рядок 19: закриваємо контекст.

Метод [Find<T>] дозволяє шукати в базі даних об’єкт типу T за його первинним ключем. Його код може виглядати так:


public T Find<T>(int id)  where T : class
    {
      try
      {
        // відкриття контексту персистентності
        using (var context = new RdvMedecinsContext())
        {
          return context.Set<T>().Find(id);
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(20, "Find<T>", ex);
      }
    }
  • рядок 8: метод Set<T> дозволяє отримати об’єкт типу DbSet<T>, до якого можна застосовувати звичайні методи.

Проект розвивається наступним чином:

3.6.4. Тестування шару [DAO]

Ми створимо програму для тестування шару [DAO]. Архітектура тесту буде такою:

Консольна програма запитує [Spring.net] про створення екземпляра шару [DAO]. Після цього вона тестує різні функції інтерфейсу шару [DAO]. Замість консольної програми було б краще написати тестову програму типу NUnit. Тестова програма для шару [DAO] могла б виглядати так:


using System;
using System.Collections.Generic;
using RdvMedecins.Dao;
using RdvMedecins.Entites;
using RdvMedecins.Exceptions;
using Spring.Context.Support;

namespace RdvMedecins.Tests
{
  class Program
  {
    public static void Main()
    {
      IDao dao = null;
      try
      {
        // інстанціювання шару [DAO] за допомогою Spring
        dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;

        // відображення клієнтів
        List<Client> clients = dao.GetAllClients();
        DisplayClients("Liste des clients :", clients);

        // відображення лікарів
        List<Medecin> medecins = dao.GetAllMedecins();
        DisplayMedecins("Liste des médecins :", medecins);

        // список часових слотів лікаря № 0
        List<Creneau> creneaux = dao.GetCreneauxMedecin((int)medecins[0].Id);
        DisplayCreneaux(string.Format("Liste des créneaux horaires du médecin {0}", medecins[0]), creneaux);

        // список прийомів лікаря на певний день
        DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));

        // додати RV до лікаря № 1 у часовому проміжку № 0
        Console.WriteLine(string.Format("Ajout d'un RV au médecin {0} avec client {1} le 23/11/2013", medecins[0], clients[0]));
        int idRv1 = dao.AjouterRv(new DateTime(2013, 11, 23), (int)creneaux[0].Id, (int)clients[0].Id);
        Console.WriteLine("Rdv ajouté");
        DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));

        // додати прийом у вже зайнятий час — має викликати виняток
        int idRv2;
        Console.WriteLine("Ajout d'un RV dans un créneau déjà occupé");
        try
        {
          idRv2 = dao.AjouterRv(new DateTime(2013, 11, 23), (int)creneaux[0].Id, (int)clients[0].Id);
          Console.WriteLine("Rdv ajouté");
          DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
        }
        catch (RdvMedecinsException ex)
        {
          Console.WriteLine(string.Format("L'erreur suivante s'est produite : {0}", ex));
        }

        // видалити прийом
        Console.WriteLine(string.Format("Suppression du RV n° {0}", idRv1));
        dao.SupprimerRv(idRv1);
        DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
      }
      catch (Exception ex)
      {
        Console.WriteLine(string.Format("L'erreur suivante s'est produite : {0}", ex));
      }
      //пауза 
      Console.ReadLine();
    }

    // допоміжні методи — відображають списки
    public static void DisplayClients(string Message, List<Client> clients)
    {
      Console.WriteLine(Message);
      foreach (Client c in clients)
      {
        Console.WriteLine(c.ShortIdentity());
      }
    }
    public static void DisplayMedecins(string Message, List<Medecin> medecins)
    {
...
    }
    public static void DisplayCreneaux(string Message, List<Creneau> creneaux)
    {
...
    }
    public static void DisplayRvs(string Message, List<Rv> rvs)
    {
...
    }
  }
}
  • рядок 14: посилання на шар [DAO]. Щоб зробити тест незалежним від фактичної реалізації цього шару, це посилання має тип інтерфейсу [IDao], а не тип класу [Dao];
  • рядок 18: шар [DAO] інстанціюється Spring. Ми ще повернемося до конфігурації, необхідної для цього. Ми приводямо об’єктне посилання, повернене Spring, до типу інтерфейсу [IDao];
  • рядки 21–22: відображають клієнтів;
  • рядки 25–26: відображають лікарів;
  • рядки 29–30: відображають список вільних часів лікаря № 0;
  • рядок 33: виводить зустрічі лікаря № 0 на дату 23.11.2013. Їх не повинно бути;
  • рядок 37: додає прийом до лікаря № 0 на 23.11.2013;
  • рядок 39: відображає зустрічі лікаря № 0 на 23.11.2013. Їх має бути одна;
  • рядок 46: додаємо той самий прийом вдруге. Має виникнути виняток;
  • рядок 57: видаляється єдиний доданий прийом;
  • рядок 58: відображає зустрічі лікаря № 0 на дату 23.11.2013. Їх не повинно бути.

3.6.5. Конфігурація Spring.net

У наведеному вище тестовому програмі ми швидко пропустили інструкцію, яка створює екземпляр шару [DAO]:


dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;

Клас [ContextRegistry] — це клас Spring у просторі імен [Spring.Context.Support]. Щоб мати змогу використовувати Spring, нам потрібно додати його DLL до залежностей проєкту. Ми робимо це таким чином:

  • у [1] за допомогою інструменту [NuGet] шукаємо пакети;
  • в [2] шукаємо пакети в Інтернеті;
  • у [3] вводимо ключове слово spring у поле пошуку;
  • у [4] відображаються пакети, опис яких містить це ключове слово. У цьому випадку нам підходить [Spring.Core]. Встановлюємо його.

Посилання на проект змінюються наступним чином:

Пакет [Spring.Core] мав залежність від пакета [Common.Logging]. Останній також було завантажено. На цьому етапі проект більше не повинен містити помилок.

Але це ще не означає, що він запрацює. Спочатку нам потрібно налаштувати Spring у файлі [App.config]. Це найскладніша частина проекту. Новий файл [App.config] має такий вигляд:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Докладнішу інформацію про налаштування Entity Framework можна знайти на сайті http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
    <!-- spring -->
    <sectionGroup name="spring">
      <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
    </sectionGroup>
    <!-- загальне ведення журналів-->
    <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <!-- Entity Framework -->
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="v11.0" />
      </parameters>
    </defaultConnectionFactory>
  </entityFramework>
  <!-- ланцюги з'єднання -->
  <connectionStrings>
    <add name="monContexte" connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;" providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.data>
    <DbProviderFactories>
      <add name="SqlClient Data Provider" invariant="System.Data.SqlClient" description=".Net Framework Data Provider for SqlServer" type="System.Data.SqlClient.SqlClientFactory, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    </DbProviderFactories>
  </system.data>
  <!-- конфігурація Spring -->
  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
    </objects>
  </spring>
  <!-- конфігурація common.logging -->
  <logging>
    <factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
      <arg key="showLogName" value="true" />
      <arg key="showDataTime" value="true" />
      <arg key="level" value="DEBUG" />
      <arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
    </factoryAdapter>
  </logging>
</configuration>

Почнемо з видалення всього, що вже відомо: Entity Framework, рядки підключення, ProviderFactory. Файл виглядає наступним чином:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Докладнішу інформацію про конфігурацію Entity Framework можна знайти на сайті http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" ... />
    <!-- spring -->
    <sectionGroup name="spring">
      <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
    </sectionGroup>
    <!-- загальне ведення журналів-->
    <sectionGroup name="common">
      <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
    </sectionGroup>
  </configSections>
...
  <!-- конфігурація Spring -->
  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
    </objects>
  </spring>
  <!-- конфігурація common.logging -->
  <common>
    <logging>
      <factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
        <arg key="showLogName" value="true" />
        <arg key="showDataTime" value="true" />
        <arg key="level" value="DEBUG" />
        <arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
      </factoryAdapter>
    </logging>
  </common>
</configuration>
  • рядки 3–15: визначають розділи конфігурації;
  • рядок 8: визначає клас, який буде керувати розділом <spring><context> файлу XML (рядки 19–21);
  • рядок 9: визначає клас, який буде керувати розділом <spring><objects> у файлі XML (рядки 22–24);
  • рядок 13: визначає клас, який буде керувати розділом <common><logging> у файлі XML (рядки 27–36);
  • рядки 7–14: є стабільними. Їх не потрібно змінювати в іншому проєкті;
  • рядки 18–25: конфігурація Spring. Є стабільною, за винятком рядків 22–24, які визначають об’єкти, інстанції яких буде створювати Spring;
  • рядок 23: визначення об’єкта. Атрибут id є довільним. Це ідентифікатор об’єкта. Атрибут type вказує на клас, який потрібно інстанціювати, у форматі «повна назва класу, асемблер, що містить клас». Клас, про який йдеться, реалізує рівень [DAO]: [RdvMedecins.Dao.Dao]. Щоб дізнатися його асемблі, потрібно переглянути властивості проєкту:

У [1] — ім’я збірки, яку слід вказати;

  • рядки 27–36: конфігурація «Common Logging» є стабільною. Може знадобитися змінити рівень логування (рядок 32). Після завершення етапу налагодження рівень логування можна змінити на INFO.

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

  • рядки 22–24, які визначають об’єкти для інстанціювання;
  • рядок 32: рівень журналу.

У тестовій програмі інструкція, що створює екземпляр шару [DAO], має такий вигляд:


dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;

[ContextRegistry] — це клас Spring, який використовує конфігурацію Spring, визначену у файлі [Web.config] або [App.config]. У даному випадку він використовуватиме наступний розділ файлу [App.config]:


  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
    </objects>
</spring>
  • ContextRegistry.GetContext() використовує контекст рядків 2–4. Рядок 3 означає, що об’єкти Spring визначені в розділі [spring/objects] конфігураційного файлу. Цей розділ займає рядки 5–7;
  • ContextRegistry.GetContext().GetObject("rdvmedecinsDao") використовує розділ, що охоплює рядки 5–7. Вона повертає посилання на об’єкт, який має атрибут id= "rdvmedecinsDao". Це об’єкт, визначений у рядку 6. Потім Spring створює екземпляр класу, визначеного атрибутом type, використовуючи його конструктор без параметрів. Отже, цей конструктор повинен існувати. Після цього посилання на створений об’єкт повертається коду, що його викликає. Якщо об’єкт запитується в коді вдруге, Spring просто повертає посилання на перший створений об’єкт. Це шаблон проектування (Design Pattern), який називається «синглтон».

Створення об’єкта може бути складнішим. Можна використовувати конструктор з параметрами або вказати ініціалізацію певних полів об’єкта після його створення. Для отримання додаткової інформації з цієї теми можна ознайомитися зі статтею «Посібник Spring IOC для .NET», за посиланням URL [http://tahe.developpez.com/dotnet/springioc/].

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

Liste des clients :
Client[35,Mr,Jules,Martin,00000118981]
Client[36,Mme,Christine,German,00000118982]
Client[37,Mr,Jules,Jacquard,00000118983]
Client[38,Melle,Brigitte,Bistrou,00000118984]
Liste des médecins :
Medecin[26,Mme,Marie,Pelissier,00000118985]
Medecin[27,Mr,Jacques,Bromard,000001189110]
Medecin[28,Mr,Philippe,Jandot,000001189123]
Medecin[29,Melle,Justine,Jacquemot,000001189124]
Liste des créneaux horaires du médecin Medecin[26,Mme,Marie,Pelissier,00000118985]
Creneau[218,8,0,8,20, 26, 00000118986]
Creneau[219,8,20,8,40, 26, 00000118987]
Creneau[220,8,40,9,0, 26, 00000118988]
Creneau[221,9,0,9,20, 26, 00000118989]
Creneau[222,9,20,9,40, 26, 00000118990]
Creneau[223,9,40,10,0, 26, 00000118991]
Creneau[224,10,0,10,20, 26, 00000118992]
Creneau[225,10,20,10,40, 26, 00000118993]
Creneau[226,10,40,11,0, 26, 00000118994]
Creneau[227,11,0,11,20, 26, 00000118995]
Creneau[228,11,20,11,40, 26, 00000118996]
Creneau[229,11,40,12,0, 26, 00000118997]
Creneau[230,14,0,14,20, 26, 00000118998]
Creneau[231,14,20,14,40, 26, 00000118999]
Creneau[232,14,40,15,0, 26, 000001189100]
Creneau[233,15,0,15,20, 26, 000001189101]
Creneau[234,15,20,15,40, 26, 000001189102]
Creneau[235,15,40,16,0, 26, 000001189103]
Creneau[236,16,0,16,20, 26, 000001189104]
Creneau[237,16,20,16,40, 26, 000001189105]
Creneau[238,16,40,17,0, 26, 000001189106]
Creneau[239,17,0,17,20, 26, 000001189107]
Creneau[240,17,20,17,40, 26, 000001189108]
Creneau[241,17,40,18,0, 26, 000001189109]
Liste des RV du médecin Medecin[26,Mme,Marie,Pelissier,00000118985], le 23/11/2013 :
Ajout d'un RV au médecin Medecin[26,Mme,Marie,Pelissier,00000118985] avec client  Client[35,Mr,Jules,Martin,00000118981] le 23/11/2013
Rdv ajouté
Liste des RV du médecin Medecin[26,Mme,Marie,Pelissier,00000118985], le 23/11/2013 :
Rv[28,23/11/2013 00:00:00,35,218,00000289145]
Ajout d'un RV dans un créneau déjà occupé
L'erreur suivante s'est produite : RdvMedecinsException[7,AjouterRv,Une erreur s'est produite lors de la mise à jour des entrées. Pour plus d'informations, consultez l'exception interne.]
Suppression du RV n° 28
Liste des RV du médecin Medecin[26,Mme,Marie,Pelissier,00000118985], le 23/11/2013 :

Результати відповідають очікуваним. Відтепер ми вважатимемо, що наш шар [DAO] є дійсним. На цьому підручник можна було б завершити. До цього моменту ми показали:

  • основи роботи з ORM у Entity Framework 5;
  • шар [DAO], що використовує цей ORM.

Згадаймо наш приклад, описаний на початку цього документа. Ми виходимо з існуючого додатка з такою архітектурою:

яку ми хочемо перетворити на таку:

де EF5 замінило NHibernate. Ми щойно створили рівень [DAO2]. Насправді він не має того самого інтерфейсу, що й рівень [DAO1], інтерфейс якого був більш обмеженим:


  public interface IDao
  {
    // список клієнтів
    List<Client> GetAllClients();
    // список лікарів
    List<Medecin> GetAllMedecins();
    // список часових проміжків лікаря
    List<Creneau> GetCreneauxMedecin(int idMedecin);
    // список RV певного лікаря на певний день
    List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
    // додати RV
    int AjouterRv(DateTime jour, int idCreneau, int idClient);
    // видалити RV
    void SupprimerRv(int idRv);
  }

У цьому інтерфейсі шару [DAO2] було додано метод:


// знайти об’єкт T за його первинним ключем
T Find<T>(int id) where T : class;

Додавання цього методу пов’язане з тим, що ORM EF 5 за замовчуванням працює в режимі Lazy Loading. Елементи надходять у шар [ASP.NET] без своїх залежностей. Зазначений вище метод дозволяє нам отримувати їх у разі потреби, а в деяких випадках це дійсно потрібно. NHibernate також за замовчуванням працює в режимі Lazy Loading, але я використовував його в режимі Eager Loading. Ентітети надходили в шар [ASP.NET] разом зі своїми залежностями.

Ми завершимо перенесення додатків ASP.NET / NHibernate на ASP.NET / EF 5. Однак, оскільки це більше не стосується EF5, ми не будемо коментувати веб-код. Ми просто пояснимо, як налаштувати веб-додаток та протестувати його. Додаток доступний на сайті цього посібника.

3.6.6. Генерація DLL на основі шару [DAO]

У такій архітектурі:

шар [ASP.NET] матиме у своєму розпорядженні шари, розташовані праворуч від нього, у вигляді DLL. Отже, ми створюємо DLL на основі шару [DAO].

  • у [1] вибираємо тестову програму, а в [2] не включаємо її до DLL, що буде згенеровано;
  • у [3] у властивостях проєкту вказується, що збірка, яку потрібно створити, — це DLL;
  • у [4], у меню VS вказується, що буде згенеровано збірку типу [Release], яка містить менше інформації, ніж збірка типу [Debug];
  • у [5] виконується перегенерація збірки проекту. Буде згенеровано DLL;
  • у 6 відображаються всі файли проекту;
  • у [7] — файл DLL для проекту шару [DAO]. Саме цей файл використовуватиме веб-проект ASP.NET;
  • у [8] ми оновлюємо відображення проекту;
  • у [9] файли DLL з папки [Release] об’єднуються у зовнішню папку [lib], що знаходиться в папці [10]. Саме звідти веб-проект буде отримувати свої посилання.

3.6.7. Рівень [ASP.NET]

Тут ми пояснимо процес перенесення додатка [ASP.NET / NHibernate] у додаток [ASP.NET / EF 5]. Ми будемо працювати з Visual Studio Express 2012 для веб-розробки, який доступний безкоштовно на URL [http://www.microsoft.com/visualstudio/fra/downloads].

Ми будемо працювати на основі існуючого веб-проєкту, створеного за допомогою VS 2010.

  • у [1] відкриваємо існуючий проєкт:
  • у [2] завантажений проєкт має такі посилання: [3]:
  • [NHibernate] є DLL фреймворку NHibernate,
  • [Spring.Core] є DLL фреймворку Spring.net,
  • [log4net] — це DLL фреймворку журналів log4net. Цей фреймворк використовується Spring.net,
  • [MySql.Data] є драйвером ADO.NET для SGBD MySQL,
  • [rdvmedecins] є DLL для шару [DAO], побудованого з використанням NHibernate;
  • у [4] ми змінюємо назву проєкту, а в [5] видаляємо попередні посилання;
  • у 6 додаємо посилання на проект;
  • у [7] у майстрі використовуємо опцію [Parcourir];
  • у [8] ми вибираємо всі DLL з проекту № 2, які раніше були поміщені в папку [lib];
  • у [9] — зведений звіт, який ми затверджуємо;
  • у форматі [10] — веб-проект із новими посиланнями.

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

  • у [1] код управління веб-сторінками розподілено між двома файлами: [Global.asax] та [Default.aspx]. Утилітарний код розміщено у папці [Entites]. Нарешті, налаштування додатка здійснюється за допомогою файлу [Web.config];
  • у файлі [2] ми генеруємо збірку проекту;
  • у файлі [3] з’являються помилки.

Розглянемо помилки, наприклад таку:

Image

та її пояснення:

Image

Тип [medecin.Id] — це int?, тоді як метод [GetCreneauxMedecin] має тип int. Тому потрібно використовувати cast. Ця помилка повторюється у всьому коді, оскільки сутності проєкту ASP.NET / NHibernate мали первинні ключі типу int, тоді як у проєктах ASP.NET / EF 5 вони мають тип int?. Виправляємо всі помилки такого типу та перегенеруємо проєкт. Після цього їх більше немає.

Перед запуском проекту нам залишається вирішити ще одну деталь: інстанціювання шару [DAO] фреймворком Spring. Це здійснюється у файлі [Global.asax]:


protected void Application_Start(object sender, EventArgs e)
    {
      // закешуємо певні дані з бази даних
      try
      {
        // інстанціювання шару [dao]
        Dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
        ...
      }
      catch (Exception ex)
      {...
      }
    }

У тестовій програмі шару [DAO] цей шар інстанціювався таким чином:


dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;

Обидва методи є ідентичними. Нагадаємо, що це створення екземпляра шару [DAO] ґрунтувалося на конфігурації, виконаній у [App.config]. Тоді замінюємо поточний вміст [Web.config] веб-проєкту на вміст [App.config] з проєкту шару [DAO], щоб отримати ту саму конфігурацію.

Ми готові до першого запуску. Відображається головна сторінка [1]:

  • у [2] вводимо дату зустрічі та підтверджуємо;
  • на [3] — помилка.

Якщо розглянути текст помилки, що відображається на сторінці, можна побачити, що повідомлено про виняток, пов’язаний із Lazy Loading: було зроблено спробу завантажити залежність об’єкта, хоча контекст персистентності, який ним керує, було закрито. Об’єкт тепер перебуває у стані «від’єднаний». Ця помилка пов’язана з тим, що NHibernate використовувався в режимі Eager Loading, тоді як EF за замовчуванням працює в режимі Lazy Loading. У рядку, виділеному червоним кольором вище:

  • rdv — це об’єкт [Rv], який було завантажено без його залежностей;
  • щоб обчислити rdv.Creneau.Id, додаток намагається завантажити залежність rdv.Creneau. Але оскільки ми вже не перебуваємо в цьому контексті, це неможливо, звідси й виникає виняток.

У цьому випадку рішення просте. У рядку 108 створюється запис у словнику, ключем якого є первинний ключ часового проміжку зустрічі. Виявляється, що суть [Rv] інкапсулює первинний ключ пов’язаного часового проміжку. Отже, пишемо:


        dicoRvPris[(int)rdv.CreneauId] = rdv;

Спробуємо виконати код ще раз. Цього разу з’являється така помилка:

Помилка аналогічна. У рядку 132 відбувається спроба завантажити залежність [Client] об’єкта [Rv] у шар ASP.NET, тобто поза контекстом. Необхідно отримати об’єкт [Client] із бази даних. Саме для усунення цієї проблеми інтерфейс [IDao] було доповнено таким методом:


    // знайти суть T за її первинним ключем
    T Find<T>(int id) where T : class;

Він дозволить отримати залежності. Таким чином, наведений вище рядок з помилкою буде переписано наступним чином:


        Client client = Global.Dao.Find<Client>(agenda.Creneaux[i].Rdv.ClientId);

Знову слід зазначити, що сутності містять свої зовнішні ключі. У цьому випадку сутність [Rv] надає нам доступ до зовнішнього ключа пов’язаної залежності [Creneau]. Після внесення цих двох виправлень додаток працює. Пропонуємо читачам протестувати додаток [RdvMedecins-SqlServer-03], який можна завантажити разом із прикладами з веб-сайту цієї статті.

3.7. Conclusion

Ми успішно завершили перенесення додатка ASP.NET / NHibernate:

на додаток ASP.NET / EF 5:

Хоча ця архітектура мала б дозволити нам зберегти шар [ASP.NET] без змін, нам довелося його модифікувати з двох причин:

  • ентітети не були абсолютно однаковими. Тип первинних ключів ентітетів NHibernate був int, тоді як у EF 5 — int?. Це змусило нас ввести cast у веб-код;
  • режим завантаження сутностей для цих двох ORM був різним: Eager Loading для NHibernate, Lazy Loading для EF 5. Це спонукало нас розширити інтерфейс шару [DAO] за допомогою узагальненого методу, що дозволяє отримувати суть за її первинним ключем.

Проте перенесення виявилося досить простим, що ще раз підтвердило, якщо це взагалі було потрібно, доцільність багатошарової архітектури та ін’єкції залежностей за допомогою Spring або іншого фреймворку для ін’єкції залежностей.

Тепер ми оцінимо вплив зміни SGBD на попередню архітектуру. Ми перенесемо всі попередні проєкти на чотири інші SGBD:

  • Oracle Database Express Edition 11g, версія 2;
  • MySQL 5.5.28;
  • PostgreSQL 9.2.1;
  • Firebird 2.1.

Коди більше не змінюватимуться. Змінюватимуться лише такі елементи:

  • визначення в сутностях поля, що використовується для контролю конкуренції доступу до сутності;
  • конфігураційні файли [App.config] або [Web.config];

Ми коментуватимемо лише ті елементи, які змінюються.