3. Studium przypadku z wykorzystaniem SQL Server Express 2012
3.1. Introduction
Przykłady dotyczące Entity Framework znalezione w Internecie to w większości przykłady z wykorzystaniem serwera SQL. Jest to całkiem normalne. Prawdopodobnie jest to najpowszechniejszy serwer SGBD na świecie, a w przedsiębiorstwach najczęściej spotykany jest serwer NET. Podążymy za tym trendem. Przykłady zostaną następnie rozszerzone na wszystkie bazy danych wymienione w punkcie 1.2.
3.2. Instalacja narzędzi
Nie będziemy opisywać instalacji narzędzi. Wymagałoby to bowiem ogromnej liczby zrzutów ekranu, które dość szybko stają się nieaktualne. Jest to zadanie (co prawda nie zawsze łatwe), które pozostawiamy czytelnikowi.
Musimy zainstalować następujące narzędzia:
- SGBD, SQL oraz Server Express 2012: [http://www.microsoft.com/fr-fr/download/details.aspx?id=29062]. Należy pobrać wersję „With Tools”, która wraz z SGBD zawiera narzędzie administracyjne:
Po zainstalowaniu pliku SGBD uruchamiamy go:
![]() |
![]() |
- [1]: w menu Start uruchom „Menedżer konfiguracji serwera SQL”;
- [2]: w tym menedżerze uruchom serwer;
- [3]: serwer został uruchomiony.
Teraz uruchamiamy narzędzie administracyjne serwera SQL:
![]() |
- [1]: w menu Start uruchom „SQL Server Management Studio”;
- [2]: narzędzie administracyjne.
Połączymy się z serwerem:
![]() |
- w oknie [1] uruchamiamy eksplorator obiektów;
- w [2] podajemy parametry połączenia:
- [3]: serwer (lokalny) (należy zwrócić uwagę na niezbędne nawiasy) oznacza serwer zainstalowany na komputerze,
- [4]: wybieramy uwierzytelnianie systemu Windows. Aby połączenie się powiodło, należy posiadać uprawnienia administratora na swoim komputerze,
- [5]: nawiązuje się połączenie;
![]() |
- [6]: połączenie nawiązane;
- [7]: chcemy zmienić niektóre właściwości serwera;
![]() |
- [8]: żądamy, aby dostępne były dwa tryby uwierzytelniania:
- uwierzytelnianie systemu Windows, tak jak zostało to właśnie wykorzystane. Użytkownik systemu Windows posiadający odpowiednie uprawnienia może wówczas się zalogować,
- uwierzytelnianie serwera SQL. Użytkownik musi należeć do grupy użytkowników zarejestrowanych w SGBD;
Po wykonaniu tej czynności można zatwierdzić właściwości serwera;
- [9]: edytujemy właściwości użytkownika „sa” (administrator systemu);
![]() |
- w [10] należy ustawić dla niego hasło. W dalszej części dokumentu hasło to brzmi: sqlserver2012;
![]() |
- w [10] przyznajemy mu uprawnienie do logowania;
- w [11] połączenie zostaje aktywowane. Tym samym kreator można zatwierdzić;
- w [12] następuje rozłączenie z serwerem.
Teraz ponownie łączymy się, używając loginu sa/sqlserver2012:
![]() |
- w [1] następuje ponowne połączenie;
- w [2], w procesie uwierzytelniania SQL Server;
- w [3], użytkownikiem jest sa;
- w [4], jego hasło to sqlserver2012;
- w [5] następuje logowanie;
![]() |
- w [6] jesteśmy zalogowani.
Teraz utworzymy bazę demonstracyjną:
![]() |
- w [1] tworzymy nową bazę o nazwie BD;
- w [2] nazwiemy ją „demo”;
- w [3] zatwierdzamy;
![]() |
- w [4] utworzono bazę danych;
- w [5] tworzymy nową tabelę w bazie demo;
![]() |
![]() |
![]() |
![]() |
- w [6] definiujemy tabelę z dwiema kolumnami: ID i NOM;
- w pliku [7] kolumna [ID] zostaje ustawiona jako klucz główny;
- w [8] klucz główny jest oznaczony symbolem klucza;
- w [9] zapisujemy tabelę;
- w [10] nadaje się jej nazwę;
- w [11], aby tabela pojawiła się w bazie [demo], należy zaktualizować bazę;
- w [12] tabela [PERSONNES] została pomyślnie utworzona.
Na razie wiemy już wystarczająco dużo o korzystaniu z narzędzia administracyjnego serwera SQL.
3.3. Wbudowany serwer (localdb)\v11.0
VS Express 2012 jest wyposażony w serwer wbudowany SQL. Zakładamy tutaj, że VS Express 2012 został zainstalowany jako [http://www.microsoft.com/visualstudio/fra/downloads]. Uruchamiamy VS 2012 [1]:
![]() |
Uruchamiamy narzędzie administracyjne serwera SQL Server 2012 [2] i logujemy się [3].
![]() |
- w [4], należy połączyć się z serwerem (localdb)\v11.0;
- w [5], z uwierzytelnianiem systemu Windows;
- w pliku [6] po pomyślnym nawiązaniu połączenia wyświetlane są bazy danych serwera. Podobnie jak poprzednio można utworzyć nową bazę danych.
Nie będziemy korzystać z tego serwera wbudowanego w VS 2012.
3.4. Tworzenie bazy danych na podstawie encji
Entity Framework 5 Code First umożliwia utworzenie bazy danych na podstawie encji. Właśnie to teraz zobaczymy. W programie VS Express 2012 tworzymy pierwszy projekt konsolowy w języku C#:
![]() |
![]() |
- w pliku [1] znajduje się definicja projektu;
- w pliku [2] – utworzony projekt.
Wszystkie nasze projekty będą wymagały biblioteki DLL z Entity Framework 5. Dodajemy ją:
![]() |
- w pliku [1] narzędzie NuGet umożliwia pobranie zależności;
![]() |
- na [2], pobieramy zależność Entity Framework;
- w [3] odwołanie zostało dodane do projektu.
Więcej informacji można uzyskać, sprawdzając właściwości dodanego odwołania:
![]() |
- w pliku [1] – wersja pliku DLL. Potrzebna jest wersja 5;
- w przypadku [2], jego lokalizacja w systemie plików: <solution>\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll, gdzie <solution> to folder rozwiązania VS. Wszystkie pakiety dodane przez NuGet trafią do folderu <solution>/packages;
- w [3] utworzono plik [packages.config]. Jego zawartość jest następująca:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EntityFramework" version="5.0.0" targetFramework="net45" />
</packages>
Zawiera on listę pakietów zaimportowanych przez NuGet.
Wróćmy do projektu VS i utwórzmy w nim folder [Models]:
![]() |
- w [1], dodanie folderu do projektu;
- w [2], będzie się nazywał [Models].
W dalszej części będziemy stosować tę praktykę i umieszczać definicje naszych encji w folderze [Models].
Aby utworzyć nasze encje, skorzystamy z definicji bazy danych MySQL 5 wykorzystywanej w projekcie NHibernate. Przypomnijmy rolę encji EF:
![]() |
Entities muszą odzwierciedlać tabele bazy danych. Warstwa dostępu do danych korzysta z tych entities zamiast pracować bezpośrednio z tabelami. Zacznijmy od tabeli [MEDECINS]:
3.4.1. Entyteta [Medecin]
Zawiera informacje o lekarzach obsługiwanych przez aplikację [RdvMedecins].
![]() | ![]() |
- ID: numer identyfikacyjny lekarza – klucz główny tabeli
- VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
- NOM: nazwisko lekarza
- PRENOM: jego imię
- TITRE: jego tytuł (panna, pani, pan)
Możemy wyjść od następującej klasy [Medecin]:
using System;
[Table("MEDECINS", Schema = "dbo")]
namespace RdvMedecins.Entites
{
public class Medecin
{
// dane
public int Id { get; set; }
public string Titre { get; set; }
public string Nom { get; set; }
public string Prenom { get; set; }
}
- wiersz 3: klasa [Medecin] jest powiązana z tabelą [MEDECINS] w bazie danych. Tabela ta znajduje się w schemacie o nazwie „dbo”.
Umieszczamy tę klasę w pliku [Entites.cs] [1]. To właśnie tam umieścimy wszystkie nasze encje.
![]() |
Wciąż w folderze [Models] tworzymy następujący plik [Context.cs]:
using System.Data.Entity;
using RdvMedecins.Entites;
namespace RdvMedecins.Models
{
// kontekst
public class RdvMedecinsContext : DbContext
{
// lekarze
public DbSet<Medecin> Medecins { get; set; }
}
// inicjalizacja bazy
public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
{
}
}
- wiersz 8: klasa [RdvMedecinsContext] będzie reprezentować kontekst trwałości c.-à-d. całość encji zarządzanych przez ORM. Musi ona wywodzić się z klasy [System.Data.Entity.DbContext];
- wiersz 11: pole [Medecins] będzie reprezentować encje typu [Medecin] z kontekstu trwałości. Jest to typ DbSet<Medecin>. Zazwyczaj liczba instancji [DbSet] odpowiada liczbie tabel w bazie danych – po jednej na tabelę;
- wiersz 15: definiuje się klasę [RdvMedecinsInitializer] w celu zainicjowania utworzonej bazy danych. W tym przypadku klasa ta wywodzi się z klasy [DropCreateDataBaseAlways], która – jak wskazuje jej nazwa – usuwa bazę, jeśli już istnieje, a następnie tworzy ją ponownie. Jest to przydatne na etapie tworzenia klasy BD. Parametrem klasy [DropCreateDataBaseAlways] jest typ kontekstu trwałości powiązanego z bazą danych. Jako klasę nadrzędną dla klasy inicjalizacyjnej można użyć innych klas niż [DropCreateDataBaseAlways]:
- [DropCreateDatabaseIfModelChanges]: odtwarza bazę, jeśli zmieniły się encje,
- [CreateDatabaseIfNotExists]: tworzy bazę, jeśli nie istnieje;
Pozostaje nam jeszcze utworzyć program główny. Będzie to następujący program: [CreateDB_01.cs]:
using System;
using System.Data.Entity;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class CreateDB_01
{
static void Main(string[] args)
{
// tworzymy bazę danych
Database.SetInitializer(new RdvMedecinsInitializer());
using (var context = new RdvMedecinsContext())
{
context.Database.Initialize(false);
}
}
}
}
- wiersz 12: [System.Data.Entity.DataBase] to klasa oferująca metody statyczne do zarządzania bazą powiązaną z kontekstem trwałości. Metoda statyczna [SetInitializer] pozwala określić klasę inicjalizującą bazę. Nie uruchamia to jednak samej inicjalizacji;
- wiersz 13: aby pracować z kontekstem trwałości, należy go zainicjować. Właśnie to robimy w tym miejscu. Używamy klauzuli
using, aby kontekst został automatycznie zamknięty po wyjściu z tej klauzuli. Zatem w wierszu 17 kontekst zostaje zamknięty; - wiersz 15: jawnie uruchamiamy generowanie bazy powiązanej z kontekstem trwałości [RdvMedecinsContext]. Parametr false wskazuje, że operacja ta nie powinna zostać wykonana, jeśli została już przeprowadzona dla tego kontekstu. W tym miejscu równie dobrze można by wpisać true.
Podczas pracy z bazą danych parametry połączenia są zazwyczaj zapisywane w pliku [App.config]. Zauważmy, że na razie ich tam nie ma:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- Więcej informacji na temat konfiguracji Entity Framework można znaleźć na stronie 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>
Powyższe elementy zostały zapisane w pliku [App.config] po dodaniu zależności Entity Framework do odwołań projektu.
Uruchommy projekt (Ctrl-F5) po uruchomieniu serwera SQL Server Express (to ważne):
![]() | ![]() |
Kompilacja powinna zakończyć się bez błędów. Otwórzmy teraz narzędzie administracyjne serwera SQL i odświeżmy widok:
![]() |
Widzimy, że utworzono bazę o pełnej nazwie klasy [RdvMedecinsContext], która zawiera tabelę [dbo.MEDECINS] (taką nazwę jej nadaliśmy) z kolumnami odpowiadającymi nazwom pól encji [Medecin]. Jeśli kod został poprawnie wykonany, a powyższa baza danych nie pojawia się, należy sprawdzić serwer wbudowany (localdb)\v11.0 (patrz strona 19). W przypadku VS 2012 Pro serwer ten jest używany, jeśli serwer SQL nie jest aktywny w momencie wykonywania kodu. W przypadku VS 2012 Express – nie.
Przyjrzyjmy się strukturze tabeli [MEDECINS]:
- zawiera nazwy pól encji [Medecin];
- kolumna [Id] jest kluczem głównym. Jest to konwencja stosowana w EF: jeśli encja E posiada pole Id lub Eid (MedecinId), wówczas kolumna ta jest kluczem głównym w powiązanej tabeli;
- typy kolumn w tabeli odpowiadają typom pól w encji;
- w przypadku kolumn Tytuł, Nazwisko, Imię zastosowano typ [nvarchar(max)]. Można by być bardziej precyzyjnym, np. 5 znaków dla tytułu, 30 dla nazwiska i imienia;
- kolumny „Tytuł”, „Nazwisko” i „Imię” mogą mieć wartość NULL. Zmienimy to.
Przyjrzyjmy się właściwościom klucza głównego [Id]:
![]() |
W przypadku [1] widać, że klucz główny jest typu [Identité], co oznacza, że jego wartość jest generowana automatycznie przez serwer SQL. Zastosujemy tę strategię do wszystkich SGBD.
Ograniczymy stosowanie konwencji EF poprzez wykorzystanie adnotacji. Kod encji w [Entites.cs] przyjmuje następującą postać:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
{
// dane
[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; }
}
}
- wiersze 2 i 3: adnotacje znajdują się w przestrzeniach nazw [System.ComponentModel.DataAnnotations] (Key, Required, MaxLength) oraz [System.ComponentModel.DataAnnotations.Schema] (Column). Inne adnotacje znajdują się w przestrzeniach nazw URL i [http://msdn.microsoft.com/en-us/data/gg193958.aspx];
- wiersz 11: [Key] oznacza klucz główny;
- wiersz 12: [Column] określa nazwę kolumny odpowiadającej polu;
- wiersz 14: [Required] wskazuje, że pole jest obowiązkowe (SQL, NOT, NULL);
- wiersz 15: [MaxLength] określa maksymalną długość ciągu znaków, a [MinLength] – minimalną;
Uruchommy projekt z tą nową definicją jednostki [Medecin]. Utworzona baza danych wygląda wówczas następująco:
![]() |
- kolumny mają nadane im nazwy;
- adnotacja [Required] została przekształcona na SQL, NOT i NULL;
- adnotacja [MaxLength(N)] została przekształcona na typ SQL nvarchar(N).
W aplikacji NHibernate kolumna [VERSION] służyła do zapobiegania równoczesnym dostępom do tego samego wiersza w tabeli. Zasada działania jest następująca:
- proces P1 odczytuje wiersz L z tabeli [MEDECINS] w momencie T1. Wiersz ten ma wersję V1;
- proces P2 odczytuje ten sam wiersz L z tabeli [MEDECINS] w momencie T2. Wiersz ma wersję V1, ponieważ proces P1 nie zatwierdził jeszcze swojej zmiany;
- proces P1 zatwierdza swoją zmianę w wierszu L. Wersja wiersza L zmienia się wówczas na V2 = V1 + 1;
- proces P2 zatwierdza swoją zmianę w wierszu L. Proces ORM zgłasza wówczas wyjątek, ponieważ proces P2 posiada wersję wiersza L o numerze V1, różną od wersji V2 znalezionej w bazie danych.
Nazywa się to optymistycznym zarządzaniem dostępem współbieżnym. W przypadku EF 5 pole pełniące tę rolę musi posiadać jeden z dwóch atrybutów: [Timestamp] lub [ConcurrencyCheck]. SQL Server ma typ [timestamp]. W przypadku kolumny o tym typie wartość jest automatycznie generowana przez SQL Server przy każdym wstawieniu lub modyfikacji wiersza. Taka kolumna może wówczas służyć do zarządzania współbieżnością dostępu. Wracając do poprzedniego przykładu, proces P2 znajdzie timestamp różniący się od tego, który odczytał, ponieważ w międzyczasie zmiana dokonana przez proces P1 spowodowała jego modyfikację.
Nasz obiekt [Medecin] zmienia się w następujący sposób:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
{
// dane
[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; }
}
}
- wiersze 26–28: nowa kolumna z atrybutem [Timestamp] z wiersza 27. Typ pola musi być typu byte[] (wiersz 28). Nazwa pola może być dowolna. Nie nadaje się mu atrybutu [Required], ponieważ to nie aplikacja dostarczy tę wartość, lecz sam SGBD.
Jeśli uruchomimy projekt z tym nowym elementem, baza danych zmieni się w następujący sposób:
![]() |
Pozostała nam jeszcze jedna kwestia do wyjaśnienia. Kontekst trwałości „wie”, że dana entyteta musi zostać wstawiona do bazy danych, ponieważ w tym momencie jej klucz podstawowy ma wartość null. To właśnie wstawienie do bazy danych nada wartość kluczowi podstawowemu. W tym przypadku typ int przypisany do klucza głównego [Id] nie jest odpowiedni, ponieważ ten typ nie akceptuje wartości null. Nadajemy mu zatem typ int?, który akceptuje wartości int oraz wskaźnik null. Wykorzystywana entyteta [Medecin] będzie zatem wyglądać następująco:
public class Medecin
{
// dane
[Key]
[Column("ID")]
public int? Id { get; set; }
...
Pozostaje nam jeszcze sprawdzić, jak w jednostce reprezentować pojęcie klucza obcego między tabelami.
3.4.2. Entyteta [Creneau]
Tabela [CRENEAUX] zawiera listę przedziałów czasowych, w których możliwe są RV:
![]() |
![]() |
- ID: numer identyfikujący przedział czasowy – klucz główny tabeli
- VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
- ID_MEDECIN: numer identyfikujący lekarza, do którego należy ten przedział czasowy – klucz obcy w kolumnie MEDECINS(ID).
- HDEBUT: godzina rozpoczęcia terminu
- MDEBUT: minuty początku przedziału czasowego
- HFIN: godzina zakończenia przedziału czasowego
- MFIN: minuty zakończenia przedziału czasowego
Drugi wiersz tabeli [CRENEAUX] (por. [1] powyżej) wskazuje na przykład, że przedział nr 2 rozpoczyna się o godz. 8:20 i kończy o godz. 8:40 oraz należy do lekarza nr 1 (pani Marie PELISSIER).
Na podstawie tej wiedzy możemy zdefiniować encję [Creneau] w tabeli [Entites.cs] w następujący sposób:
[Table("CRENEAUX", Schema = "dbo")]
public class Creneau
{
// dane
[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; }
}
Jedyna zmiana dotyczy wierszy 20–21. Fakt, że tabela [CRENEAUX] posiada klucz obcy do tabeli [MEDECINS], znajduje odzwierciedlenie w encji [Creneau] poprzez obecność odniesienia do encji [Medecin], wiersz 21. Nazwa pola nie ma znaczenia, liczy się jedynie typ. Właściwość ta musi zostać zadeklarowana jako wirtualna za pomocą słowa kluczowego virtual. W rzeczywistości EF ma za zadanie przedefiniować wszystkie tzw. właściwości nawigacyjne, tj. te, które odpowiadają kluczowi obcemu i umożliwiają przejście z jednej tabeli do drugiej.
Aby przetestować nową encję, musimy wprowadzić kilka zmian w [Context.cs]:
using System.Data.Entity;
using RdvMedecins.Entites;
namespace RdvMedecins.Models
{
// kontekst
public class RdvMedecinsContext : DbContext
{
// podmioty
public DbSet<Medecin> Medecins { get; set; }
public DbSet<Creneau> Creneaux { get; set; }
}
// inicjalizacja bazy
public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
{
}
}
Wiersz 12 odzwierciedla fakt, że kontekst ma do zarządzania o jedną encję więcej. Po uruchomieniu projektu otrzymujemy następującą nową bazę danych:
![]() |
Tabela [CRENEAUX] została pomyślnie utworzona, a nowością jest obecność kluczy obcych [1] i [2]. Jej nazwa została wygenerowana na podstawie nazwy odpowiedniego pola w encji (Medecin) z dodanym sufiksem „_Id”. Aby sprawdzić właściwości tego klucza obcego, spróbujmy go zmodyfikować na [3].
![]() |
Powyższy zrzut ekranu pokazuje, że [Medecin_Id] jest kluczem obcym tabeli [CRENEAUX] i odnosi się do klucza głównego [ID] w tabeli [MEDECINS].
Jeśli tworzymy encje dla istniejącej bazy danych, kolumna klucza obcego niekoniecznie będzie nosiła nazwę [Medecin_Id]. W przypadku pozostałych kolumn widzieliśmy już, że adnotacja [Column] rozwiązuje ten problem. Co dziwne, w przypadku klucza obcego sprawa jest bardziej skomplikowana. Należy postępować w następujący sposób:
public class Creneau
{
// dane
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
...
}
- wiersze 5–7: tworzymy pole typu klucza obcego (int). Za pomocą atrybutu [Column] określamy nazwę kolumny, która będzie kluczem obcym w tabeli powiązanej z encją;
- wiersz 9: dodajemy adnotację [ForeignKey] do pola typu [Medecin]. Argumentem tej adnotacji jest nazwa pola (a nie kolumny), które jest powiązane z kolumną klucza obcego w tabeli.
Wykonanie projektu tworzy tym razem następującą tabelę:
![]() |
Jak widać powyżej, kolumna klucza obcego nosi nazwę, którą jej nadaliśmy. Należy zauważyć, że pola:
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
dały początek tylko jednej kolumnie, a mianowicie kolumnie [MEDECIN_ID]. Niemniej jednak obecność pola [MedecinId] jest istotna. Podczas odczytu wiersza z tabeli [CRENEAUX] otrzyma on wartość z kolumny [MEDECIN_ID], tj. wartość klucza obcego z tabeli [MEDECINS]. Jest to często przydatne.
Powyższe pole [Medecin] odzwierciedla relację „wiele do jednego”, która łączy encję [Creneau] z encją [Medecin]. Kilka obiektów [Creneau] jest powiązanych z jednym obiektem [Medecin]. Odwrotną relację, w której jeden obiekt [Medecin] jest powiązany z wieloma obiektami [Creneau], można zamodelować za pomocą dodatkowego pola w encji [Medecin]:
public class Medecin
{
// dane
[Key]
[Column("ID")]
public int? Id { get; set; }
...
public ICollection<Creneau> Creneaux { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
W wierszu 8 dodano pole [Creneaux], które jest zbiorem obiektów [Creneau]. Pole to zapewni nam dostęp do wszystkich terminów wizyt u lekarza.
Po ponownym uruchomieniu projektu widać, że tabela [MEDECINS] pozostała bez zmian:
![]() |
Nie dodano żadnej kolumny. Relacja klucza obcego istniejąca między tabelą [CRENEAUX] a tabelą [MEDECINS] wystarcza, aby tabela EF mogła wygenerować pola z nią powiązane:
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; }
...
}
Wiemy już najważniejsze rzeczy. Możemy zakończyć tworzeniem pozostałych dwóch podmiotów.
3.4.3. Entytety [Client] i [Rv]
Korzystając z zdobytej wiedzy, możemy utworzyć jednostki [Client] i [Rv]. Jednostka [Client] zawiera informacje o klientach obsługiwanych przez aplikację [RdvMedecins].
![]() | ![]() |
- ID: numer identyfikacyjny klienta – klucz główny tabeli
- VERSION: numer identyfikujący wersję wiersza w tabeli. Liczba ta jest zwiększana o 1 za każdym razem, gdy wprowadzana jest zmiana w wierszu.
- NOM: nazwisko klienta
- PRENOM: imię klienta
- TITRE: tytuł (panna, pani, pan)
Entyteta [Client] mogłaby wyglądać następująco:
[Table("CLIENTS", Schema = "dbo")]
public class Client
{
// dane
[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; }
// terminy spotkań z klientem
public ICollection<Rv> Rvs { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
Klasa [Client] jest niemal identyczna z klasą [Medecin]. Można by je wyprowadzić z tej samej klasy nadrzędnej. Nowością jest wiersz 21. Odzwierciedla on fakt, że klient może mieć kilka spotkań, i wynika z obecności klucza obcego z tabeli [RVS] do tabeli [CLIENTS].
Entyteta [Rv] reprezentuje spotkanie:
![]() |
- ID: numer jednoznacznie identyfikujący RV – klucz główny
- JOUR: dzień RV
- ID_CRENEAU: przedział czasowy dla RV – klucz obcy w kolumnie [ID] tabeli [CRENEAUX] – określa zarówno przedział czasowy, jak i danego lekarza.
- ID_CLIENT: numer klienta, dla którego dokonano rezerwacji – klucz obcy w kolumnie [ID] tabeli [CLIENTS]
Entyteta [Rv] mogłaby wyglądać następująco:
[Table("MEDECINS", Schema = "dbo")]
public class Rv
{
// dane
[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; }
}
- wiersze 5–7: klucz główny;
- wiersze 8–10: data spotkania;
- wiersze 11–12: klucz obcy z tabeli [RVS] do tabeli [CLIENTS];
- wiersze 13–15: klient, który ma spotkanie;
- wiersze 16–17: klucz obcy z tabeli [RVS] do tabeli [CRENEAUX];
- wiersze 18–20: przedział czasowy spotkania;
- wiersze 21–23: pole zarządzania dostępem współbieżnym.
W wierszu 17 widać relację „wiele do jednego”: jeden przedział czasowy może odpowiadać wielu spotkaniom (nie w tym samym dniu). Relację odwrotną można odzwierciedlić w encji [Creneau]:
public class Creneau
{
// terminy spotkania dla danego przedziału czasowego
public ICollection<Rv> Rvs { get; set; }
...
}
Wiersz 4 zawiera zbiór spotkań umówionych w tym przedziale czasowym.
Po uruchomieniu projektu generowana jest następująca baza danych:
![]() |
Tabele [MEDECINS] i [CRENEAUX] nie uległy zmianie. Tabele [CLIENTS] i [RVS] mają następujący wygląd:
![]() | ![]() |
Tak właśnie miało być. Pozostało nam jeszcze dopracować kilka szczegółów:
- zarządzać nazwą bazy danych. W tym przypadku została ona wygenerowana przez EF;
- wypełnienie bazy danymi.
3.4.4. Ustalanie nazwy bazy danych
Aby ustalić nazwę bazy wygenerowanej przez EF, wykorzystamy ciąg połączenia zdefiniowany w [App.config]. Ten plik konfiguracyjny zmienia się w następujący sposób:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- Więcej informacji na temat konfiguracji Entity Framework można znaleźć na stronie 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>
<!-- łańcuch połączenia z bazą danych -->
<connectionStrings>
<add name="RdvMedecinsContext"
connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- dostawca fabryki -->
<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>
- wiersze 15–19: ciąg połączenia z bazą danych;
- wiersz 16: atrybut [name] przejmuje nazwę klasy [RdvMedecinsContext] używanej dla kontekstu trwałości. Należy o tym pamiętać. Ograniczenie to można obejść w konstruktorze kontekstu:
// konstruktor
public RdvMedecinsContext()
: base("monContexte")
{
}
W tym przypadku możemy otrzymać wartość name= „monContexte”. Tak właśnie będzie wyglądać dalsza część dokumentu.
- wiersz 17: ciąg połączenia. [Data Source]: nazwa serwera, na którym znajduje się SGBD, [Initial Catalog]: nazwa bazy danych, czyli w tym przypadku [rdvmedecins-ef], [User Id]: właściciel połączenia, [Password]: jego hasło. Czytelnik dostosuje ten ciąg znaków do swojego środowiska;
- wiersze 21–29: definiują [DbProviderFactory]. Nie wiem, co to jest. Sądząc po nazwie, może to być klasa umożliwiająca wygenerowanie warstwy [ADO.NET], która oddziela EF od SGBD:
![]() |
W rzeczywistości te wiersze są zbędne dla serwera SQL, ale musiałem je dodać dla pozostałych serwerów SGBD. Umieszczam je tutaj więc dla przypomnienia. Nie przeszkadzają. Jedynym istotnym punktem jest wersja z wiersza 27. Jest to wersja z plików DLL i [System.Data], które znajdują się w odnośnikach projektu:
![]() |
To wszystko. Jesteśmy gotowi. Uruchamiamy projekt i otrzymujemy następującą bazę [rdvmedecins-ef]:
![]() |
To będzie nasza ostateczna baza danych. Pozostaje nam jeszcze wprowadzić do niej dane.
3.4.5. Wypełnianie bazy
Do wstawiania danych do bazy można wykorzystać klasę inicjalizacyjną bazy:
public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
{
// inicjalizacja bazy
public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
{
protected override void Seed(RdvMedecinsContext context)
{
base.Seed(context);
// inicjowanie bazy danych
// klienci
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);
}
// lekarze
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);
}
// terminy wizyt
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);
}
// wizyty
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
}
}
}
- wiersz 6: inicjalizacja odbywa się w metodzie [Seed]. Metoda ta istnieje w klasie nadrzędnej. Tutaj została ona ponownie zdefiniowana. Argumentem jest kontekst trwałości [RdvMedecinsContext] aplikacji;
- wiersz 8: argument jest przekazywany do klasy nadrzędnej; prawdopodobnie otwiera ona przekazany jej kontekst trwałości, ponieważ dalsze otwieranie tego kontekstu nie jest już konieczne;
- wiersze 11–16: utworzenie 4 klientów;
- wiersze 17–20: klienci ci są dodawani do kontekstu trwałości, a dokładniej do jego lekarzy. Należy zwrócić uwagę na metodę [Add], która to umożliwia. Należy tu przypomnieć definicję kontekstu:
public class RdvMedecinsContext : DbContext
{
// jednostki
public DbSet<Medecin> Medecins { get; set; }
public DbSet<Creneau> Creneaux { get; set; }
public DbSet<Client> Clients { get; set; }
public DbSet<Rv> Rvs { get; set; }
...
Mówi się również, że klienci zostali powiązani z kontekstem, tzn. są teraz zarządzani przez EF. Wcześniej nie byli z nim powiązani. Istnieli jako obiekty, ale nie byli zarządzani przez EF;
- wiersze 21–27: utworzenie 4 lekarzy;
- wiersze 28–31: umieszczamy ich w kontekście trwałości;
- wiersze 33–70: utworzenie terminów. Wiersze 34–57 dotyczą lekarza medecins[0], wiersze 58–69 – lekarza medecins[1]. Pozostali lekarze nie mają przypisanych terminów;
- wiersze 71–74: umieszczamy te przedziały czasowe w kontekście trwałości;
- wiersz 76: utworzenie wizyty dla pierwszego klienta w pierwszym przedziale czasowym i umieszczenie jej w kontekście trwałości.
Po uruchomieniu projektu otrzymujemy następującą bazę danych:
![]() | ![]() |
Powyżej widoczna jest wypełniona tabela [CLIENTS].
3.4.6. Modyfikacja encji
Obecnie klasy [Medecin] i [Client] są niemal identyczne. W rzeczywistości, jeśli usunie się pola dodane w celu zarządzania trwałością w klasie EF 5, są one identyczne. Sprawimy, by pochodziły one od klasy [Personne]. Obie te encje przyjmą wówczas następujący wygląd:
// osoba
public abstract class Personne
{
// dane
[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; }
// podpis
public override string ToString()
{
return String.Format("[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// skrót podpisu
public string ShortIdentity()
{
...
}
// narzędzie
private string dump(byte[] timestamp)
{
...
}
}
[Table("MEDECINS", Schema = "dbo")]
public class Medecin : Personne
{
// terminy wizyt u lekarza
public ICollection<Creneau> Creneaux { get; set; }
// podpis
public override string ToString()
{
return String.Format("Medecin {0}", base.ToString());
}
}
[Table("CLIENTS", Schema = "dbo")]
public class Client : Personne
{
// terminy wizyt pacjenta
public ICollection<Rv> Rvs { get; set; }
// podpis
public override string ToString()
{
return String.Format("Client {0}", base.ToString());
}
}
Po uruchomieniu projektu otrzymujemy tę samą bazę danych. EF 5 zmapował najniższe klasy w łańcuchu dziedziczenia, przypisując każdą z nich do osobnej tabeli. W rzeczywistości EF 5 stosuje różne strategie generowania tabel w celu odwzorowania dziedziczenia encji. Nie będziemy ich tutaj omawiać. Można zapoznać się na przykład z artykułem „ Entity Framework Code First Inheritance: Table Per Hierarchy and Table Per Type”, dostępnym pod adresem URL [http://www.codeproject.com/Articles/393228/Entity-Framework-Code-First-Inheritance-Table-Per].
Od tej pory będziemy korzystać z tej wersji encji.
3.4.7. Dodawanie ograniczeń do bazy danych
Pozostała nam jeszcze jedna kwestia do dopracowania. Tabela [RVS] zawierająca terminy wygląda następująco:
![]() |
Ta tabela musi posiadać ograniczenie unikalności: w danym dniu przedział czasowy lekarza może zostać zarezerwowany tylko raz na wizytę. W kontekście tabeli oznacza to, że para (JOUR,CRENEAU_ID) musi być unikalna. Nie wiem, czy to ograniczenie można wyrazić bezpośrednio w kodzie, czy to na poziomach encji, czy w kontekście. Jest to prawdopodobne, ale nie sprawdziłem tego. Zastosujemy inne podejście. Wykorzystamy klient administracyjny serwera SQL, aby dodać to ograniczenie.
W programie „SQL Server Management Studio” nie znalazłem prostego sposobu na dodanie tego ograniczenia poza wykonaniem polecenia SQL, które je tworzy:
![]() |
- w [1] tworzy się zapytanie SQL dla bazy [rdvmedecins-ef];
- w [2] zapytanie SQL, które tworzy ograniczenie unikalności;
- w [3] wykonanie tego zapytania spowodowało utworzenie nowego indeksu w tabeli [RVS].
Istnieją również inne narzędzia do administrowania serwerem SQL. W tym przypadku skorzystamy z narzędzia EMS SQL Manager for SQL Server Freeware [http://www.sqlmanager.net/fr/products/mssql/manager/download]. Po zainstalowaniu uruchamiamy je:
![]() |
- w [1] zapisujemy bazę danych;
- w [2] łączymy się z serwerem (lokalnym);
- w [3], z uwierzytelnieniem SQL Server;
- w pliku [4] – pod identyfikatorem „sa”;
- w [5] oraz hasłem „sqlserver2012”;
- w [6] przechodzi się do następnego etapu;
![]() |
- w [7] wybieramy bazę danych [rdvmedecins-ef];
- w [8] kończymy pracę z kreatorem;
- w [9] baza pojawia się w drzewie baz. Łączymy się z nią w [10];
- w [11] – połączenie zostało nawiązane.
„SQL Manager Lite for SQL Server” umożliwia utworzenie ograniczenia unikalności w tabeli [RVS].
![]() |
- w [1] widoczny jest warunek unikalności, który utworzyliśmy wcześniej;
- w tabeli [2] usuwamy ją;
- w [3] zniknął indeks odpowiadający temu ograniczeniu unikalności.
Ponownie tworzymy usunięty warunek:
![]() |
- w [1] tworzymy nowy indeks dla tabeli [RVS];
- w [2] nadajemy mu nazwę;
- w [3] jest to ograniczenie unikalności;
- w [4], na kolumnach JOUR i CRENEAU_ID;
Zakładka DDL podaje nam kod SQL, który zostanie wykonany:
![]() |
- w [6] kompilujemy polecenie SQL;
![]() |
- na [7], potwierdzamy;
- w [8] pojawił się nowy indeks.
Interfejs oferowany przez „SQL Manager Lite for SQL server” jest analogiczny do interfejsu oferowanego przez „SQL Server Management Studio”. Podobne interfejsy można znaleźć dla serwerów SGBD Oracle, PostgreSQL, Firebird oraz MySQL. Dlatego też będziemy odtąd kontynuować pracę z tą rodziną narzędzi administracyjnych SGBD.
Aby uzyskać dostęp do informacji zawartych w tabeli, wystarczy dwukrotnie kliknąć na nią:
![]() |
Informacje dotyczące wybranej tabeli są dostępne w zakładkach. Powyżej widoczna jest zakładka [Fields] tabeli [CLIENTS]. Zakładka [Data] wyświetla zawartość tabeli:

3.4.8. Ostateczna baza danych
Mamy już naszą ostateczną bazę danych. Eksportujemy jej skrypt SQL, aby w razie potrzeby móc ją odtworzyć.
![]() |
- do [1], początek kreatora;
- do [2] – serwer;
- na [3] – baza danych, która ma zostać wyeksportowana;
![]() |
- w pliku [4] należy podać nazwę pliku, w którym zostanie zapisany skrypt SQL;
- w polu [5] należy określić kodowanie;
- w polu [6] należy określić, co ma zostać wyodrębnione (tabele, ograniczenia, dane);
![]() |
- w [7] można doprecyzować skrypt, który zostanie wygenerowany;
- w [8] zakończ pracę kreatora.
Skrypt został wygenerowany i załadowany do edytora skryptów. Można przejrzeć wygenerowany kod SQL. Na podstawie tego skryptu odbudujemy bazę danych.
![]() |
- w [1] usuwamy bazę;
- w [2] i [3] odtwarzamy ją;
![]() |
- w pliku [4] następuje uwierzytelnienie;
- w [5] uruchamia się skrypt SQL służący do utworzenia bazy danych;
![]() |
- w [6] zapisujemy ją w „SQL Manager”;
- w [7] nawiązuje się połączenie z właśnie utworzoną bazą danych;
![]() |
- w [8] baza danych nie zawiera na razie żadnych tabel;
- w [9a] otwieramy edytor skryptów SQL;
![]() |
- w [9b] otwieramy wcześniej utworzony skrypt SQL;
- w [10] uruchamia się go;
![]() |
- w pliku [11] utworzono tabele;
- w pliku [12] zostały one wypełnione;
![]() |
- w [14] odnajdujemy ograniczenie unikalności, które utworzyliśmy dla tabeli [RVS].
Będziemy teraz pracować z tą istniejącą bazą danych. Jeśli zostanie ona zniszczona lub uszkodzona, wiemy, jak ją odtworzyć.
3.5. Wykorzystanie bazy danych za pomocą Entity Framework
Będziemy:
- dodawać, usuwać i modyfikować elementy bazy;
- wykonywać zapytania do bazy danych przy użyciu LINQ to Entities;
- zarządzać równoczesnym dostępem do tego samego elementu bazy;
- zapoznać się z pojęciami Lazy Loading / Eager Loading;
- dowiedzieć się, że aktualizacja bazy danych przez kontekst trwałości odbywa się w ramach transakcji.
3.5.1. Usuwanie elementów z kontekstu trwałości
Mamy wypełnioną bazę danych. Opróżnimy ją. Tworzymy nową klasę [Erase.cs] w bieżącym projekcie [1]:
![]() |
Klasa [Erase] wygląda następująco:
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class Erase
{
static void Main(string[] args)
{
using (var context = new RdvMedecinsContext())
{
// czyści się bieżącą bazę
// klienci
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
// lekarze
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// zapisywanie kontekstu trwałości
context.SaveChanges();
}
}
}
}
- wiersz 9: operacje na kontekście trwałości są zawsze wykonywane w klauzuli [using]. Gwarantuje to, że po wyjściu z [using] kontekst został zamknięty;
- wiersz 13: przeglądamy kontekst klientów [context.Clients]. Wszyscy klienci z bazy zostaną umieszczeni w kontekście trwałości;
- wiersz 15: dla każdego z nich wykonywana jest operacja [Remove], która usuwa ich z kontekstu. W rzeczywistości nadal znajdują się oni w kontekście, ale w stanie „usunięty”;
- wiersze 18–21: wykonujemy to samo dla lekarzy;
- wiersz 23: zapisujemy kontekst trwałości w bazie danych.
Podczas zapisywania kontekstu w bazie danych elementy kontekstu, które:
- mają klucz główny o wartości null, są przedmiotem operacji SQL INSERT;
- znajdują się w stanie „usunięte”, są przedmiotem operacji SQL DELETE;
- znajdują się w stanie „zmieniony” – podlegają operacji SQL UPDATE;
Jak zauważymy później, operacje te SQL są wykonywane w ramach jednej transakcji. Jeśli jedna z nich zakończy się niepowodzeniem, wszystkie wcześniejsze działania zostaną cofnięte.
Ustawmy program [Erase] jako nowy obiekt startowy projektu [1], a następnie uruchommy projekt.
![]() |
Sprawdźmy bazę danych. Zauważymy, że wszystkie tabele są puste ([2]). Jest to zaskakujące, ponieważ poprosiliśmy jedynie o usunięcie lekarzy i klientów. To właśnie dzięki mechanizmowi kluczy obcych pozostałe tabele zostały kaskadowo opróżnione.
Definicja klucza obcego z tabeli [CRENEAUX] do tabeli [MEDECINS] została zdefiniowana przez dostawcę EF 5 w następujący sposób:
![]() |
- w tabeli [1] należy wybrać tabelę [CRENEAUX];
- w [2] wybiera się zakładkę kluczy obcych;
- w [3] edytuje się jedyny klucz obcy;
![]() |
- w [4], w zakładce DDL, definicja SQL ograniczenia klucza obcego;
- W tabeli [5] klauzula ON DELETE CASCADE powoduje, że usunięcie lekarza prowadzi do usunięcia przypisanych mu terminów.
Ograniczenia klucza obcego w tabeli [RVS] są zdefiniowane w podobny sposób:
- wiersze 1–6: usunięcie klienta spowoduje również usunięcie powiązanych z nim terminów wizyt;
- wiersze 1–6: usunięcie przedziału czasowego spowoduje również usunięcie wszystkich powiązanych z nim spotkań.
3.5.2. Dodawanie elementów do kontekstu trwałości
Teraz, gdy opróżniliśmy bazę danych, ponownie ją wypełnimy. Dodajemy do projektu program [Fill.cs] [1].
![]() |
Program [Fill.cs] wygląda następująco:
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class Fill
{
static void Main(string[] args)
{
using (var context = new RdvMedecinsContext())
{
// opróżnianie bieżącej bazy
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// resetujemy ją
// klienci
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);
}
// lekarze
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);
}
// przedziały czasowe
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);
}
// spotkania
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
// zapisujemy kontekst trwałości
context.SaveChanges();
}
}
}
}
- wiersz 10: otwieramy kontekst trwałości;
- wiersze 13–20: wiersze z tabel [CLIENTS] i [MEDECINS] są umieszczane w kontekście, a następnie z niego usuwane. Jak właśnie widzieliśmy, powodowało to całkowite opróżnienie bazy danych;
- wiersze 22–88: do kontekstu trwałości dodawane są elementy. Wszystkie mają klucz główny ustawiony na null. Zostaną więc wstawione do bazy danych;
- wiersz 90: zmiany wprowadzone w kontekście są synchronizowane z bazą danych. Baza ta zostanie poddana serii operacji SQL DELETE, a następnie serii operacji SQL INSERT;
Ustawiamy program [Fill] jako nowy obiekt startowy projektu [1], a następnie uruchamiamy ten ostatni.
![]() |
W [2] widać, że tabele zostały wypełnione.
3.5.3. Wyświetlanie zawartości bazy danych
Teraz wyświetlimy zawartość bazy danych za pomocą zapytań LINQ to Entity. LINQ (język zapytań INtegrated) pojawił się wraz z frameworkiem .NET 3.5 w 2007 roku. Stanowi on rozszerzenie języków .NET i c.a.d, z którymi jest zintegrowany, a jego składnia jest sprawdzana przez kompilator. Umożliwia on wysyłanie zapytań do różnych kolekcji przy użyciu składni wykazującej podobieństwa do języka SQL (Structured Query Language) służącego do wysyłania zapytań do baz danych. Istnieją różne wersje języka LINQ:
- LINQ to Object, służąca do wysyłania zapytań do kolekcji przechowywanych w pamięci;
- LINQ to XML, służąca do wysyłania zapytań do XML;
- LINQ do Entity, w celu wysyłania zapytań do baz danych;
Aby istnieć, LINQ opiera się na licznych rozszerzeniach wprowadzonych do języków .NET. Rozszerzenia te mogą być wykorzystywane poza LINQ. Nie będziemy ich tutaj omawiać, a jedynie podamy dwa źródła, w których czytelnik znajdzie szczegółowy opis LINQ:
- „LINQ in Action”, autorstwa Fabrice’a Marguerie, Steve’a Eicherta i Jima Wooleya, wydawnictwo Manning;
- „LINQ pocket reference”, autorstwa Josepha i Bena Albahari, wydane przez O’Reilly.
Przeczytałem pierwszą z nich i uważam, że jest znakomita. Drugiej nie czytałem, ale przeczytałem książkę tych samych autorów „C# 3.0 in a nutshell”, która ukazała się w tym samym czasie co „LINQ”. Uważam, że ta książka znacznie przewyższa poziomem przeciętne pozycje, które zazwyczaj czytam. Wygląda na to, że pozostałe książki tych dwóch autorów są na tym samym poziomie. Będziemy również korzystać z LINQPad, narzędzia do nauki języka LINQ autorstwa Josepha Albahari.
Wyświetlimy encje znajdujące się w bazie. W tym celu dodamy do ich klas dwie metody wyświetlania. Zacznijmy od encji [Medecin]:
// lekarz
public class Medecin
{
// dane
[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; }
// przedziały czasowe lekarza
public ICollection<Creneau> Creneaux { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
// podpis
public override string ToString()
{
return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// skrót podpisu
public string ShortIdentity()
{
return ToString();
}
// narzędzie
private string dump(byte[] timestamp){
string str = "";
foreach (byte b in timestamp)
{
str += b;
}
return str;
}
}
- wiersze 27–30: metoda ToString klasy. Należy zauważyć, że nie wyświetla ona kolekcji z wiersza 21;
- wiersze 32–37: metoda ShortIdentity, która działa w ten sam sposób.
W tym miejscu należy wyjaśnić pojęcia ładowania leniwego (Lazy Loading) i ładowania natychmiastowego (Eager Loading), aby ocenić wpływ obu poprzednich metod. Widzieliśmy, że dana jednostka może być zależna od innej jednostki. Zależności te mogą być dwojakiego rodzaju:
- od jednego do wielu, jak powyżej, gdzie lekarz jest powiązany z wieloma terminami wizyt;
- od wielu do jednego, jak w przypadku encji [Creneau] poniżej, gdzie wiele terminów jest powiązanych z tym samym lekarzem;
public class Creneau
{
// dane
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
...
}
Gdy zależności są ładowane jednocześnie z encjami, do których są przypisane, mówi się o ładowaniu typu „Eager Loading”. W przeciwnym razie mówi się o „Lazy Loading”: zależności są ładowane dopiero przy pierwszym odwołaniu do nich. Domyślnie EF 5 korzysta z „Lazy Loading”: zależności nie są ładowane jednocześnie z encją.
Przyjrzyjmy się naszej metodzie [ToString] powyżej:
// godziny przyjęć lekarza
public ICollection<Creneau> Creneaux { get; set; }
// podpis
public override string ToString()
{
return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// skrót podpisu
public string ShortIdentity()
{
return ToString();
}
Metoda [ToString] nie wyświetla zależności [Creneaux] z linii 2. Gdyby to zrobiła, wymusiłaby wówczas załadowanie wszystkich terminów wizyt lekarza przed jej wykonaniem. Aby uniknąć tego kosztownego ładowania, zależność nie została uwzględniona w sygnaturze encji. Ogólnie rzecz biorąc, w każdej encji uwzględnimy dwie sygnatury:
- metodę ToString, która wyświetli encję wraz z jej ewentualnymi zależnościami typu „wiele do jednego”. Jak właśnie wyjaśniono, spowoduje to załadowanie zależności;
- metoda ShortIdentity, która nie będzie odwoływać się do żadnych zależności. Nie nastąpi zatem żadne ładowanie zależności;
Metody wyświetlania pozostałych encji będą następujące:
Entyteta [Client]:
public class Client
{
// dane
...
// terminy wizyt pacjenta
public ICollection<Rv> Rvs { get; set; }
// podpis
public override string ToString()
{
return String.Format("Client[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// skrócony podpis
public string ShortIdentity()
{
return ToString();
}
}
- wiersze 9–12: metoda [ToString] nie wyświetla zależności z wiersza 6;
Jednostka [Creneau]:
public class Creneau
{
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
// terminy spotkania
public ICollection<Rv> Rvs { get; set; }
// podpis
public override string ToString()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
}
// krótki podpis
public string ShortIdentity()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Timestamp, MedecinId, dump(Timestamp));
}
}
- wiersz 16: metoda [ToString] odwołuje się do zależności z wiersza 9. Spowoduje to jej załadowanie;
- wiersz 11: zależność [Rvs] nie jest odwołana. Nie zostanie ona załadowana;
- wiersze 21–22: metoda [ShortIdentity] nie odwołuje się już do odwołania [Medecin] z wiersza 9. Nie zostanie ono zatem załadowane.
Entyteta [Rv]:
public class Rv
{
// dane
...
[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; }
// podpis
public override string ToString()
{
return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, Client, Creneau, dump(Timestamp));
}
// krótki podpis
public string ShortIdentity()
{
return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, ClientId, CreneauId, dump(Timestamp));
}
}
- wiersze 17–20: metoda [ToString] odwołuje się do zależności z wierszy 9 i 14. Spowoduje to ich załadowanie;
- wiersze 17–20: metoda [ShortIdentity] zapobiega temu, więc zależności nie zostaną załadowane.
Podsumowując, należy zwrócić uwagę na metody [ToString] w encjach. Jeśli nie zwróci się na to uwagi, wyświetlenie tabeli może spowodować załadowanie połowy bazy danych, jeśli tabela ma wiele zależności.
Mając to na uwadze, piszemy następujący nowy kod [Dump.cs]:
using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;
namespace RdvMedecins_01
{
class Dump
{
static void Main(string[] args)
{
// zrzut bazy danych
using (var context = new RdvMedecinsContext())
{
// klienci
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
// lekarze
Console.WriteLine("Médecins--------------------------------------");
var medecins = from medecin in context.Medecins select medecin;
foreach (Medecin medecin in medecins)
{
Console.WriteLine(medecin);
}
// terminy wizyt
Console.WriteLine("Créneaux horaires--------------------------------------");
var creneaux = from creneau in context.Creneaux select creneau;
foreach (Creneau creneau in creneaux)
{
Console.WriteLine(creneau);
}
// wizyty
Console.WriteLine("Rendez-vous--------------------------------------");
var rvs = from rv in context.Rvs select rv;
foreach (Rv rv in rvs)
{
Console.WriteLine(rv);
}
}
}
}
}
Wyjaśnimy wiersze 17–21, które wyświetlają encje [Client]. Podane wyjaśnienie będzie miało zastosowanie również do pozostałych encji.
// klienci
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
- wiersz 3: słowo kluczowe var zostało wprowadzone w C# 3.0. Pozwala ono uniknąć podawania dokładnego typu zmiennej. Kompilator wywodzi go wówczas na podstawie typu wyrażenia przypisanego do zmiennej;
- wiersz 3: wyrażenie przypisane do zmiennej clients jest zapytaniem typu „LINQ to Entity”. Można w nim rozpoznać słowa kluczowe języka SQL przeniesione do LINQ. Zastosowana tutaj składnia jest następująca:
from variable in DbSet select variable
Bardziej ogólna składnia LINQ to
from variable in collection select variable
Kolekcja zostanie przejrzana, a dla każdego jej elementu zmienna zostanie obliczona. Odbywa się to dopiero wtedy, gdy zmienna [clients] z linii 3 zostanie wyliczona przez pętlę for / each z linii 4–7. Dopóki to nie nastąpi, zmienna [clients] jest jedynie nieocenionym zapytaniem;
- wiersz 4: zapytanie [clients] jest wyliczane. Spowoduje to wymuszenie oceny zapytania. Wiersze tabeli [CLIENTS] będą kolejno wprowadzane do kontekstu trwałości;
- wiersz 6: do wyświetlenia wykorzystywana jest metoda [ToString] encji [Client]. Nie następuje żadne ładowanie zależności;
Przejdźmy do kolejnych wierszy kodu:
- wiersze 24–28: wiersze tabeli [MEDECINS] są wprowadzane do kontekstu trwałości i wyświetlane. Nie następuje ładowanie zależności;
- wiersze 31–35: wiersze z tabeli [CRENEAUX] są przenoszone do kontekstu trwałości i wyświetlane. Zauważyliśmy, że metoda [ToString] tej encji wyświetla zależność [Medecin]. Zależność ta jest jednak już załadowana. Nie nastąpi więc ponowne ładowanie;
- wiersze 38–42: wiersze z tabeli [RVS] są przenoszone do kontekstu trwałości i wyświetlane. Widzieliśmy, że metoda [ToString] tej encji wyświetlała zależności [Client] i [Creneau]. Są one jednak już załadowane. Nie nastąpi więc ich ponowne załadowanie.
Należy zauważyć, że kolejność wyświetlania ma znaczenie. Gdybyśmy chcieli najpierw wyświetlić jednostki [Rv], metoda [ToString] tej jednostki spowodowałaby załadowanie jednostek [Client] i [Creneau] powiązanych z tymi spotkaniami. Pozostałe nie zostałyby załadowane. Zostałyby załadowane później, podczas innego wyświetlania. Ma to wpływ na wydajność. Powyższy kod wymaga czterech poleceń SQL, aby wyświetlić wszystkie rekordy. Załóżmy teraz, że najpierw wykorzystujemy tabelę spotkań [RVS]. Konieczne jest pierwsze zapytanie SQL dotyczące tabeli [RVS]. Następnie metoda [ToString] encji [Rv] spowoduje ewentualne załadowanie powiązanych encji [Client] i [Creneau]. Dla każdej z nich potrzebne jest zapytanie SQL. Zakładając, że istnieje N2 klientów i N3 terminów oraz że wszystkie te encje są odwołane w tabeli [RVS], wyświetlenie tej tabeli będzie wymagało 1 + N2 + N3 zapytań SQL. Zatem wydajność jest niższa niż w analizowanej wersji. Aby wyświetlić tabelę [RVS] wraz z jej zależnościami, konieczne byłoby połączenie tabel. Można to zrealizować za pomocą tabeli LINQ. Wrócimy do tego na przykładzie. Na razie należy pamiętać, że musimy zwrócić uwagę na zapytania SQL leżące u podstaw naszego kodu LINQ.
Konfigurujemy projekt tak, aby wykonał ten nowy kod [1] i [2], a następnie uruchamiamy go:
![]() |
W konsoli wyświetla się następujący komunikat:
3.5.4. Uczenie się kodu LINQ przy użyciu kodu LINQPad
W powyższym przykładzie wykorzystaliśmy zapytania typu „LINQ to Entity” w celu wyświetlenia zawartości tabel bazy danych. Joseph Albahari napisał program służący do nauki różnych form zapytania LINQ. Przedstawiamy go teraz.
LINQPad jest dostępny na następującej stronie: URL. Po zainstalowaniu uruchamiamy go: [1]:
![]() |
Początkujący użytkownicy LINQ mogą zapoznać się z przykładami w zakładce [Samples] [2], która zawiera bardzo wiele przykładów. Wybierzmy przykład [3], który wyświetli się wówczas w innym oknie [4]. Pełny kod tego przykładu wygląda następująco:
// A teraz proste wyrażenie zapytania LINQ-to-objects (zwróć uwagę, że nie ma średnika):
from word in "The quick brown fox jumps over the lazy dog".Split()
orderby word.Length
select word
// Możesz to edytować... (nikt tego nie obserwuje!) Zostaniesz poproszony o zapisanie wszelkich
// zmiany w osobnym pliku.
//
// Wskaz ówka: Możesz wykonać część zapytania, zaznaczając ją, a następnie naciskając klawisz F5.
Wiersze 3–5 stanowią przykład zapytania LINQ to Object. Zapytanie LINQ jest zgodne z następującą składnią:
from variable in collection orderby élément1 select élément2
- zmienna oznacza bieżący element kolekcji. W naszym przykładzie tą kolekcją jest lista słów wynikających z podzielonego ciągu znaków;
- kolekcja jest uporządkowana zgodnie z parametrem élément1 opcji orderby. W naszym przykładzie kolekcja słów zostanie uporządkowana według ich długości;
- słowo kluczowe select określa, co chcemy pobrać z bieżącego elementu variable z kolekcji. W naszym przykładzie będzie to słowo.
Wykonajmy to zapytanie LINQ:
![]() |
- w [1]: wyrażenie LINQ jest wykonywane przez [F5] lub za pomocą przycisku uruchamiania;
- w [2]: wyświetlanie. Słowa są wyświetlane w kolejności według ich długości. Ten prosty przykład pokazuje możliwości LINQ;
- w [3] można pobrać inne przykłady, w szczególności te z książki „LINQ in action” [4];
![]() |
- w [5] wybieramy przykład z książki;
string[] words = { "hello", "wonderful", "linq", "beautiful", "world" };
// Pogrupuj słowa według długości
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 };
// Wydrukuj każdą grupę
foreach (var group in groups)
{
Console.WriteLine("Words of length " + group.Length);
foreach (string word in group.Words)
Console.WriteLine(" " + word);
}
- wiersz 4: nowe zapytanie LINQ z nowymi słowami kluczowymi;
- wiersz 5: żądana kolekcja to tablica słów z wiersza 1;
- wiersz 6: zbiór jest posortowany alfabetycznie według słów;
- wiersz 7: zbiór jest grupowany (słowo kluczowe „into”) w nowym zbiorze lengthGroups. lengthGroups.Key oznacza czynnik grupowania (słowo kluczowe „by”), w tym przypadku długość słów. lengthGroups gromadzi słowa o tym samym czynniku grupowania, a więc o tej samej długości;
- wiersz 8: kolekcja lengthGroups jest uporządkowana według klucza grupowania w porządku malejącym, a więc w tym przypadku według malejącej długości słów;
- wiersz 9: z tej kolekcji tworzone są nowe obiekty (klasy anonimowe) posiadające dwa pola:
- Length: długość słów,
- Words: słowa o tej długości;
W tym miejscu szczególnie widać znaczenie słowa kluczowego var z wiersza 4. Ponieważ w wierszu 9 użyto klasy anonimowej, nie można określić typu zmiennej groups. Kompilator nada natomiast anonimowej klasie wewnętrzną nazwę i na jej podstawie określi typ zmiennej groups. Następnie będzie w stanie stwierdzić, czy zmienna groups jest używana poprawnie
- wiersz 12: przetwarzanie zapytania z wiersza 4. Dopiero w tym momencie jest ono oceniane. Przypomnijmy, że jego wykonanie spowoduje utworzenie kolekcji obiektów, określonych w wierszu 9;
- wiersz 14: wyświetlana jest właściwość Length bieżącego elementu, czyli długość słów;
- wiersze 15–17: wyświetlany jest każdy element kolekcji właściwości Words, a więc zbiór słów o długości wyświetlonej poprzednio.
Po wykonaniu tego zapytania otrzymujemy następujący wynik w LINQPad:
![]() |
Skoro zapoznaliśmy się już z kilkoma przykładami zapytań [LINQ to Object], przyjrzyjmy się zapytaniom [LINQ to Entity], które pozwolą nam wysyłać zapytania do baz danych. Najpierw połączymy się z bazą danych SQL Server, którą utworzyliśmy i wypełniliśmy:
![]() |
- w [1] dodajemy połączenie z bazą danych;
- w [2] definiujemy sposoby dostępu do źródła danych. Aby uzyskać dostęp do bazy SQL Server, użyjemy [LINQPad Driver];
- w pliku [3] można również pobrać kontekst trwałości [DbContext] zdefiniowany w pliku .exe lub .dll o nazwie assembly (opcja 3). Niestety, na dzień dzisiejszy (8 października 2012 r.) Entity Framework 5 nie jest obsługiwany;
- w [4] można pobrać sterowniki dla innych SGBD niż SQL Server;
- w [5] pobieramy sterownik dla SGBD, MySQL i Oracle;
![]() |
- w [6] – pobrany sterownik;
- w [7] łączymy się z serwerem bazy danych SQL;
![]() |
- w [8] baza znajduje się na serwerze nazw (lokalnym);
- w [9] łączymy się przy użyciu uwierzytelniania sa / sqlserver2012;
- w [10], do bazy danych [rdvmedecins-ef], którą utworzyliśmy;
- w [11] można przetestować połączenie;
- w [12] zamykamy kreatora;
- w [13] połączenie pojawia się w LINQPad.
Entities zostały utworzone na podstawie tabeli [rdvmedecins-ef]. Są to następujące:
![]() |
- w [1], [CLIENTS] reprezentuje zbiór encji [Client]. Każda encja posiada:
- właściwości (ID, TITRE, NOM, PRENOM, TIMESTAMP),
- relację „jeden do wielu” z [CLIENTRVS];
- gdzie [2], [CRENEAUXes] reprezentują zbiór jednostek [Creneau]. Każda jednostka posiada:
- właściwości (ID, HDEBUT, MDEBUT, HFIN, MFIN, MEDECIN_ID, TIMESTAMP),
- relacja „jeden do wielu” [CRENEAURVS],
- relację „wiele do jednego” [MEDECIN];
- w przypadku [3], jednostka [MEDECINS] reprezentuje zbiór jednostek [Medecin]. Każda jednostka posiada:
- właściwości (ID, TITRE, NOM, PRENOM, TIMESTAMP),
- relację „jeden do wielu” z [MEDECINCRENEAUXes];
- w przypadku [4] jednostka [RVS] reprezentuje zbiór jednostek [Rv]. Każda jednostka posiada:
- właściwości (ID, JOUR, CLIET_ID, CRENEAU_ID, TIMESTAMP),
- relacja „wiele do jednego” z [CLIENT],
- relacja wiele do jednego [CRENEAU].
Warto zauważyć, że nazwy powyższych właściwości różnią się od nazw, których używaliśmy do tej pory. Nie ma to jednak znaczenia. Chcemy po prostu poznać podstawowe zasady tworzenia zapytań do bazy danych.
Zobaczmy, jak możemy wysyłać zapytania do tej bazy encji. Na przykład chcemy uzyskać listę lekarzy posortowaną według ich TITRE i NOM:
![]() |
- w [1] tworzymy nowe zapytanie;
- w [2] – tekst zapytania;
![]() |
- w pliku [3] zapisano wynik zapytania;
- w [4] to to samo zapytanie z wyrażeniami lambda. Zapytanie z wyrażeniami lambda jest mniej czytelne niż zapytanie tekstowe i można by chcieć się bez niego obejść. Są one jednak czasami niezbędne, ponieważ umożliwiają pewne rzeczy, których nie pozwalają na zapytania tekstowe. Wyrażenie lambda oznacza funkcję z jednym parametrem wejściowym a i jednym parametrem wyjściowym b, w postaci a=>b. Powyższa metoda OrderBy przyjmuje funkcję lambda jako jedyny parametr. Funkcja ta dostarcza jej parametr, według którego ma zostać posortowana kolekcja. Tak więc MEDECINS.OrderBy(m=>m.TITRE) to lista lekarzy posortowana według tytułów. Instrukcję tę należy traktować jako potok operacji na zbiorze. Zbiór lekarzy jest przekazywany jako dane wejściowe do metody OrderBy. Metoda ta przetwarza obiekty [Medecin] jeden po drugim. W wyrażeniu lambda m=>m.TITRE m reprezentuje dane wejściowe funkcji lambda. Można je nazwać dowolnie. W tym przypadku danymi wejściowymi funkcji lambda będzie entyteta [Medecin]. Funkcja m=>m.TITRE odczytuje się następująco: jeśli nazwę m moim argumentem wejściowym (entytą [Medecin]), to moim wynikiem wyjściowym jest m.TITRE, czyli tytuł lekarza. MEDECINS.OrderBy(m=>m.TITRE) jest z kolei zbiorem – zbiorem lekarzy uporządkowanym według tytułów. Ta nowa kolekcja może zasilać inną metodę, w tym przykładzie metodę ThenBy. Działa ona na tej samej zasadzie. Służy do wskazania dodatkowych parametrów sortowania kolekcji.
Zapoznanie się z kodem lambda odpowiadającym kodowi tekstowemu, który zazwyczaj wpisujemy, jest dobrym sposobem na naukę;
![]() |
- w [5], polecenie SQL wysłane do bazy. Również w tym przypadku należy uważnie zapoznać się z tym kodem. Pozwala on oszacować rzeczywisty koszt zapytania LINQ.
Poniżej przedstawiamy kilka przykładów zapytań LINQ. Za każdym razem pokazujemy wyświetlane wyniki oraz odpowiadające im kody lambda i SQL. Aby zrozumieć te zapytania, należy przypomnieć sobie relacje typu „wiele do jednego”, które łączą encje między sobą. To właśnie dzięki nim przechodzi się od jednej encji do drugiej. Nazywa się je właściwościami nawigacyjnymi.
![]() |
// klienci o tytule „Pan” posortowani według malejącej kolejności nazwisk
Wyniki:
![]() |
LINQ | |
Lambda | |
SQL | |
// wszystkie terminy wizyt u danego lekarza
Wyniki (częściowe):
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// wszystkie wizyty z powiązanym klientem i lekarzem
Wyniki:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// lekarze bez umówionych wizyt
Wyniki:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
Nie ma zapytania o numer LINQ dla tej prośby. Należy skorzystać z wyrażeń lambda. To wyrażenie odczytuje się w następujący sposób: pobieram zbiór lekarzy (MEDECINS) i zachowuję (Where) tylko tych lekarzy (m), dla których nie jestem w stanie znaleźć w zbiorze wizyt (RVS) żadnej wizyty (rv) z tym lekarzem (m).
// terminy pani Pélissier
Wyniki (częściowe):
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// liczba spotkań pani Pélissier w dniu 08.10.2012
Wyniki:
![]() |
LINQ | |
Lambda | |
SQL | |
// lista klientów, którzy umówili się na spotkanie z panią Pélissier w dniu 08.10.2012
Wyniki:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// liczba terminów na lekarza
Wyniki:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
3.5.5. Modyfikacja encji powiązanej z kontekstem trwałości
Omówiliśmy następujące operacje na kontekście trwałości:
- dodanie elementu do kontekstu ([dbContext].[DbSet].Add);
- usunięcie elementu z kontekstu ([dbContext].[DbSet].Remove);
- wykonywanie zapytań dotyczących kontekstu za pomocą zapytań LINQ.
Aby zsynchronizować kontekst z bazą danych, należy wpisać [dbContext].SaveChanges().
![]() | ![]() |
Kod [ModifyAttachedEntity] ilustruje modyfikację elementu powiązanego z kontekstem:
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;
// Pierwszy kontekst
using (var context = new RdvMedecinsContext())
{
// Opróżniamy bieżącą bazę
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// dodajemy klienta
client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
context.Clients.Add(client1);
// monitorowanie
Console.WriteLine("client1--avant");
Console.WriteLine(client1);
// zapis kontekstu
context.SaveChanges();
// monitorowanie
Console.WriteLine("client1--après");
Console.WriteLine(client1);
}
// drugi kontekst
using (var context = new RdvMedecinsContext())
{
// pobieramy klienta1 z klienta2
client2 = context.Clients.Find(client1.Id);
// kontynuacja
Console.WriteLine("client2");
Console.WriteLine(client2);
// modyfikacja klienta 2
client2.Nom = "yy";
// zapis kontekstu
context.SaveChanges();
}
// trzeci kontekst
using (var context = new RdvMedecinsContext())
{
// pobieramy klienta2 do klienta3
client3 = context.Clients.Find(client2.Id);
// kontynuacja
Console.WriteLine("client3");
Console.WriteLine(client3);
}
}
}
}
- wiersz 15: otwarcie kontekstu aplikacji;
- wiersze 18–25: kontekst zostaje opróżniony. Dokładniej rzecz biorąc, wszystkie elementy są pobierane do kontekstu z bazy danych, a następnie przechodzą w stan „usunięte”. Należy zauważyć, że na tym etapie baza danych pozostaje niezmieniona. Dopóki kontekst nie zostanie zsynchronizowany z bazą danych, ta ostatnia nie ulega zmianie. Przypomnijmy, że usunięcie encji [Medecin] i [Client] wystarcza do opróżnienia bazy dzięki kaskadowemu usuwaniu;
- wiersze 27–28: do bazy dodawany jest nowy klient;
- wiersze 30–31: wyświetla się go przed zapisaniem w bazie;
- wiersz 33: kontekst jest synchronizowany z bazą. Entities oznaczone jako „usunięte” zostaną poddane operacji SQL DELETE, a dodany element zostanie poddany operacji SQL INSERT;
- wiersze 35–36: wyświetla się klienta po synchronizacji z bazą danych;
Wynik wyświetlany w konsoli jest następujący:
Należy zwrócić uwagę na następujące kwestie:
- przed synchronizacją z bazą klient nie posiada ani klucza głównego, ani timestamp,
- po synchronizacji już je posiada. Przypominamy, że klucz główny został skonfigurowany tak, aby był generowany przez serwer SQL. Podobnie ten SGBD automatycznie generuje znacznik czasu;
- wiersz 37: kontekst trwałości zostaje zamknięty. Entities, które zawierał, stają się „odłączone”. Istnieją one jako obiekty, ale nie jako entities przypisane do kontekstu trwałości;
- wiersz 39: uruchamia się nowy, pusty kontekst;
- wiersz 42: pobieramy klienta bezpośrednio z bazy danych za pomocą jego klucza głównego. Następnie jest on wprowadzany do kontekstu. Jeśli nie zostanie znaleziony, metoda Find zwraca wskaźnik null;
- wiersze 48–49: wyświetla się go;
Daje to następujący wynik:
- wiersz 47: modyfikuje się go;
- wiersz 49: synchronizuje się kontekst z bazą. EF wykryje, że niektóre elementy kontekstu zostały zmodyfikowane od momentu ich wprowadzenia do niego. Dla tych elementów wygeneruje polecenia SQL i UPDATE w bazie. W tym przypadku synchronizacja będzie więc polegać na jednym poleceniu UPDATE;
- wiersz 50: drugi kontekst zostaje zamknięty. Entyteta client2, która była przypisana do kontekstu, zostaje teraz od niego odłączona;
- wiersz 52: otwieramy trzeci, pusty kontekst;
- wiersz 55: ponownie przenosimy do niego jedynego klienta z bazy. Chcemy sprawdzić, czy zmiana dokonana na nim w poprzednim kontekście została odzwierciedlona w bazie;
- wiersze 57–58: wyświetlany jest klient. Daje to następujący wynik:
Nazwa klienta została rzeczywiście zmieniona w bazie danych. Warto zauważyć, że jego identyfikator timestamp został zaktualizowany.
- wiersz 59: zamykamy kontekst. Przy okazji warto zauważyć, że w przeciwieństwie do dwóch poprzednich przypadków nie było wcześniej potrzeby synchronizacji kontekstu z bazą danych (SaveChanges), ponieważ kontekst nie został zmodyfikowany.
3.5.6. Zarządzanie odłączonymi encjami
Wróćmy do architektury warstwowej aplikacji takiej jak ta z przypadku studyjnego:
![]() |
Warstwa [DAO] wykorzystuje warstwy ORM i EF5 w celu uzyskania dostępu do danych. Mamy podstawowe elementy tej warstwy. Każda metoda otworzy kontekst trwałości, wykona w nim niezbędne operacje (wstawianie, modyfikowanie, usuwanie, wyszukiwanie), a następnie go zamknie. Entities zarządzane przez warstwę [DAO] są przekazywane do warstwy internetowej ASP.NET. W tej warstwie znajdują się one poza kontekstem trwałości, a zatem są odłączone. W warstwie internetowej użytkownik może modyfikować te entity (dodawanie, modyfikacja, usuwanie). Kiedy powracają do warstwy [DAO], nadal pozostają odłączone. Warstwa [DAO] będzie jednak musiała odzwierciedlić zmiany wprowadzone przez użytkownika w bazie danych. Będzie więc musiała pracować z odłączonymi encjami. Przyjrzyjmy się trzem możliwym przypadkom:
Dodawanie elementu odłączonego
Jest to typowy przypadek dodawania. Wystarczy dodać (Add) odłączony element do kontekstu, upewniając się, że ma on klucz główny równy null.
Modyfikacja oddzielnego obiektu
Można użyć następującego kodu:
- metoda [DbContext].Entry(element-oddzielony) umieści element w kontekście;
- status tej jednostki zostanie ustawiony na „zmieniony”, aby mogła ona stać się przedmiotem polecenia SQL UPDATE.
Usunięcie jednostki odłączonej
Można użyć następującego kodu:
- wiersz 1: umieszczamy w kontekście jednostkę o tym samym kluczu głównym co jednostka odłączona;
- wiersz 2: usuwa się ją:
Należy zauważyć, że wymaga to w pierwszej kolejności kodu SELECT, a następnie DELETE, podczas gdy zazwyczaj wystarcza sam kod DELETE. Można również skorzystać z przykładu modyfikacji jednostki odłączonej i wpisać:
Ponieważ nie udało mi się wdrożyć logów dotyczących operacji SQL wykonywanych w bazie, nie wiem, czy jedna z tych metod jest bardziej zalecana niż druga.
Oto przykład:
![]() | ![]() |
Kod programu [ModifyDetachedEntities] wygląda następująco:
using System;
using System.Data;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class ModifyDetachedEntities
{
static void Main(string[] args)
{
Client client1;
// opróżnianie bieżącej bazy
Erase();
// dodajemy klienta
using (var context = new RdvMedecinsContext())
{
// utworzenie klienta
client1 = new Client { Titre = "x", Nom = "x", Prenom = "x" };
// dodanie klienta do kontekstu
context.Clients.Add(client1);
// zapisywanie kontekstu
context.SaveChanges();
}
// wyświetlanie bazy
Dump("1-----------------------------");
// klient1 nie znajduje się w kontekście – modyfikacja
client1.Nom = "y";
// nowy kontekst
using (var context = new RdvMedecinsContext())
{
// tutaj mamy pusty kontekst
// umieszczamy klienta1 w kontekście w stanie zmodyfikowanym
context.Entry(client1).State = EntityState.Modified;
// zapisujemy kontekst
context.SaveChanges();
}
// wyświetlanie podstawowe
Dump("2-----------------------------");
// usunięcie encji spoza kontekstu
using (var context = new RdvMedecinsContext())
{
// tutaj mamy nowy pusty kontekst
// umieszczamy klienta1 w kontekście w stanie usuniętym
context.Entry(client1).State = EntityState.Deleted;
// zapisujemy kontekst
context.SaveChanges();
}
// wyświetlanie bazy
Dump("3-----------------------------");
}
static void Erase()
{
// opróżniamy bazę
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);
}
// zapisano kontekst
context.SaveChanges();
}
}
static void Dump(string str)
{
Console.WriteLine(str);
// wyświetla bazę
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);
}
}
}
}
}
- wiersz 15: baza danych zostaje wyczyszczona;
- wiersze 17–25: do bazy dodawany jest klient;
- wiersz 27: wyświetla zawartość bazy danych;
- po wierszu 25 kontekst trwałości już nie istnieje. Nie ma więc już żadnych przypisanych encji. Encja client1 przeszła w stan „odłączony”;
- wiersz 29: zmienia się nazwę odłączonej encji;
- wiersz 31: otwiera się nowy, pusty kontekst;
- wiersz 35: odłączona jednostka client1 zostaje umieszczona w kontekście w stanie „zmieniony”;
- wiersz 37: kontekst jest synchronizowany z bazą;
- wiersz 38: kontekst zostaje zamknięty;
- wiersz 40: wyświetlana jest baza danych;
Nazwa klienta została poprawnie zmieniona w bazie danych. Należy zauważyć, że timestamp został zaktualizowany;
- wiersz 42: otwarcie nowego, pustego kontekstu;
- wiersz 46: oddzielony obiekt client1 zostaje umieszczony w kontekście w stanie „usunięty”;
- wiersz 48: kontekst jest synchronizowany z bazą danych;
- wiersz 49: kontekst zostaje zamknięty;
- wiersz 51: baza jest wyświetlana;
Entyteta została pomyślnie usunięta z bazy danych.
Teraz przyjrzyjmy się dwóm trybom ładowania zależności encji: Lazy Loading i Eager Loading.
3.5.7. Lazy i Eager Loading
Wróćmy do schematu zależności typu „wiele do jednego” jednej z naszych czterech encji:
![]() |
Powyżej encja [Creneau] posiada właściwość nawigacyjną [Creneau.Medecin] prowadzącą do encji [Medecin]. Nazywamy to zależnością. Widzieliśmy już, że istnieją również zależności typu „jeden do wielu”. Zasada, którą teraz wyjaśnimy, ma również zastosowanie do nich.
Domyślnie EF 5 działa w trybie Lazy Loading: gdy pobiera encję z bazy danych do kontekstu trwałości, nie pobiera jej zależności. Zależności te zostaną pobrane dopiero przy ich pierwszym użyciu. Jest to rozwiązanie oparte na zdrowym rozsądku. Gdyby tak nie było, pobranie spotkań do kontekstu spowodowałoby, zgodnie z powyższymi zależnościami, pobranie:
- entytety [Creneau] powiązane z terminami spotkań;
- entytety [Medecin] powiązane z tymi przedziałami czasowymi;
- entytety [Clients] powiązane z terminami spotkań.
Czasami jednak potrzebna jest dana jednostka wraz z jej zależnościami. Zilustrujemy oba sposoby ładowania.
![]() | ![]() |
Kod dla [LazyEagerLoading] jest następujący:
using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;
namespace RdvMedecins_01
{
class LazyEagerLoading
{
// obiekty
static Medecin[] medecins;
static Client[] clients;
static Creneau[] creneaux;
static void Main(string[] args)
{
// inicjowanie bazy
InitBase();
Console.WriteLine("Initialisation terminée");
// wstępne ładowanie
Creneau creneau;
int idCreneau = (int)creneaux[0].Id;
using (var context = new RdvMedecinsContext())
{
// przedział nr 0
creneau = context.Creneaux.Include("Medecin").Single<Creneau>(c => c.Id == idCreneau);
Console.WriteLine(creneau.ShortIdentity());
}
// wyświetlanie zależności
try
{
Console.WriteLine("Médecin={0}", creneau.Medecin);
}
catch (Exception e)
{
Console.WriteLine("L'erreur 1 suivante s'est produite : {0}", e);
}
// ładowanie opóźnione – tryb domyślny
using (var context = new RdvMedecinsContext())
{
// okno nr 0
creneau = context.Creneaux.Single<Creneau>(c => c.Id == idCreneau);
Console.WriteLine(creneau.ShortIdentity());
}
// wyświetlanie zależności
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()
{
// inicjowanie bazy
using (var context = new RdvMedecinsContext())
{
// opróżnianie bieżącej bazy
...
// inicjowanie bazy
// klienci
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" }
};
...
// terminy spotkań
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
// zapisywanie kontekstu trwałości
context.SaveChanges();
}
}
}
}
- wiersz 18: wychodzimy od znanej bazy, tej używanej do tej pory. Po tej operacji tabele w wierszach 11–13 są wypełniane oddzielnymi jednostkami;
- wiersze 21–22: skupiamy się na pierwszym przedziale czasowym i przypisanym do niego lekarzu;
- wiersz 23: nowy kontekst;
- wiersz 26: umieszczamy termin w kontekście wraz z jego zależnością (eager loading). Ponieważ nie jest to tryb domyślny, należy wyraźnie zażądać tej zależności. Umożliwia to metoda Include. Jej parametrem jest nazwa zależności w encji wprowadzonej do kontekstu. Zapytanie, które wprowadza encję do kontekstu, wykorzystuje wyrażenia lambda. Metoda
Singlepozwala określić warunek umożliwiający pobranie pojedynczej encji. W tym przypadku wyszukujemy w bazie encję [Creneau], która ma klucz główny odpowiadający przedziałowi nr 0; - wiersz 27: wyświetlana jest pobrana encja. Przypomnijmy dwie metody zapisu używane w encjach:
// podpis
public override string ToString()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5},{6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
}
// krótki podpis
public string ShortIdentity()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, MedecinId, dump(Timestamp));
}
- wiersze 2–5: metoda [ToString] wyświetla zależność [Medecin]. Jeśli nie ma jej jeszcze w kontekście, zostanie wyszukana w bazie danych w celu dodania jej do kontekstu;
- wiersze 8–11: metoda [ShortIdentity] nie wyświetla zależności [Medecin]. Nie będzie więc jej szukać w bazie, jeśli nie ma jej w kontekście;
Na tym etapie wyświetlacz konsoli wygląda następująco:
- wiersz 28: kontekst jest zamknięty;
- wiersze 30–37: następuje próba zapisania zależności [Medecin] dla tej jednostki. Przypomnijmy, jak działa mechanizm Lazy Loading: zależność jest ładowana przy pierwszym użyciu, jeśli nie jest obecna. W tym przypadku powinna być obecna. Wyświetlany jest następujący komunikat:
- wiersze 39–44: w nowym kontekście ponownie wyszukiwany jest slot nr 0 w bazie danych i pobierany do kontekstu. W tym przypadku zależność [Medecin] nie jest wyraźnie żądana. Nie zostanie więc pobrana (Lazy Loading);
- wiersz 43: wyświetlany jest skrócony identyfikator przedziału w następującej postaci:
W tym miejscu ważne jest, aby użyć ShortIdentity zamiast ToString w celu wyświetlenia encji. Jeśli użyjemy ToString, wyświetlona zostanie zależność [Medecin], a w związku z tym zostanie ona wyszukana w bazie danych. Nie chcemy jednak, aby tak się stało.
- wiersz 44: kontekst jest zamknięty;
- wiersze 46–53: próbujemy wyświetlić zależność encji. Ważne jest, aby zrobić to poza kontekstem, w przeciwnym razie zostanie ona wyszukana w bazie i znaleziona. Tutaj jesteśmy poza kontekstem. Entyteta [Creneau] jest odłączona, a jej zależność [Medecin] nie istnieje (Lazy Loading). Co się stanie? Wyświetlany ekran wygląda następująco:
EF wykrył brak zależności [Medecin]. Próbował ją załadować, ale ponieważ kontekst był zamknięty, operacja ta nie była już możliwa. Zapamiętamy ten wyjątek [System.ObjectDisposedException], ponieważ jest on charakterystyczny dla próby załadowania zależności poza otwartym kontekstem.
Przeanalizujmy teraz współbieżność dostępu do encji.
3.5.8. Konkurencja dostępu do encji
Wróćmy do definicji encji [Client]:
public class Client
{
// dane
[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; }
// dane kontaktowe klienta
public ICollection<Rv> Rvs { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
// podpis
...
}
Skupimy się na polu [Timestamp] w wierszu 23. Wiemy, że jego wartość jest generowana przez SGBD. Stwierdziliśmy również, że adnotacja [Timestamp] w wierszu 22 powodowała, że EF 5 wykorzystywał adnotowane pole do zarządzania konkurencją dostępu do encji. Przypomnijmy, na czym polega zarządzanie konkurencją dostępu:
- proces P1 odczytuje wiersz L z tabeli [MEDECINS] w momencie T1. Wiersz ten ma identyfikator timestamp TS1;
- proces P2 odczytuje ten sam wiersz L z tabeli [MEDECINS] w momencie T2. Wiersz ma wartości timestamp i TS1, ponieważ proces P1 nie zatwierdził jeszcze swojej zmiany;
- proces P1 zatwierdza zmianę w wierszu L. Wówczas wartość timestamp wiersza L zmienia się na TS2;
- proces P2 zatwierdza zmianę w wierszu L.ORM generuje wówczas wyjątek, ponieważ proces P2 ma wartości timestamp i TS1 dla wiersza L, które różnią się od wartości timestamp i TS2 znalezionym w bazie danych.
Nazywa się to optymistycznym zarządzaniem dostępem współbieżnym. W przypadku EF 5 pole pełniące tę rolę musi posiadać jeden z dwóch atrybutów: [Timestamp] lub [ConcurrencyCheck]. Serwer SQL ma typ [timestamp]. Wartość kolumny o tym typie jest automatycznie generowana przez serwer SQL przy każdym wstawieniu lub modyfikacji wiersza. Taka kolumna może wówczas służyć do zarządzania współbieżnością dostępu.
Zilustrujemy tę konkurencję dostępu za pomocą dwóch wątków, które będą jednocześnie modyfikować tę samą encję [Client] w bazie danych. Projekt rozwija się w następujący sposób:
![]() | ![]() |
Kod programu [AccèsConcurrents] wygląda następująco:
using System;
using System.Data;
using System.Linq;
using System.Threading;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
// obiekt wymieniany z wątkami
class Data
{
public int Duree { get; set; }
public string Nom { get; set; }
public Client Client { get; set; }
}
// program testowy
class AccèsConcurrents
{
static void Main(string[] args)
{
Client client1;
using (var context = new RdvMedecinsContext())
{
// wątek główny
Thread.CurrentThread.Name = "main";
// opróżnianie bieżącej bazy
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// dodajemy klienta
client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
context.Clients.Add(client1);
// monitorowanie
Console.WriteLine("{0} client1--avant sauvegarde du contexte", Thread.CurrentThread.Name);
Console.WriteLine(client1.ShortIdentity());
// zapis
context.SaveChanges();
// monitorowanie
Console.WriteLine("{0} client1--après sauvegarde du contexte", Thread.CurrentThread.Name);
Console.WriteLine(client1.ShortIdentity());
}
// zmodyfikujemy klienta1 przy użyciu dwóch wątków
// wątek t1
Thread t1 = new Thread(Modifie);
t1.Name = "t1";
t1.Start(new Data { Duree = 5000, Nom = "yy", Client = client1 });
// wątek t2
Thread t2 = new Thread(Modifie);
t2.Name = "t2";
t2.Start(new Data { Duree = 5000, Nom = "zz", Client = client1 });
// czekamy na zakończenie obu wątków
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);
// wyświetlamy zmianę – tylko jedna z nich musiała się powieść
using (var context = new RdvMedecinsContext())
{
// pobieramy klient1 do klient2
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());
}
}
// wątek
static void Modifie(object infos)
{
...
}
- wiersz 26: uruchamia się pusty kontekst;
- wiersz 29: nadano nazwę bieżącemu wątku, aby odróżnić go od dwóch wątków, które zostaną utworzone później;
- wiersze 31–38: jednostki [Medecin] i [Client] są ustawiane w stanie „usunięte”;
- wiersze 40–41: do kontekstu dodawany jest klient;
- wiersze 43–44: wyświetla się go przed synchronizacją kontekstu;
- wiersz 46: synchronizacja kontekstu z bazą danych: elementy o statusie „usunięte” zostaną usunięte z bazy danych. Element [Client] umieszczony w kontekście zostanie wstawiony do bazy danych. Będzie to jedyny element w bazie danych;
- wiersze 47–49: wyświetlanie klienta po synchronizacji kontekstu. Na tym etapie ekrany wyglądają następująco:
Należy zauważyć, że po synchronizacji kontekstu klient posiada klucz główny oraz timestamp;
- wiersz 50: kontekst zostaje zamknięty;
- wiersz 53: wątek t1 jest powiązany z metodą [Modifie] z wiersza 84. Oznacza to, że po uruchomieniu wykona on metodę [Modifie];
- wiersz 54: nadano nazwę wątku t1;
- wiersz 55: wątek t1 zostaje uruchomiony. Przekazujemy mu parametry w postaci struktury [Data] zdefiniowanej w wierszach 12–17:
- Czas trwania: wątek zatrzyma się na Durée sekund przed zakończeniem wykonywania,
- Klient: odwołanie do klienta, którego dane mają zostać zaktualizowane w bazie,
- Nazwa: nazwa, jaką należy nadać temu klientowi;
- wiersze 57–59: to samo dotyczy drugiego wątku. Ostatecznie dwa wątki będą próbowały zmienić w bazie nazwę tego samego klienta;
- wiersze 60–63: po uruchomieniu obu wątków wątek główny czeka na zakończenie ich wykonywania;
- wiersz 62: oczekiwanie na zakończenie wątku t1;
- wiersz 63: oczekiwanie na zakończenie wątku t2;
- wiersz 64: nie wiadomo, w jakiej kolejności zakończą się oba wątki. Pewne jest tylko to, że w wierszu 64 są już zakończone;
- wiersze 66–72: w nowym kontekście sprawdzamy klienta w bazie danych, aby zobaczyć, w jakim jest stanie.
Zobaczmy teraz, co robią dwa wątki t1 i t2. Wykonują one następującą metodę [Modifie]:
static void Modifie(object infos)
{
// pobieramy parametr
Data data = (Data)infos;
try
{
using (var context = new RdvMedecinsContext())
{
Console.WriteLine("Début Thread {0}", Thread.CurrentThread.Name);
// pobieramy klient1 do klient2
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());
// modyfikujemy klienta2
client2.Nom = data.Nom;
// czekamy chwilę
Thread.Sleep(data.Duree);
// zapisujemy zmiany
context.SaveChanges();
}
}
catch (Exception e)
{
// wyjątek
Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, e);
}
// koniec wątku
Console.WriteLine("Fin Thread {0}", Thread.CurrentThread.Name);
}
- wiersz 4: pobierane są parametry wątku (czas trwania, nazwa, klient);
- wiersz 7: nowy kontekst;
- wiersz 11: klient jest wprowadzany do kontekstu;
- wiersze 12–13: sprawdzanie statusu klienta;
- wiersz 15: zmiana nazwy klienta;
- wiersz 17: wątek zatrzymuje się na Duree milisekund. Ma to interesujący efekt. Wątek zwalnia procesor, na którym był wykonywany, ustępując miejsca innemu wątku. W naszym przykładzie mamy trzy wątki: main, t1 i t2. Wątek main jest zatrzymany i czeka na zakończenie wątków t1 i t2. Zakładając, że wątek t1 jako pierwszy uzyskał dostęp do procesora, przekazuje go teraz wątku t2. Spowoduje to, że wątek t2 odczyta dokładnie to samo, co wątek t1 – tego samego klienta o tym samym identyfikatorze timestamp;
- wiersz 19: kontekst jest zsynchronizowany z bazą danych. Załóżmy ponownie, że wątek t1 budzi się jako pierwszy. Zapisze on klienta o nazwie „yy”. Będzie mógł to zrobić, ponieważ posiada ten sam identyfikator timestamp, co w bazie danych. W wyniku tej aktualizacji identyfikator SGBD zmieni identyfikator timestamp. Kiedy wątek t2 z kolei się obudzi, będzie miał klienta z identyfikatorem timestamp różnym od tego, który obecnie znajduje się w bazie danych. Jego aktualizacja zostanie odrzucona.
Wyświetlane komunikaty są następujące:
- wiersz 4: klient w bazie danych;
- wiersz 9: klient odczytany przez wątek t2;
- wiersz 11: klient odczytany przez wątek t1. Oba wątki odczytały więc to samo;
- wiersz 12: wątek t2 kończy działanie jako pierwszy. Mógł więc dokonać aktualizacji. Nazwa musiała zmienić się na „zz”;
- wiersz 13: wątek t1 zgłasza wyjątek typu [System.Data.OptimisticConcurrencyException]. EF wykrył, że nie posiada właściwego timestamp;
- wiersz 21: wątek t1 również się kończy;
- wiersz 22: wątek główny zakończył oczekiwanie;
- wiersz 24: wątek główny wyświetla klienta z bazy danych. To właśnie wątek t2 wygrał. Nazwa to „zz”. Należy zauważyć, że timestamp uległ zmianie.
Teraz przyjrzyjmy się innemu aspektowi: transakcji, która reguluje synchronizację kontekstu trwałości z bazą danych.
3.5.9. Synchronizacja w ramach transakcji
Tabela [CRENEAUX] posiada ograniczenie unikalności, które dodaliśmy ręcznie (patrz paragraf 2.2.4, strona 12):
Postąpimy w następujący sposób: dodamy jednocześnie dwa terminy wizyt dla tego samego lekarza, tego samego dnia i w tym samym przedziale czasowym. Zobaczymy, co się stanie.
Projekt zmienia się w następujący sposób:
![]() | ![]() |
Kod programu [SynchronisationTransaction] jest następujący:
using System;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
// program testowy
class SynchronisationTransaction
{
static void Main(string[] args)
{
using (var context = new RdvMedecinsContext())
{
// opróżnianie bieżącej bazy danych
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
context.SaveChanges();
}
// tworzymy klienta
Client client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
// tworzymy lekarza
Medecin medecin1 = new Medecin { Nom = "xx", Prenom = "xx", Titre = "xx" };
// tworzy się termin dla tego lekarza
Creneau creneau1 = new Creneau { Hdebut = 8, Mdebut = 20, Hfin = 8, Mfin = 40, Medecin = medecin1 };
// tworzymy dwie wizyty dla tego lekarza i tego klienta, tego samego dnia, w tym samym przedziale czasowym
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
{
// umieszczamy wszystkie te dane w kontekście trwałości
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);
// zapisujemy kontekst – musi wystąpić wyjątek
//, ponieważ bazowy rekord BD ma ograniczenie unikalności uniemożliwiające
// istnienia dwóch rekordów RDV tego samego dnia i w tym samym przedziale czasowym
context.SaveChanges();
}
}
catch (Exception e)
{
Console.WriteLine("Erreur : {0}", e);
}
// jeśli zapis odbywa się w ramach transakcji, to nic nie mogło zostać wstawione do bazy
// z powodu poprzedniego wyjątku – sprawdzamy
using (var context = new RdvMedecinsContext())
{
// klientów
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
// lekarzy
Console.WriteLine("Médecins--------------------------------------");
var medecins = from medecin in context.Medecins select medecin;
foreach (Medecin medecin in medecins)
{
Console.WriteLine(medecin);
}
// terminy
Console.WriteLine("Créneaux horaires--------------------------------------");
var creneaux = from creneau in context.Creneaux select creneau;
foreach (Creneau creneau in creneaux)
{
Console.WriteLine(creneau);
}
// wizyty
Console.WriteLine("Rendez-vous--------------------------------------");
var rvs = from rv in context.Rvs select rv;
foreach (Rv rv in rvs)
{
Console.WriteLine(rv);
}
}
}
}
}
- wiersze 15–27: wykorzystuje się kontekst trwałości do opróżnienia bazy danych;
- wiersz 30: utworzenie obiektu [Client];
- wiersz 32: utworzenie obiektu [Medecin];
- wiersz 34: utworzenie obiektu [Creneau];
- wiersz 36: utworzenie obiektu [Rv];
- wiersz 37: utworzenie drugiego obiektu [Rv] identycznego z poprzednim;
- wiersz 41: otwarcie nowego kontekstu;
- wiersze 43–47: utworzone wcześniej obiekty są dołączane do nowego kontekstu. Należy tutaj zauważyć, że biorąc pod uwagę zależności, moglibyśmy zminimalizować liczbę operacji Add. Jednak operacja EF zoptymalizuje kolejność operacji SQL i INSERT, które mają zostać wysłane do bazy;
- wiersz 51: kontekst jest zsynchronizowany z bazą. Jak wskazuje komentarz, wstawienie jednego z dwóch terminów musi zakończyć się niepowodzeniem z powodu ograniczenia unikalności w tabeli [RVS]. Co więcej, jeśli synchronizacja odbywa się w ramach transakcji, wszystko musi zostać cofnięte. Dlatego nie może nastąpić żadne wstawienie. Baza danych musi pozostać pusta;
- wiersz 53: kontekst jest zamykany;
- wiersze 61–90: wyświetlenie zawartości bazy danych. Musi być pusta.
Wyświetlany ekran wygląda następująco:
- wiersz 1: wyjątek spowodowany naruszeniem ograniczenia unikalności w tabeli [RVS];
- wiersze 9–12: baza jest rzeczywiście pusta. Synchronizacja kontekstu z bazą odbyła się zatem w ramach transakcji.
W tabeli EF 5 z pewnością można by zbadać jeszcze inne kwestie. Wiemy jednak już wystarczająco dużo, aby powrócić do analizy architektury wielowarstwowej. Na początku niniejszego dokumentu czytelnik znajdzie odniesienia do artykułów i książek, które pozwolą mu pogłębić wiedzę na temat tabeli EF 5.
3.6. Analiza architektury wielowarstwowej opartej na EF 5
Wracamy do naszego studium przypadku opisanego w akapicie 2. Jest to aplikacja internetowa ASP.NET o następującej strukturze:
![]() |
Zaczniemy od zbudowania warstwy [DAO] zapewniającej dostęp do danych. Warstwa ta będzie oparta na EF5.
3.6.1. Nowy projekt
Tworzymy nowy projekt konsoli VS 2012 [RdvMedecins-SqlServer-02] w bieżącym rozwiązaniu [1]:
![]() |
Dodajemy cztery foldery o nazwach [2], w których rozdzielimy nasze kody. Folder [Entites] jest kopią folderu [Entites] z poprzedniego projektu. Po skopiowaniu pojawiają się błędy wynikające z braku odpowiednich odwołań. Musimy dodać odwołanie do Entity Framework 5. W tym celu zastosujemy metodę opisaną w paragrafie 3.4 na stronie 21. Lista odwołań wygląda następująco: [3]:
![]() |
Na tym etapie projekt nie powinien już zawierać błędów kompilacji. Z poprzedniego projektu skopiujemy również plik [App.config], który konfiguruje połączenie z bazą danych:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- Więcej informacji na temat konfiguracji Entity Framework można znaleźć na stronie 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>
<!-- ciąg połączenia z bazą danych -->
<connectionStrings>
<add name="monContexte"
connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- dostawca fabryki -->
<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. Klasa Exception
Będziemy korzystać z klasy wyjątków specyficznej dla tego projektu. Jest to klasa generowana przez warstwę [DAO]:
![]() |
Warstwa [DAO] zatrzyma wszystkie wyjątki, które dotrą do niej, i zamknie je w wyjątku typu [RdvMedecinsException]. Wyjątek ten będzie wyglądał następująco:
using System;
namespace RdvMedecins.Exceptions
{
public class RdvMedecinsException : Exception
{
// właściwości
public int Code { get; set; }
// konstruktorzy
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;
}
// tożsamość
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);
}
}
}
}
- wiersz 5: klasa ta wywodzi się z klasy [Exception];
- wiersz 9: dodaje do swojej klasy bazowej kod błędu;
- wiersze 12–32: różne konstruktory uwzględniają obecność pola [Code].
Projekt ewoluuje w następujący sposób:
![]() |
3.6.3. Warstwa [DAO]
![]() |
Warstwa [DAO] zapewnia interfejs do warstwy [ASP.NET]. Aby zidentyfikować tę warstwę, należy przejrzeć strony internetowe aplikacji:
![]() |
- w przypadku warstwy [1] powyżej lista rozwijana została wypełniona listą lekarzy. Listę tę dostarczy warstwa [DAO];
- w przypadku [2] warstwa [DAO] dostarczy;
- listę wizyt u lekarza na dany dzień,
- listę dostępnych terminów u lekarza,
- dodatkowe informacje o wybranym lekarzu;
![]() |
- w [3] lista rozwijana klientów będzie dostarczana przez warstwę [DAO];
![]() |
- w [4] użytkownik zatwierdza wizytę. Warstwa [DAO] musi mieć możliwość dodania tej wizyty do bazy danych. Musi również mieć możliwość udostępnienia dodatkowych informacji o wybranym kliencie;
![]() |
- w warstwie [5] użytkownik usuwa spotkanie. Warstwa [DAO] musi to umożliwiać.
Biorąc pod uwagę te informacje, interfejs [IDao] warstwy [DAO] mógłby wyglądać następująco:
using System;
using System.Collections.Generic;
using RdvMedecins.Entites;
namespace RdvMedecins.Dao
{
public interface IDao
{
// lista klientów
List<Client> GetAllClients();
// lista lekarzy
List<Medecin> GetAllMedecins();
// lista terminów wizyt u lekarza
List<Creneau> GetCreneauxMedecin(int idMedecin);
// lista RV danego lekarza w danym dniu
List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
// dodaj RV
int AjouterRv(DateTime jour, int idCreneau, int idClient);
// usunięcie terminu wizyty
void SupprimerRv(int idRv);
// znalezienie podmiotu T na podstawie jego klucza głównego
T Find<T>(int id) where T : class;
}
}
Metody w wierszach 10–20 wynikają z przeprowadzonej właśnie analizy. Metoda w wierszu 22 ma na celu zaradzenie temu, że pracujemy w trybie Lazy Loading. Jeśli w warstwie [ASP.NET] potrzebna jest zależność od jakiejś encji, pobierzemy ją z bazy danych za pomocą tej metody.
Implementacja tego interfejsu w klasie [Dao] będzie wyglądać następująco:
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
{
//lista klientów
public List<Client> GetAllClients()
{
// lista klientów
List<Client> clients = null;
try
{
// otwarcie kontekstu trwałości
using (var context = new RdvMedecinsContext())
{
// lista klientów
clients = context.Clients.ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(1, "GetAllClients", ex);
}
// zwracamy wynik
return clients;
}
// lista lekarzy
public List<Medecin> GetAllMedecins()
{
// lista lekarzy
List<Medecin> medecins = null;
try
{
// otwarcie kontekstu trwałości
using (var context = new RdvMedecinsContext())
{
// lista lekarzy
medecins = context.Medecins.ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(2, "GetAllMedecins", ex);
}
// zwracamy wynik
return medecins;
}
// lista terminów wizyt danego lekarza
public List<Creneau> GetCreneauxMedecin(int idMedecin)
{
...
}
// lista RV danego lekarza na dany dzień
public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
{
...
}
// dodaj termin
public int AjouterRv(DateTime jour, int idCreneau, int idClient)
{
...
}
// usunięcie terminu RV
public void SupprimerRv(int idRv)
{
...
}
// znalezienie klienta
public Client FindClient(int id)
{
...
}
// znaleźć wolny termin
public Creneau FindCreneau(int id)
{
...
}
// znaleźć lekarza
public Medecin FindMedecin(int id)
{
....
}
// znajdź termin wizyty
public Rv FindRv(int id){
...
}
}
}
Wyjaśnijmy metodę [GetAllClients], która ma zwrócić listę wszystkich klientów:
- wiersze 18–31: wyszukiwanie klientów odbywa się w bloku try/catch. Tak samo będzie w przypadku wszystkich kolejnych metod;
- wiersz 21: otwarcie nowego kontekstu;
- wiersz 24: encje [Client] są ładowane do kontekstu i umieszczane na liście.
Metoda [GetAllMedecins], która ma zwrócić listę wszystkich lekarzy, działa analogicznie (wiersze 37–57).
Metoda [GetCreneauxMedecin] wygląda następująco:
// lista terminów wizyt u danego lekarza
public List<Creneau> GetCreneauxMedecin(int idMedecin)
{
// lista terminów
try
{
// otwarcie kontekstu trwałości
using (var context = new RdvMedecinsContext())
{
// pobieranie lekarza wraz z jego terminami
Medecin medecin = context.Medecins.Include("Creneaux").Single(m => m.Id == idMedecin);
// lista terminów lekarza
return medecin.Creneaux.ToList<Creneau>();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(3, "GetCreneauxMedecin", ex);
}
}
- wiersz 9: otwarcie nowego kontekstu trwałości;
- wiersz 11: wyszukiwanie lekarza, którego klucz główny jest znany. Żądamy uwzględnienia zależności [Creneaux], która jest zbiorem terminów tego lekarza. Jeśli lekarz nie istnieje, metoda Single zgłasza wyjątek;
- wiersz 13: zwracana jest lista terminów.
Metoda [GetRvMedecinJour] powinna zwracać listę wizyt lekarza w danym dniu. Jej kod mógłby wyglądać następująco:
// lista RV lekarza dla danego dnia
public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
{
// lista wizyt
List<Rv> rvs = null;
try
{
// otwarcie kontekstu trwałości
using (var context = new RdvMedecinsContext())
{
// pobieranie danych lekarza
Medecin medecin = context.Medecins.Find(idMedecin);
if (medecin == null)
{
throw new RdvMedecinsException(10, string.Format("Médecin [{0}] inexistant", idMedecin));
}
// lista wizyt
rvs = context.Rvs.Where(r => r.Creneau.Medecin.Id == idMedecin && r.Jour == jour).ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(4, "GetRvMedecinJour", ex);
}
// zwracanie wyniku
return rvs;
}
- wiersz 13: pobieramy do kontekstu lekarza, którego klucz główny posiadamy;
- wiersze 14–17: jeśli nie istnieje, zgłaszamy wyjątek;
- wiersz 19: zapytanie LINQ w celu pobrania wizyt dla tego lekarza;
Metoda [AjouterRv] musi dodać wizytę do bazy danych i zwrócić klucz główny wstawionego elementu. Jej kod mógłby wyglądać następująco:
// dodaj RV
public int AjouterRv(DateTime jour, int idCreneau, int idClient)
{
// numer dodanej wizyty
int idRv;
try
{
// otwarcie kontekstu trwałości
using (var context = new RdvMedecinsContext())
{
// pobieramy przedział czasowy
Creneau creneau = context.Creneaux.Find(idCreneau);
if (creneau == null)
{
throw new RdvMedecinsException(5, string.Format("Créneau [{0}] inexistant", idCreneau));
}
// pobieranie danych klienta
Client client = context.Clients.Find(idClient);
if (client == null)
{
throw new RdvMedecinsException(6, string.Format("Client [{0}] inexistant", idCreneau));
}
// utworzenie terminu
Rv rv = new Rv { Jour = jour, Client = client, Creneau = creneau };
// dodanie do kontekstu
context.Rvs.Add(rv);
// zapis kontekstu
context.SaveChanges();
// pobieranie klucza głównego dodanego terminu spotkania
idRv = (int)rv.Id;
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(7, "AjouterRv", ex);
}
// wynik
return idRv;
}
- wiersz 12: wyszukiwany jest termin wizyty w bazie danych;
- wiersze 13–16: jeśli nie można go znaleźć, zgłaszany jest wyjątek;
- wiersz 18: wyszukiwanie klienta wizyty w bazie danych;
- wiersze 19–22: jeśli go nie ma, zgłaszamy wyjątek;
- wiersz 24: tworzymy obiekt [Rv] z niezbędnymi informacjami;
- wiersz 26: dodajemy go do kontekstu trwałości;
- wiersz 28: synchronizujemy kontekst trwałości z bazą danych. Spotkanie zostanie wówczas zapisane w bazie;
- wiersz 30: wiemy, że po synchronizacji bazy klucze główne wstawionych elementów są dostępne. Pobieramy klucz dodanego terminu spotkania;
- wiersz 31: zamykamy kontekst trwałości.
Metoda [SupprimerRv] ma usunąć spotkanie, którego klucz pierwotny został jej przekazany.
// usunięcie RV
public void SupprimerRv(int idRv)
{
try
{
// otwarcie kontekstu trwałości
using (var context = new RdvMedecinsContext())
{
// pobieranie obiektu Rv
Rv rv = context.Rvs.Find(idRv);
if (rv == null)
{
throw new RdvMedecinsException(5, string.Format("Rv [{0}] inexistant", idRv));
}
// usunięcie Rv
context.Rvs.Remove(rv);
// zapisanie kontekstu
context.SaveChanges();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(8, "SupprimerRv", ex);
}
}
- wiersz 7: nowy kontekst trwałości;
- wiersz 10: do kontekstu wprowadzamy spotkanie, które ma zostać usunięte;
- wiersze 11–15: jeśli nie istnieje, zgłaszany jest wyjątek;
- wiersz 16: usuwa się go z kontekstu;
- wiersz 18: synchronizujemy kontekst z bazą danych;
- wiersz 19: zamykamy kontekst.
Metoda [Find<T>] umożliwia wyszukiwanie w bazie danych encji typu T na podstawie jej klucza głównego. Jej kod mógłby wyglądać następująco:
public T Find<T>(int id) where T : class
{
try
{
// otwarcie kontekstu trwałości
using (var context = new RdvMedecinsContext())
{
return context.Set<T>().Find(id);
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(20, "Find<T>", ex);
}
}
- wiersz 8: metoda Set<T> pozwala pobrać obiekt typu DbSet<T>, na którym można stosować standardowe metody.
Projekt rozwija się w następujący sposób:
![]() |
3.6.4. Testowanie warstwy [DAO]
Stworzymy program do testowania warstwy [DAO]. Architektura testu będzie wyglądać następująco:
![]() |
Program konsolowy zwraca się do [Spring.net] z prośbą o utworzenie instancji warstwy [DAO]. Następnie testuje różne funkcje interfejsu warstwy [DAO]. Zamiast programu konsolowego lepiej byłoby napisać program testowy typu NUnit. Program testowy warstwy [DAO] mógłby wyglądać następująco:
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
{
// instancjonowanie warstwy [DAO] za pomocą Spring
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
// wyświetlanie klientów
List<Client> clients = dao.GetAllClients();
DisplayClients("Liste des clients :", clients);
// wyświetlanie lekarzy
List<Medecin> medecins = dao.GetAllMedecins();
DisplayMedecins("Liste des médecins :", medecins);
// lista terminów wizyt lekarza nr 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);
// lista wizyt lekarza w danym dniu
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)));
// dodaj wizytę RV dla lekarza nr 1 w przedziale czasowym nr 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)));
// dodaj wizytę w już zajętej godzinie – powinno spowodować wyjątek
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));
}
// usunięcie wizyty
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));
}
//pauza
Console.ReadLine();
}
// metody pomocnicze – wyświetla listy
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)
{
...
}
}
}
- wiersz 14: odwołanie do warstwy [DAO]. Aby test był niezależny od rzeczywistej implementacji tej warstwy, odwołanie to jest typu interfejsu [IDao], a nie typu klasy [Dao];
- wiersz 18: warstwa [DAO] jest instancjonowana przez Spring. Powrócimy do konfiguracji niezbędnej, aby było to możliwe. Przekształcamy odwołanie do obiektu zwrócone przez Spring na odwołanie typu interfejsu [IDao];
- wiersze 21–22: wyświetlają klientów;
- wiersze 25–26: wyświetlają listę pacjentów;
- wiersze 29–30: wyświetlają listę terminów lekarza nr 0;
- wiersz 33: wyświetla wizyty lekarza nr 0 z datą 23.11.2013 r. Nie powinno być żadnych;
- wiersz 37: dodaje wizytę u lekarza nr 0 na dzień 23.11.2013;
- wiersz 39: wyświetla wizyty lekarza nr 0 z dnia 23.11.2013 r. Powinna być jedna;
- wiersz 46: dodajemy tę samą wizytę po raz drugi. Powinien wystąpić błąd;
- wiersz 57: usuwa się jedyną dodaną wizytę;
- wiersz 58: wyświetla wizyty lekarza nr 0 z dnia 23.11.2013 r. Nie powinno być żadnej wizyty.
3.6.5. Konfiguracja Spring.net
W powyższym programie testowym pominęliśmy pokrótce instrukcję, która instancjonuje warstwę [DAO]:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
Klasa [ContextRegistry] jest klasą Springa w przestrzeni nazw [Spring.Context.Support]. Aby móc korzystać ze Springa, musimy dodać jego bibliotekę DLL do zależności projektu. Postępujemy w następujący sposób:
![]() |
- w [1] wyszukujemy pakiety za pomocą narzędzia [NuGet];
![]() |
- w [2] wyszukujemy pakiety online;
- w [3] wpisuje się słowo kluczowe spring w polu wyszukiwania;
- w [4] wyświetlane są pakiety, których opis zawiera to słowo kluczowe. W tym przypadku odpowiedni jest dla nas [Spring.Core]. Instalujemy go.
Odnośniki projektu zmieniają się w następujący sposób:
![]() |
Pakiet [Spring.Core] był zależny od pakietu [Common.Logging]. Ten również został pobrany. Na tym etapie projekt nie powinien już zawierać żadnych błędów.
Nie oznacza to jednak, że projekt będzie działał. Najpierw musimy skonfigurować Spring w pliku [App.config]. Jest to najtrudniejsza część projektu. Nowy plik [App.config] wygląda następująco:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- Więcej informacji na temat konfiguracji Entity Framework można znaleźć na stronie 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>
<!-- ogólne rejestrowanie-->
<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>
<!-- łańcuchy połączeń -->
<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>
<!-- konfiguracja 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>
<!-- konfiguracja 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>
Zacznijmy od usunięcia wszystkiego, co już znamy: Entity Framework, ciągi połączeń, ProviderFactory. Plik wygląda teraz następująco:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- Więcej informacji na temat konfiguracji Entity Framework można znaleźć na stronie 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>
<!-- ogólne rejestrowanie-->
<sectionGroup name="common">
<section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
</sectionGroup>
</configSections>
...
<!-- konfiguracja 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>
<!-- konfiguracja 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>
- wiersze 3–15: definiują sekcje konfiguracyjne;
- wiersz 8: definiuje klasę, która będzie zarządzać sekcją <spring><context> w pliku XML (wiersze 19–21);
- wiersz 9: definiuje klasę, która będzie zarządzać sekcją <spring><objects> w pliku XML (wiersze 22–24);
- wiersz 13: definiuje klasę, która będzie obsługiwać sekcję <common><logging> w pliku XML (wiersze 27–36);
- wiersze 7–14: są niezmienne. Nie trzeba ich zmieniać w innym projekcie;
- wiersze 18–25: konfiguracja Springa. Jest stabilna, z wyjątkiem wierszy 22–24, które definiują obiekty, które Spring będzie instancjonował;
- wiersz 23: definicja obiektu. Atrybut id jest dowolny. Stanowi identyfikator obiektu. Atrybut type określa klasę, która ma zostać zainicjowana, w postaci „pełna nazwa klasy, Assembly zawierające tę klasę”. Klasą, o której tutaj mowa, jest ta, która implementuje warstwę [DAO]: [RdvMedecins.Dao.Dao]. Aby poznać jej assembly, należy sprawdzić właściwości projektu:
![]() |
W [1] – nazwa zestawu, który należy podać;
- wiersze 27–36: konfiguracja „Common Logging” jest stała. Może zaistnieć potrzeba zmiany poziomu logowania w wierszu 32. Po zakończeniu fazy debugowania można ustawić poziom na INFO.
Ostatecznie plik konfiguracyjny Springa, choć na pierwszy rzut oka wydaje się skomplikowany, okazuje się prosty. Należy zmodyfikować jedynie:
- wiersze 22–24, które definiują obiekty do instancjonowania;
- wiersz 32: poziom logowania.
W programie testowym instrukcja instancjonująca warstwę [DAO] wygląda następująco:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
[ContextRegistry] to klasa Springa, która korzysta z konfiguracji Springa zapisanej w pliku [Web.config] lub [App.config]. W tym przypadku wykorzystuje ona następującą sekcję pliku [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() wykorzystuje kontekst wierszy 2–4. Wiersz 3 oznacza, że obiekty Springa są zdefiniowane w sekcji [spring/objects] pliku konfiguracyjnego. Sekcja ta obejmuje wiersze 5–7;
- ContextRegistry.GetContext().GetObject("rdvmedecinsDao") wykorzystuje sekcję z wierszy 5–7. Zwraca ona odwołanie do obiektu, który ma atrybut id= „rdvmedecinsDao”. Jest to obiekt zdefiniowany w wierszu 6. Następnie Spring instancjonuje klasę zdefiniowaną przez atrybut type, wykorzystując jej konstruktor bez parametrów. Konstruktor ten musi zatem istnieć. Po wykonaniu tej operacji odwołanie do utworzonego obiektu jest zwracane do kodu wywołującego. Jeśli obiekt zostanie wywołany po raz drugi w kodzie, Spring po prostu zwraca odwołanie do pierwszego utworzonego obiektu. Jest to wzorzec projektowy (Design Pattern) zwany singletonem.
Tworzenie obiektu może być bardziej złożone. Można użyć konstruktora z parametrami lub określić inicjalizację niektórych pól obiektu po jego utworzeniu. Więcej informacji na ten temat można znaleźć w artykule „Samouczek Spring IOC dla .NET”, dostępnym pod adresem URL [http://tahe.developpez.com/dotnet/springioc/].
Po wykonaniu tych czynności możemy uruchomić aplikację. Wyniki wyświetlane na ekranie są następujące:
Wyniki są zgodne z oczekiwaniami. Od tej pory będziemy uznawać, że nasza warstwa [DAO] jest poprawna. Samouczek mógłby się na tym zakończyć. Do tej pory pokazaliśmy:
- podstawy pracy z Entity Framework 5 w ORM;
- warstwę [DAO] wykorzystującą ten ORM.
Przypomnijmy sobie nasz przypadek użycia opisany na początku tego dokumentu. Wychodzimy od istniejącej aplikacji o następującej architekturze:
![]() |
którą chcemy przekształcić w następującą:
![]() |
gdzie EF5 zastąpiło NHibernate. Właśnie stworzyliśmy warstwę [DAO2]. W rzeczywistości nie posiada ona takiego samego interfejsu jak warstwa [DAO1], której interfejs był bardziej ograniczony:
public interface IDao
{
// lista klientów
List<Client> GetAllClients();
// lista lekarzy
List<Medecin> GetAllMedecins();
// lista terminów wizyt u lekarza
List<Creneau> GetCreneauxMedecin(int idMedecin);
// lista RV danego lekarza w danym dniu
List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
// dodaj RV
int AjouterRv(DateTime jour, int idCreneau, int idClient);
// usunięcie terminu RV
void SupprimerRv(int idRv);
}
Warstwa [DAO2] dodała do tego interfejsu metodę:
// znalezienie encji T na podstawie jej klucza głównego
T Find<T>(int id) where T : class;
Dodanie tej metody wynika z faktu, że warstwa ORM EF 5 domyślnie działa w trybie Lazy Loading. Entities trafiają do warstwy [ASP.NET] bez swoich zależności. Powyższa metoda pozwala nam je pobrać w razie potrzeby, a w niektórych przypadkach jest to konieczne. NHibernate również domyślnie działa w trybie Lazy Loading, ale korzystałem z niego w trybie Eager Loading. Entities trafiały do warstwy [ASP.NET] wraz ze swoimi zależnościami.
Zakończymy przenoszenie aplikacji ASP.NET / NHibernate do aplikacji ASP.NET / EF 5. Ponieważ jednak nie dotyczy to już aplikacji EF5, nie będziemy komentować kodu internetowego. Wyjaśnimy jedynie, jak skonfigurować aplikację internetową i ją przetestować. Jest ona dostępna na stronie internetowej niniejszego samouczka.
3.6.6. Generowanie DLL na podstawie warstwy [DAO]
W następującej architekturze:
![]() |
warstwa [ASP.NET] będzie miała do dyspozycji warstwy znajdujące się po jej prawej stronie w postaci DLL. Tworzymy zatem warstwę DLL na podstawie warstwy [DAO].
![]() |
- w [1] wybieramy program testowy, a w [2] nie uwzględniamy go w generowanym pliku DLL;
- w pliku [3], w właściwościach projektu, określa się, że tworzony plik assembly to DLL;
- w pliku [4], w menu pliku VS, należy wskazać, że zostanie wygenerowany plik typu [Release], który zawiera mniej informacji niż plik typu [Debug];
![]() |
- w [5] ponownie generuje się zestaw projektu. Zostanie wygenerowany plik DLL;
- w pliku [6] wyświetla się wszystkie pliki projektu;
![]() |
- w [7], plik DLL z projektu warstwy [DAO]. To właśnie ten plik zostanie wykorzystany przez projekt internetowy ASP.NET;
- w [8] odświeżamy wyświetlanie projektu;
![]() |
- w [9] pliki DLL z folderu [Release] są gromadzone w zewnętrznym folderze [lib], który znajduje się w folderze [10]. To właśnie stamtąd projekt internetowy będzie pobierał swoje odniesienia.
3.6.7. Warstwa [ASP.NET]
W tym miejscu wyjaśnimy proces przeniesienia aplikacji [ASP.NET / NHibernate] do aplikacji [ASP.NET / EF 5]. Będziemy pracować z programem Visual Studio Express 2012 dla sieci WWW, dostępnym bezpłatnie na stronie URL [http://www.microsoft.com/visualstudio/fra/downloads].
Będziemy pracować na podstawie istniejącego projektu internetowego utworzonego w programie VS 2010.
![]() |
- w [1] otwieramy istniejący projekt:
- w [2] załadowany projekt ma następujące odniesienia do [3]:
- [NHibernate] jest plikiem DLL w ramach frameworka NHibernate,
- [Spring.Core] jest wersją DLL w ramach frameworka Spring.net,
- [log4net] jest wersją DLL w ramach frameworka logowania log4net. Framework ten jest wykorzystywany przez Spring.net,
- [MySql.Data] jest sterownikiem ADO.NET dla SGBD MySQL,
- [rdvmedecins] jest DLL warstwy [DAO] zbudowanej przy użyciu NHibernate;
- w [4] zmieniamy nazwę projektu, a w [5] usuwamy poprzednie odniesienia;
![]() |
- w pliku [6] dodajemy odniesienia do projektu;
- w [7], w kreatorze korzystamy z opcji [Parcourir];
![]() |
- w pliku [8] wybieramy wszystkie pliki DLL z projektu nr 2, umieszczone wcześniej w folderze [lib];
- w pliku [9] tworzymy podsumowanie, które zatwierdzamy;
- w [10] – projekt internetowy wraz z nowymi odniesieniami.
Po wykonaniu tej czynności projekt wygląda następująco:
![]() |
- w pliku [1] kod zarządzający stronami internetowymi został podzielony na dwa pliki: [Global.asax] i [Default.aspx]. Kod pomocniczy umieszczono w folderze [Entites]. Wreszcie aplikacja jest konfigurowana przez plik [Web.config];
- w pliku [2] generujemy zestaw projektowy;
- w pliku [3] pojawiają się błędy.
Przyjrzyjmy się tym błędom, na przykład następnemu:
![]()
oraz jej wyjaśnienie:
![]()
Typ [medecin.Id] to int?, podczas gdy metoda [GetCreneauxMedecin] jest typu int. Potrzebny jest zatem cast. Ten błąd powtarza się w całym kodzie, ponieważ encje projektu ASP.NET / NHibernate miały klucze główne typu int, podczas gdy te w projekcie ASP.NET / EF 5 są typu int?. Poprawiamy wszystkie błędy tego typu i ponownie generujemy projekt. Wtedy już ich nie ma.
Przed uruchomieniem projektu pozostaje nam jeszcze jedna kwestia do rozwiązania: instancjonowanie warstwy [DAO] przez framework Spring. Odbywa się to w pliku [Global.asax]:
protected void Application_Start(object sender, EventArgs e)
{
// buforujemy niektóre dane z bazy danych
try
{
// instancjonowanie warstwy [dao]
Dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
...
}
catch (Exception ex)
{...
}
}
W programie testowym warstwy [DAO] instancjonowano warstwę [DAO] w następujący sposób:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
Obie metody są identyczne. Przypomnijmy, że to instancjonowanie warstwy [DAO] opierało się na konfiguracji dokonanej w [App.config]. Następnie zastępujemy aktualną zawartość [Web.config] projektu internetowego zawartością [App.config] z projektu warstwy [DAO], aby uzyskać tę samą konfigurację.
Jesteśmy gotowi do pierwszego uruchomienia. Wyświetlana jest strona główna [1]:
![]() |
- w [2] wpisujemy datę spotkania i zatwierdzamy;
![]() |
- w [3] pojawia się błąd.
Po przeanalizowaniu tekstu błędu wyświetlanego na stronie okazuje się, że zgłoszony wyjątek dotyczy funkcji Lazy Loading: próbowano załadować zależność obiektu, podczas gdy kontekst trwałości, który nim zarządza, został zamknięty. Obiekt znajduje się teraz w stanie „odłączonym”. Błąd ten wynika z faktu, że obiekt NHibernate był używany w trybie Eager Loading, podczas gdy obiekt EF domyślnie działa w trybie Lazy Loading. W powyższym wierszu zaznaczonym na czerwono:
- rdv oznacza obiekt [Rv], który został załadowany bez swoich zależności;
- aby obliczyć wartość rdv.Creneau.Id, aplikacja próbuje załadować zależność rdv.Creneau. Ponieważ jednak nie znajdujemy się już w odpowiednim kontekście, nie jest to możliwe, stąd wyjątek.
W tym przypadku rozwiązanie jest proste. W linii 108 tworzymy wpis w słowniku, którego kluczem jest klucz podstawowy przedziału czasowego spotkania. Okazuje się jednak, że encja [Rv] zawiera klucz podstawowy powiązanego przedziału czasowego. Piszemy zatem:
dicoRvPris[(int)rdv.CreneauId] = rdv;
Ponownie próbujemy wykonać kod. Tym razem pojawia się następujący błąd:
![]() |
Błąd jest podobny. W wierszu 132 próbujemy załadować zależność [Client] obiektu [Rv] do warstwy ASP.NET, a więc poza kontekstem. Należy pobrać obiekt [Client] z bazy danych. Aby rozwiązać ten problem, interfejs [IDao] został wzbogacony o następującą metodę:
// znalezienie encji T na podstawie jej klucza głównego
T Find<T>(int id) where T : class;
Umożliwi to pobranie zależności. W ten sposób powyższy błędny wiersz zostanie przepisany w następujący sposób:
Client client = Global.Dao.Find<Client>(agenda.Creneaux[i].Rdv.ClientId);
Ponownie warto zwrócić uwagę na korzyści płynące z tego, że encje zawierają swoje klucze obce. W tym przypadku encja [Rv] zapewnia nam dostęp do klucza obcego powiązanej zależności [Creneau]. Po wprowadzeniu tych dwóch poprawek aplikacja działa poprawnie. Zachęcamy czytelnika do przetestowania aplikacji [RdvMedecins-SqlServer-03], dostępnej w sekcji plików do pobrania z przykładami na stronie internetowej poświęconej temu artykułowi.
3.7. Conclusion
Udało nam się pomyślnie przeprowadzić portowanie aplikacji ASP.NET / NHibernate:
![]() |
na aplikację ASP.NET / EF 5:
![]() |
Chociaż ta architektura powinna była pozwolić nam zachować warstwę [ASP.NET] w niezmienionym stanie, musieliśmy ją zmodyfikować z dwóch powodów:
- entytety nie były dokładnie takie same. Typ kluczy głównych entytetów NHibernate to int, podczas gdy w przypadku EF 5 był to int?. To skłoniło nas do wprowadzenia cast do kodu internetowego;
- sposób ładowania encji nie był taki sam dla obu encji ORM: ładowanie natychmiastowe (Eager Loading) dla NHibernate, ładowanie opóźnione (Lazy Loading) dla EF 5. To skłoniło nas do wzbogacenia interfejsu warstwy [DAO] o metodę generyczną umożliwiającą pobranie encji na podstawie jej klucza głównego.
Niemniej jednak przeniesienie okazało się raczej proste, co po raz kolejny potwierdza – gdyby było to potrzebne – słuszność architektury warstwowej oraz wstrzykiwania zależności za pomocą Springa lub innego frameworka do wstrzykiwania zależności.
Teraz zamierzamy ocenić wpływ zmiany warstwy SGBD na poprzednią architekturę. Przeniesiemy wszystkie poprzednie projekty do czterech innych warstw SGBD:
- Oracle Database Express Edition 11g, wersja 2;
- MySQL 5.5.28;
- PostgreSQL 9.2.1;
- Firebird 2.1.
Kody nie ulegną już zmianie. Zmianie ulegną jedynie następujące elementy:
- definicja w encjach pola służącego do kontroli współbieżności dostępu do encji;
- pliki konfiguracyjne [App.config] lub [Web.config];
Omówimy tylko te elementy, które ulegają zmianie.


















































































































































