niedziela, 30 grudnia 2012

[SQL|ORM] Entity Framework : Stored Procedures

Procedury składowane to narzędzie, z którego bardzo często warto skorzystać. Przeniesienie niektórych obliczeń na serwer bazy danych może usprawnić działanie aplikacji i uporządkować ją z logicznego punktu widzenia. Ponieważ procedury "żyją" po stronie bazy danych, należy wiedzieć, jak wywoływać je z poziomu narzędzia ORM. Poniżej kilka praktycznych przykładów w EF:

Procedura zwracająca kolekcję encji

Dla tabeli Customer mamy przykładową procedurę składowaną:

CREATE PROCEDURE dbo.GetCustomers
(@Company VARCHAR(50), @ContactTitle VARCHAR(50))
AS
 BEGIN
  SELECT * FROM Customer
  WHERE (@Company IS null OR Company = @Company) AND
  (@ContactTitle is null or ContactTitle = @ContactTitle)
 END

Po dodaniu do EDM odpowiednich encji i procedury GetCustomers, przechodzimy do okna designera. Z menu dostępnego pod prawym przyciskiem myszy wybieramy opcję Add -> Function Import...
W menu należy podać nazwę procedury, oraz określić co ma zwracać ( w tym przypadku kolekcję encji typu Customer). To wszystko, teraz w kodzie możemy na obiekcie kontekstu wołać GetCustomers.

var allCustomers = context.GetCustomers("GoShopNow.com", "Sales Manager");
foreach (var customer in allCustomers)
{
    Console.WriteLine("\t{0}", customer.Name);
}

Procedura zwracająca wartości przez parametr

Przykładowa procedura przyjmująca jeden parametr wejściowy i dwa wyjściowe, zwracająca dodatkowo kolekcję encji.

CREATE PROCEDURE dbo.GetVehiclesWithRentals
(@date DATE,
@totalRentals INT OUTPUT,
@totalPayments DECIMAL(18,2) OUTPUT)
AS
 BEGIN
 SELECT @totalRentals = COUNT(*), @totalPayments = SUM(Payment)
 FROM dbo.Rental
 WHERE RentalDate = @date
 
 SELECT DISTINCT v.*
 FROM dbo.Vehicle AS v JOIN dbo.Rental AS r
 ON v.VehicleId = r.VehicleId 
END

Procedurę taką dodajemy w sposób analogiczny, jak poprzednio. Różnice pojawiają się w kodzie. Korzystamy z obiektów typu ObjectParameter

string reportDate = "2/2/2010";
var totalRentals = new ObjectParameter("totalRentals", typeof (int));
var totalPayments = new ObjectParameter("totalPayments", typeof(decimal));
var vehicles = context.GetVehiclesWIthRentals(DateTime.Parse(reportDate), 
    totalRentals, totalPayments);
foreach (var vehicle in vehicles)
{
    Console.WriteLine("{0} {1}, {2} year", vehicle.Manufacturer, vehicle.Model,
        vehicle.Year);
}
Console.WriteLine("Total Rentals: {0}", totalRentals.Value);
Console.WriteLine("Total Payments: {0}", totalPayments.Value);

Custom Functions

Custom Functions to funkcje definiowane na poziome storage model. Nie musimy zatem (być może nie mamy dostępu) tworzyć procedur na serwerze bazy danych, a możemy je dodać do SSDL. Zatem treść procedury dodajemy do schematu bazy <Schema></Schema> edytując plik.edmx jako XML. Na przykład można dodać taką procedurę:

<Function Name="MembersWithTheMostMessages" IsComposable="false">
  <CommandText>
    select m.*
    from dbo.Member m
    join
    (
    select distinct msg.MemberId
    from dbo.Message msg where DateSent = @datesent
    ) temp on m.MemberId = temp.MemberId
  </CommandText>
  <Parameter Name="datesent" Type="date" />
</Function>

Funkcję taką wykorzystuje się tam samo jak procedurę składowaną

var members = context.MemberWithTheMostMessages(today);

piątek, 28 grudnia 2012

[SQL|ORM] Entity Framework : Plain Old CLR Objects

Klasy generowane przez EF mają jedną zasadniczą wadę: są mocno związane z konkretnym EDM. Fakt ten ogranicza możliwości testowania i praktycznie eliminuje reużywalność. Jeżeli  chcemy skorzystać z własnych klas jako encji, mamy do dyspozycji mechanizm POCO. Nazwa Plain Old CLR oznacza mniej więcej tyle, że obiekty nie mają referencji do zewnętrznych frameworków, nie muszą dziedziczyć po żadnych specyficznych klasach.

Przykładowo, mamy zestaw tabel, mapowany przez EF na poniższy zestaw encji:


W pierwszej kolejności należy wyłączyć generowanie klas ustawiając w Designerze opcję Code Generation Strategy na None. Następnie tworzymy klasy dla encji. Każda nazwa oraz typ property (również navigation property) musi zgadzać się z nazwą z designera. Kolekcje reprezentujemy przez ISet<T>.

 public class Customer
    {
        public int CustomerId { get; set; }
        public string CustomerName { get; set; }
        public ISet<Order> Orders { get; set; }
        public Customer()
        {
            Orders = new HashSet<Order>();
        }
    }

    public class Order
    {
        public int OrderId { get; set; }
        public int CustomerId { get; set; }
        public DateTime OrderDate { get; set; }
        public Customer Customer { get; set; }
        public ISet<OrderDetail> OrderDetails { get; set; }
        public Order()
        {
            OrderDetails = new HashSet<OrderDetail>();
        }
    }

    public class OrderDetail
    {
        public int OrderId { get; set; }
        public int ProductId { get; set; }
        public decimal UnitPrice { get; set; }
        public int Quantitiy { get; set; }
        public Order Order { get; set; }
        public Product Product { get; set; }
    }

    public class Product
    {
        public int ProductId { get; set; }
        public string ProductName { get; set; }
        public decimal UnitPrice { get; set; }
    }

Ostatni krok to konieczność stworzenia klasy kontekstu.

public class EntityFrameworkRecipes : ObjectContext
{
    private ObjectSet<Customer> _customers;
    private ObjectSet<Order> _orders;
    private ObjectSet<OrderDetail> _orderdetails;
    private ObjectSet<Product> _products;

    public EntityFrameworkRecipes()
        : base("name=EntityFrameworkRecipesEntities2", "EntityFrameworkRecipesEntities2")
    {
        _orders = CreateObjectSet<Order>();
        _orderdetails = CreateObjectSet<OrderDetail>();
        _products = CreateObjectSet<Product>();
    }

    public ObjectSet<Customer> Customers
    {
        get { return _customers ?? (_customers = CreateObjectSet<Customer>()); }
    }

    public ObjectSet<Order> Orders
    {
        get { return _orders; }
    }

    public ObjectSet<OrderDetail> OrderDetails
    {
        get { return _orderdetails; }
    }

    public ObjectSet<Product> Products
    {
        get { return _products; }
    }
}

Do konstruktora bazowego przekazujemy ścieżkę do connectionstring z pliku konfiguracyjnego, oraz nazwę kontenera.

Wczytywanie encji powiązanych

Klasa ObjectContext udostępnie metodę LoadProperty i to z niej należy skorzystać, aby wczytać powiązane encje

context.LoadProperty(venue, v => v.Events);

Lazy Loading

Aby zapewnić Lazy Loading należy odpowiednie propercje w obiektach POCO oznaczyć słowem kluczowym virtual.  Dzięki temu stworzone zostaną obiekty proxy umożliwiające Lazy Loading. Dodatkowo na obiekcie kontekstu, w property ContextOptions należy ustawić opcję LazyLoadingEnabled na true.

sobota, 22 grudnia 2012

[SQL|ORM] Entity Framework : CSDL, MSL, SSDL

Pliki .edmx opisane językiem XML składają się z kilku części. Wszystkie zmiany wykonywane w Entity Designerze zapisywane są właśnie w plikach .edmx. Dane z CSDL, MSL i SSDL możemy zaszyć w assembly, bądź wyeksportować do wyjściowego katalogu zaznaczając opcję Metada Artifact Processing na Copy To Output Directory.

CSDL (Conceptual Schema Definition Language) - język opisujący encje relacje i funkcje, czyli schemat bazy danych na poziomie konceptualnym. Typy danych odpowiadają typom z CLR.

Przykładowy plik .csdl

<?xml version="1.0" encoding="utf-8"?>
<Schema Namespace="EntityFrameworkRecipesModel" Alias="Self"
 xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation" 
 xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
  <EntityContainer Name="EntityFrameworkRecipesEntities1"
   annotation:LazyLoadingEnabled="true">
    <EntitySet Name="Poem" EntityType="EntityFrameworkRecipesModel.Poem" />
    <EntitySet Name="Poet" EntityType="EntityFrameworkRecipesModel.Poet" />
    <AssociationSet Name="FK_Poem_Poet" Association="EntityFrameworkRecipesModel.FK_Poem_Poet">
      <End Role="Poet" EntitySet="Poet" />
      <End Role="Poem" EntitySet="Poem" />
    </AssociationSet>
  </EntityContainer>
  <EntityType Name="Poem">
    <Key>
      <PropertyRef Name="PoemId" />
    </Key>
    <Property Name="PoemId" Type="Int32" Nullable="false" />
    <Property Name="PoetId" Type="Int32" />
    <Property Name="Title" Type="String" MaxLength="Max" Unicode="false" FixedLength="false" />
    <Property Name="MeterId" Type="Int32" />
    <NavigationProperty Name="Poet" Relationship="EntityFrameworkRecipesModel.FK_Poem_Poet" 
      FromRole="Poem" ToRole="Poet" />
  </EntityType>
  <EntityType Name="Poet">
    <Key>
      <PropertyRef Name="PoetId" />
    </Key>
    <Property Name="PoetId" Type="Int32" Nullable="false" />
    <Property Name="FirstName" Type="String" MaxLength="50" Unicode="false" FixedLength="false" />
    <Property Name="MiddleName" Type="String" MaxLength="50" Unicode="false" FixedLength="false" />
    <Property Name="LastName" Type="String" MaxLength="50" Unicode="false" FixedLength="false" />
    <NavigationProperty Name="Poem" Relationship="EntityFrameworkRecipesModel.FK_Poem_Poet" 
      FromRole="Poet" ToRole="Poem" />
  </EntityType>
  <Association Name="FK_Poem_Poet">
    <End Role="Poet" Type="EntityFrameworkRecipesModel.Poet" Multiplicity="0..1" />
    <End Role="Poem" Type="EntityFrameworkRecipesModel.Poem" Multiplicity="*" />
    <ReferentialConstraint>
      <Principal Role="Poet">
        <PropertyRef Name="PoetId" />
      </Principal>
      <Dependent Role="Poem">
        <PropertyRef Name="PoetId" />
      </Dependent>
    </ReferentialConstraint>
  </Association>
</Schema>

SSDL (Store Schema Definition Language) - język opisujący schemat bazy danych na poziomie zasobu, w którym dane są przechowywane (bazy danych). Typy propercji są typami z SQL.

Przykładowy plik .ssdl

<?xml version="1.0" encoding="utf-8"?>
<Schema Namespace="EntityFrameworkRecipesModel.Store" Alias="Self" 
  Provider="System.Data.SqlClient" ProviderManifestToken="2008" 
  xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" 
  xmlns="http://schemas.microsoft.com/ado/2009/02/edm/ssdl">
  <EntityContainer Name="EntityFrameworkRecipesModelStoreContainer">
    <EntitySet Name="Poem" EntityType="EntityFrameworkRecipesModel.Store.Poem" 
      store:Type="Tables" Schema="dbo" />
    <EntitySet Name="Poet" EntityType="EntityFrameworkRecipesModel.Store.Poet" 
      store:Type="Tables" Schema="dbo" />
    <AssociationSet Name="FK_Poem_Poet" Association="EntityFrameworkRecipesModel.Store.FK_Poem_Poet">
      <End Role="Poet" EntitySet="Poet" />
      <End Role="Poem" EntitySet="Poem" />
    </AssociationSet>
  </EntityContainer>
  <EntityType Name="Poem">
    <Key>
      <PropertyRef Name="PoemId" />
    </Key>
    <Property Name="PoemId" Type="int" Nullable="false" />
    <Property Name="PoetId" Type="int" />
    <Property Name="Title" Type="varchar(max)" />
    <Property Name="MeterId" Type="int" />
  </EntityType>
  <EntityType Name="Poet">
    <Key>
      <PropertyRef Name="PoetId" />
    </Key>
    <Property Name="PoetId" Type="int" Nullable="false" />
    <Property Name="FirstName" Type="varchar" MaxLength="50" />
    <Property Name="MiddleName" Type="varchar" MaxLength="50" />
    <Property Name="LastName" Type="varchar" MaxLength="50" />
  </EntityType>
  <Association Name="FK_Poem_Poet">
    <End Role="Poet" Type="EntityFrameworkRecipesModel.Store.Poet" Multiplicity="0..1" />
    <End Role="Poem" Type="EntityFrameworkRecipesModel.Store.Poem" Multiplicity="*" />
    <ReferentialConstraint>
      <Principal Role="Poet">
        <PropertyRef Name="PoetId" />
      </Principal>
      <Dependent Role="Poem">
        <PropertyRef Name="PoetId" />
      </Dependent>
    </ReferentialConstraint>
  </Association>
</Schema>

MSL(Mapping Specification Language) - język mapowania pomiędzy typami z SSDL i CSDL

Przykładowy plik .msl

<?xml version="1.0" encoding="utf-8"?>
<Mapping Space="C-S" xmlns="http://schemas.microsoft.com/ado/2008/09/mapping/cs">
  <EntityContainerMapping StorageEntityContainer="EntityFrameworkRecipesModelStoreContainer" 
    CdmEntityContainer="EntityFrameworkRecipesEntities1">
    <EntitySetMapping Name="Poem">
      <EntityTypeMapping TypeName="EntityFrameworkRecipesModel.Poem">
        <MappingFragment StoreEntitySet="Poem">
          <ScalarProperty Name="PoemId" ColumnName="PoemId" />
          <ScalarProperty Name="PoetId" ColumnName="PoetId" />
          <ScalarProperty Name="Title" ColumnName="Title" />
          <ScalarProperty Name="MeterId" ColumnName="MeterId" />
        </MappingFragment>
      </EntityTypeMapping>
    </EntitySetMapping>
    <EntitySetMapping Name="Poet">
      <EntityTypeMapping TypeName="EntityFrameworkRecipesModel.Poet">
        <MappingFragment StoreEntitySet="Poet">
          <ScalarProperty Name="PoetId" ColumnName="PoetId" />
          <ScalarProperty Name="FirstName" ColumnName="FirstName" />
          <ScalarProperty Name="MiddleName" ColumnName="MiddleName" />
          <ScalarProperty Name="LastName" ColumnName="LastName" />
        </MappingFragment>
      </EntityTypeMapping>
    </EntitySetMapping>
  </EntityContainerMapping>
</Mapping>

Cała struktura CSDL, MSL i SSDL jest generowana przez Entity Designer, jednak nic nie stoi na przeszkodzie, by ręczenie edytyować fragmenty pliku .edmx do naszych potrzeb.

czwartek, 13 grudnia 2012

[SQL|ORM] Entity Framework : Loading Entities

Entity Framework udostępnia szereg mechanizmów umożliwiających sterowanie sposobem ładowania encji powiązanych w zapytaniach.Domyślnie ładowane są jedynie encje bezpośrednio wołane w zapytaniu. Czasami jednak potrzebujemy zredukować ilość zapytań do bazy danych i pobrać zarówno bezpośrednio interesujące nas encje, jak i encje powiązane za pomocą jednego zapytania. Wiąże się to jednak ze znacznym zwiększeniem grafu obiektów w pamięci. Nie ma jednoznacznej odpowiedzi na to, jak powinno się konstruować zapytania, wiele zależy od ilości danych i częstości korzystania z danych powiązanych.

Ładowanie encji powiązanych

W tym celu należy skorzystać z funkcji Include(), gdzie przez string podaje się nazwę navigation property..

var customers = context.Customers.Include("CustomerType").
                    Include("CustomerEmails");
foreach (var customer in customers)
{
    Console.WriteLine("Name : {0}, Email : {1}", customer.Name, 
        customer.CustomerType.Description);
    foreach (var customerEmail in customer.CustomerEmails)
    {
        Console.WriteLine(customerEmail.Email);
    }
}

Wykorzystując funkcję Include powyższy kod przetransformuje się do jednego dużego zapytania do bazy danych. Jeśli usuniemy Include, dostejemy po dwa dodatkowe zapytania do bazy danych dla każdej encji z Customers.

Pobieranie całego grafu encji

W bardziej skomplikowanym przypadku, przedstawionym na poniższym diagramie, poprzez odpowiedni dobór kolejności wywołań funkcji Include(), również możemy w jednym zapytaniu załadować do pamięci pełny graf.



var graph = context.Courses.Include("Sections.Students")
                    .Include("Sections.Instructor");

A zatem korzystając z funkcji Include możemy podawać nazwy encji z całego grafu poprzez ścieżkę do takiej nazwy budowaną przy użyciu navigation properties.

W przypadku dziedziczenia encji, gdy tworzymy zapytanie o encje dziedziczące, możemy bez problemu korzystać z navigation properties encji bazowych.

Korzystanie z Include w dowolnych zapytaniach LINQ

Jeżeli chcemy wykorzystać metodę Include() w przypadku dowolnych zapytań, np. joinów, group by czy where, warto pamiętać o kilku faktach:
  • Niezmaterializowane wyrażenie LINQ musimy rzutować do typu ObjectQuery<T>, aby móc skorzystać z metody Include()
  • Include() jest stosowane jedynie do końcowych rezultatów zapytania, w podzapytaniach będzie ignorowane
  • Include() będzie ignorowane, gdy kolekcja zawiera cokolwiek innego niż encje
W poniższym przykładzie zmienna events przechowuje zapytanie typu IQueryable<T> , dopiero podczas materializacji możemy skorzystać z Include().

var events = from ev in context.Events
             where ev.Club.City == "New York"
             group ev by ev.Club
             into g
             select g.FirstOrDefault(e1 => e1.EventDate == g
                 .Min(s => s.EventDate));
var e = ((ObjectQuery<Event>) events).Include("Club").First();

Opóźnione ładowanie encji powiązanych

Sytuacja przedstawia się następująco: mamy jeden obiekt w pamięci i chcemy pobrać dla niego dane z wielu encji powiązanych. Nie chcemy drugi raz pobierać instancji obiektu zmaterializowanego. Możemy skorzystać z metody CreateSourceQuery(), a na zwróconym przez nią obiekcie wywołać dopiero Include(). Tak zbudowane zapytanie należy dołożyć do kontekstu za pomocą funkcji Attach(). Jest to rozwiązanie bardzo wydajne, ponieważ nie musimy po raz drugi pozyskiwać kolumn z encji, którą mamy już w pamięci.

var jill = context.Employees.Where(e => e.Name == "Jill Carpenter").First();
var moreResults = jill.DepartmentReference.CreateSourceQuery()
    .Include("Company").First();
context.Attach(moreResults);
Console.WriteLine("{0} works in {1}", jill.Name, jill.Department.Company.Name);

CreateSourceQuery() zwraca obiekt zapytania, który w momencie wykonania zwraca ten sam zestaw obiektów, który istnieje w obecnej kolekcji.
Attach() dołącza obiekt lub graf obiektów do kontekstu.

Filtrowanie i sortowanie encji powiązanych

Mamy encję w pamięci i chcemy przefiltrować oraz posortować encje z nią powiązane. Kolejny raz skorzystać można z metody CreateSourceQuery(), która pozwoli uzyskać dostęp do zapytania używanego przy pobieraniu kolekcji przez navigation property. Funkcję Include() musimy wywołać w momecie, gdy mamy do czynienia z odpowiednim typem (ObjectQuery<T>). Metoda Attach() łączy przefiltrowane encje z pierwszą encją w pamięci.

var hotel = context.Hotels.First();
var rooms = hotel.Rooms.CreateSourceQuery()
            .Include("Reservations")
            .Where(r => r is ExecutiveSuite && r.Reservations.Any())
            .OrderBy(r => r.Rate);

hotel.Rooms.Attach(rooms);

Sprawdzenie, czy referencja została już załadowana do pamięci 

Chcemy sprawdzić, czy dana referencja do encji powiązanej (lub powiązanej kolekcji) została już załadowana do kontekstu. EF udostępnie property IsLoaded. Różnica polega na tym, że dla kolekcji sprawdzamy IsLoaded bezpośrednio na navigation property, natomiast dla pojedynczych referencji przez nazwa_propertyReference.

var project = context.Projects.Include("Manager").First();
if(project.ManagerReference.IsLoaded)
    Console.WriteLine("Manager reference is loaded");
else
    Console.WriteLine("Manager reference is NOT loaded");
if(project.Contractors.IsLoaded)
    Console.WriteLine("Contractors are loaded");

Aby doczytać kolekcje w sposób jawny, można skorzystać z funkcji Load(), przed jej wywołaniem warto sprawdzić, czy referencja jest już w pamięci za pomocą IsLoaded. Metoda Load jest przeładowana tak, że jako parametr przyjmuje flagę MergeOption, dostępne opcje:
  • AppendOnly dołącza instancje, których nie ma obecnie w kontekście
  • OverwriteChanges przywraca do kontekstu stan z bazy danych
  • NoTracking wyłącza śledzenie stanu
  • PreserveChanges przeciwieństwo dla OverwriteChanges

czwartek, 6 grudnia 2012

[SQL|ORM] Entity Framework : Queries

Przegląd zagadnień związanych z pisaniem zapytań w Entity Framework:

Wyrażenia SQL

Do wykonywania zapytań SQL korzystać można z metody ExecuteStoreCommand wołanej na obiekcie kontekstu. Metodzie przekazujemy przez string treść zapytania, oraz przez tablicę zestaw parametrów zapytania, zwraca ona ilość zmienionych rekordów w bazie.

using (var context = new EntityFrameworkRecipesEntities())
{
    string sql =
        @"insert into dbo.Payment(Amount,Vendor)
                   values (@Amount, @Vendor)";
    var args = new DbParameter[]
                   {
                       new SqlParameter() {ParameterName = "Amount", Value = 99.9M},
                       new SqlParameter() {ParameterName = "Vendor", Value = "Ace Plumbing"}
                   };
    int rowCount = context.ExecuteStoreCommand(sql, args);
}

Jeżeli chcemy zwrócić kolekcję obiektów, korzystamy z metody ExecuteStoreQuery<>.

using (var context = new EntityFrameworkRecipesEntities1())
{
    var sql = "select * from dbo.Student where Degree = @Major";
    var args = new DbParameter[]
                   {
                       new SqlParameter() {ParameterName = "Major", Value = "Masters"}
                   };
    var students = context.ExecuteStoreQuery<Student>(sql, args);
}

Wyrażenia EntitySQL

Entity SQL jest modyfikacją znanego SQL na potrzeby Entity Framework. Do tego typu zapytań wykorzystuje się metodę CreateQuery.

using (var context = new EntityFrameworkRecipesEntities2())
{
    var esql = "select value c from Customers as c";
    var customers = context.CreateQuery<Customer>(esql);
    foreach (var customer in customers)
    {
        Console.WriteLine("{0} {1}", customer.Name, customer.Email);
    }
}

Kluczowe jest słowo value, które umożliwia mapowanie rezultatu bezpośrednio do typu Customer.

Entity SQL może się okazać także przydatny w momencie, gdy modelujemy encje na zasadzie dziedziczenia typu "Tabela per Typ" i chcemy otrzymać jedynie encje pewnego typu. Przykładowo dla hierarchii encji jak poniżej


zapytanie wygląda następująco:

using (var context = new EntityFrameworkRecipesEntities3())
{
    var esql = "select value p from OfType(People,Querying.Teacher) as p";
    var teachers = context.CreateQuery<Teacher>(esql);
    Console.WriteLine("Teachers...");
    foreach (var teacher in teachers)
    {
        Console.WriteLine("{0}, isProfessor:{1}", teacher.Name, teacher.IsProfessor);
    }
}

Jako drugi parametr podajemy typ z CLR, który ma posłużyć jako filtr. Zatem w tym przykadku Querying jest nazwą namespace. Alternatywna wersja to:

var esql = "using Querying; select value p from OfType(People,Teacher) as p";

Zwracanie więcej niż jednej kolekcji danych przez procedurę składowaną

Mamy w bazie danych dwie tabele powiązane kluczem obcym, oraz procedurę składowaną, wybierającą wszystkie rekordy z obu tabel. Po dodaniu tabel i procedury do EDM, możemy skorzystać z tej procedury do pobrania dwóch kolekcji danych przy jednym wywołaniu. Niestety mapowanie takie jest nieco bardziej skomplikowane, niż przy wywoływaniu prostych zapytań SQL. Poniżej przykład procedury zwracającej dwa zestawy danych.

CREATE PROCEDURE [dbo].[GetBidDetails] 
AS
 BEGIN  
  SELECT * FROM Job
  SELECT * FROM Bid
 END

oraz sposób obsługi takiej procedury z poziomu Entity Framework:

using (var context = new EntityFrameworkRecipesEntities4())
{
    var cs = @"data source=PC-MKL;initial catalog=EntityFrameworkRecipes;
                integrated security=True;multipleactiveresultsets=True;
                App=EntityFramework";
    var conn = new SqlConnection(cs);
    var cmd = conn.CreateCommand();
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.CommandText = "dbo.GetBidDetails";
    conn.Open();
    var reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
    var jobs = context.Translate<Job>(reader, "Jobs", MergeOption.AppendOnly).ToList();
    reader.NextResult();
    context.Translate<Bid>(reader, "Bids", MergeOption.AppendOnly).ToList();
    foreach (var job in jobs)
    {
        Console.WriteLine("Job: {0}", job.JobDetails);
        foreach (var bid in job.Bids)
        {
            Console.WriteLine("\tBid: {0} from {1}", bid.Amount, bid.Bidder);
        }
    }
}

Aby obsłużyć wiele kolekcji jako wynik procedury musimy skorzystać z obiektu klasy SqlCommand zwracanej przez SqlConnection. Mapowanie kolekcji zwróconej przez procedurę następuje przy pomocy metody Translate, której podajemy, na który obiekt z kontekstu ma zostać zmapowany wynik. Opcja AppendOnly zapewnia śledzenie zmian wykonanych na zwróconych obiektach. Polecenie ToList() wymusza natychmiastowe wykonanie zapytania. Dzięki metodzie NextResult przechodzimy do następnego zbioru wynikowego.

Porównania do kolekcji w pamięci

Pisząc zapytania często potrzebujemy filtrować wyniki po tym, czy wartości występują w pewnej kolekcji danych. W takiej sytuacji nie możemy bezpośrednio skorzystać z operacji join w LINQ, natomiast możemy wykorzystać funkcję Contains. Zapytanie takie przetransformuje się na SQL-owy INNER JOIN.

using (var context = new EntityFrameworkRecipesEntities5())
{
    var cats = new List<string>() {"Programming", "Databases"};
    var books = from b in context.Books
                where cats.Contains(b.Category.Name)
                select b;
    foreach (var book in books)
    {
        Console.WriteLine("{0} : {1}", book.Category.Name, book.Title);
    }
}

Profiler pokazuje następujące zapytanie SQL: 

SELECT 
[Extent1].[BookId] AS [BookId], 
[Extent1].[Title] AS [Title], 
[Extent1].[CategoryId] AS [CategoryId]
FROM   [dbo].[Book] AS [Extent1]
INNER JOIN [dbo].[Category] AS [Extent2] ON [Extent1].[CategoryId] = [Extent2].[CategoryId]
LEFT OUTER JOIN [dbo].[Category] AS [Extent3] ON [Extent1].[CategoryId] = [Extent3].[CategoryId]
WHERE [Extent2].[Name] = N'Programming' OR [Extent3].[Name] = N'Databases'

Grupowanie po dacie

Mamy tabelę z kolumną typu DATE i chcemy pogrupować wyniki po dacie w Entity Framework. Korzystając z LINQ nie możemy grupować bezpośrednio po danym property, ponieważ typ DateTime z CLR nie przetłumaczy się bezpośrednio do SQL. Należy wykorzystać funkcję TruncateTime.

var groups = from r in context.Registrations
             group r by EntityFunctions.TruncateTime(r.RegistrationDate)
                into g
             select g;

Dane zostaną pogrupowane po dniach.

Grupowanie po wielu properties

Do grupowania po kilku properties można skorzystać z typów anonimowych.

var results = from e in context.Events
              group e by new {e.State, e.City}
              into g
              select new
                {
                    State = g.Key.State,
                    City = g.Key.City,
                    Events = g
                };

Join na wielu kolumnach

Przykładowo dla dwóch encji jak poniżej



możemy dokonać takiej operacji przy pomocy LINQ w następujący sposób.

var orders = from o in context.C_Order
             join a in context.C_Account on
                 new {Id = o.AccountId, City = o.ShipCity, State = o.ShipState}
                 equals
                 new {Id = a.AccountId, a.City, a.State}
             select o;

Korzystamy z faktu, że porównanie dwóch typów anonimowych jest porównaniem na zasadzie porównywania par properties i zwróci true jedynie, gdy wszystkie properties są sobie równe.

sobota, 1 grudnia 2012

[SQL|ORM] Entity Framework : Database First

Database First to podejście, w którym mamy już stworzoną bazę danych i chcemy ją jedynie zmapować na encje w naszej aplikacji. Przy takiej okazji pojawia się kilka ciekawych zagadnień.

Modelowanie relacji wiele do wielu

W relacyjnych bazach danych do tworzenia tego typu relacji stosuje się tzw. junction tables. Są to tabele, które zawierają przeważnie dwie kolumny - klucze obce do obu "stron" relacji. Prosty skrypt tworzący taką relację poniżej

CREATE TABLE Album (
  AlbumId INT IDENTITY PRIMARY KEY,
  AlbumName VARCHAR(50)
  )
 
CREATE TABLE Artist (
  ArtistId INT IDENTITY PRIMARY KEY,
  FirstName VARCHAR(50),
  MiddleName VARCHAR(50),
  SecondName VARCHAR(50)
  )
 
CREATE TABLE LinkTable (
  ArtistId INT NOT NULL ,
  AlbumId INT NOT NULL ,
  PRIMARY KEY (ArtistId, AlbumId) ,
  CONSTRAINT fk_Artist_has_Album
    FOREIGN KEY (ArtistId)
    REFERENCES Album (AlbumId),
  CONSTRAINT fk_Album_has_Artist
    FOREIGN KEY (AlbumId)
    REFERENCES Artist (ArtistId)
    );

Po dodaniu ADO .NET Entity Data Model i wybraniu powyższych tabel, otrzymamy w designerze dwie encje połączone relacją wiele do wielu.

Obie encje otrzymały dodatkowe Navigation Properties mapowane do LinkTable. Dzięki temu w łatwy sposób można dodawać Artystów do albumu (i na odwrót) przez dodanie referencji do kolekcji.

Nieco inaczej sytuacja wygląda w przypadku, gdy tabela łącząca zawiera dodatkowe dane.Wtedy Entity Framework utworzy trzy encje danych, jak w przykładzie poniżej:
Dane dodajemy zatem do tabel poprzez stworzenie trzech obiektów.

Modelowanie tabel zawierających referencje do samych siebie

Tabele zawierające klucz główny, oraz klucz obcy będący referencją do własnego klucza głównego są w relacji do samych siebie. Dodając taką tabelę do EDM dostaniemy encję z dwoma dodatkowymi Navigation Properties. Możemy zmienić ich nazwy, na takie, które będą odpowiadały ich reprezentacji. Jedna z nich będzie właściwością skalarną (relacja 0..1), drugą kolekcją (relacja 1..*). W ten sposób modelowane mogą być dane reprezentowane np. jako drzewa.

Encja taka może mieć jedną nadkategorię (lub nie mieć nadkategorii - relacja 0..1) oraz wiele podkategorii.

Jedna encja - wiele tabel

Mamy dwie tabele dzielące ten sam klucz główny. Z punktu widzenia programu możemy połączyć zatem dwie encje w jedną.



Po wybraniu obu tabel w okienku Entity Data Model, dostaniemy dwie encje w designerze. Aby je połączyć, należy:
  • Przekopiować właściwości z encji ProductWebInfo do Product
  • Usunąć ProductWebInfo, wybierając opcję No przy pytaniu o usuwanie tabel z store model
  • W widoku Mapping Details w VS dla encji Product należy dodać tabelę ProductWebInfo
  • Dla tabeli ProductWebInfo ustawić mapowania (SKU na property SKU, ImageURL na property ImageURL)
Podczas zapisywania obiektu encji Product do bazy danych zostaną dodane wpisy do obu tabel.

Jedna tabela - wiele encji

Sytuacja taka może być przydatna, gdy mamy w tabeli pewne pola dużych rozmiarów, z których rzadko korzystamy. Aby nie ładować ich nie potrzebnie za każdym razem możemy dodać tabelę do EDM, a następnie odpowiednio zmapować ją do dwóch encji:
  • Dodać nową encję do designera, zmieniając w polu klucza wartość na nazwę kolumny klucza głównego  z encji głównej
  • Przenieść rzadko używane właściwości do nowej encji
  • W Mapping Window na nowej encji dodać tabelę i ustawić mapowanie właściwości na odpowiednie kolumny
  • Dodać asocjację (PPM na jednej z encji) między dwoma encjami, ustawioną na 1 do 1, odznaczając dodawanie nowego klucza obcego
  • Ostatecznie klikając dwukrotnie na połączeniu należy dodać ograniczenia (Constraint) w Referential Constraint ustawiając Principal na Photograph, a właściwości klucza na PhotoId
Po poprawnej konfiguracji otrzymujemy Lazy Loading dla pól w dodatkowej encji. Warto pamiętać, że usuwając encję główną, usuwane także encję dodatkową, oraz, że aby dodać encję dodatkową do zbioru, musi istnieć odpowiadająca jej encja główna.

Modelowanie dziedziczenia "tabela per typ"

Dysponujemy jedną tabelą główną, oraz dwoma powiązanymi z nią relacjami kluczy obcych. Dwie dodatkowe tabele stanowią uzupełnienie informacji o obiekcie z tabeli głównej.



Z punktu widzenia aplikacji pisanej w języku obiektowym, dwie tabele dziedziczą z tabeli głównej. Aby zamodelować dziedziczenie w EF, należy po dodaniu trzech tabel do Entity Data Model:
  • usunąć wszystkie asocjacje
  • na encji odpowiadającej tabeli głównej wybrać Add...Inheritance. W dialog boxie wybrać tabelę bazową jako encję bazową oraz jedną z tabel uzupełniających, jako encję dziedziczącą
  • czynność powtórzyć dla drugiej tabeli
  • w oknie mapowania usunąć property odpowiadające kolumnie z kluczem obcym z encji dziedziczących i dodać je, jako mapowane do kolumny z tabeli głównej
W efekcie uzyskamy hierarchę obiektów jak poniżej:


Filtrowanie kolekcji obiektów

Podczas mapowania tabeli na encję możemy założyć filtr odrzucający pewne rekordy. Przykładowo możemy odrzucać wszystkie wpisy, gdzie pola z pewnej kolumny są różne od NULL. Po dodaniu tabeli do EDM, należy przejść do widoku Mapping Details, a następnie kliknąć w opcję <Add a Condition> . W dodanym warunku wybieramy kolumnę, rodzaj warunku (is lub =) i wartość (w tym przypadku is Null. Nie będziemy potrzebowali więcej property, które służy za warunek, zatem można je usunąć z listy właściwości na encji. Pozostałe możliwości warunków
  • <column> Not Null
  • <integer> = <value>
  • <String> = <value>

Modelowanie dziedziczenia "tabela per hierarchia"

Mamy tabelę, zawierającą jedną kolumnę, która determinuje typ (rodzaj) obiektu. Chcemy tak zmapować ją do encji, aby uzyskać jeden bazowy typ abstrakcyjny i dwa dziedziczące z niego. Po dodaniu naszej tabeli do Entity Data Model należy:
  • Dodać dwie dodatkowe encje w designerze, które będą reprezentowały typy dziedziczące. Podczas dodawania należy ustalić typ bazowy na encję mapowaną bezpośrednio do tabeli
  • Przenieść properties charakterystyczne dla danego typu z encji bazowych do odpowiednich encji dziedziczących
  • W widoku Mapping Details wybrać dla obu typów dziedziczących tabelę, do której będą mapowane, oraz sprawdzić poprawność mapowania properies nowej encji do odpowiednich kolumn tabeli
  • W tym samym widoku w polu Add a Condition należy dodać warunek na podstawie kolumny z tabeli głównej, np. 


W opcjach encji bazowej można ustawić pole abstract na true, oraz usunąć property decydujące o typie encji.



Modelowanie relacji typu "Is-a" oraz "Has-a"

Może się zdarzyć taka sytuacja że dwie tabele są w relacji przedstawionej poniżej:


Obie tabele posiadają ten sam klucz główny, a dodatkowo jedna posiada asocjację z drugą poprzez klucz obcy.

Aby uprościć dostęp do danych po stronie programu, możemy dodać je do EDM, a następnie:
  • usunąć asocjację 1...0.1
  • na tabeli bez klucza obcego dodać Inheritance i jako encję dziedziczącą dodać tą odpowiadającą drugiej tabeli
  • z encji dziedziczącej usunąć property odpowiadające kluczowi głównemu, a w szczegółach mapowania ustawić mapowanie tej kolumny (ParkId) na Id encji bazowej (LocationId)
  • zmienić nazwę Navigation Property reprezentującego klucz obcy
Uproszone encje wyglądają następująco:


Typy złożone

Jedna tabele reprezentuje kilka logicznych obiektów, np osobę i jej adres jednocześnie.

Po dodaniu do EDM tabeli, należy:
  • zaznaczyć properties z danej grupy i kliknąć opcję Refactor into New Complex Type dostępną pod prawym przyciskiem myszy
  • w widoku Model Browser możemy zmienić typ np. na Name oraz w Designerze możemy zmienić nazwę property
  • Complex property można także dodawać poprzez kliknięcie prawym przyciskiem myszy na encji i wybranie Add...ComplexProperty. 
  • W oknie szczegółów mapowania wszystkie properties powinny być mapowane na propercje typów złóżonych


czwartek, 29 listopada 2012

[SQL|ORM] Entity Framework : Entity Data Model - podstawy

Jednym z najważniejszych narzędzi w Entity Framework jest ADO .NET Entity Data Model.

Podstawowym jego zadaniem jest enkapsulacja logiki dostępu do danych, ale to nie wszystko. Przy użyciu EDM możemy z jednej strony tworzyć własne, relacyjne tabele, a z drugiej strony mapować już stworzone tabele na obiekty. Zatem odpowiednie korzystanie z niego spowoduje przełożenie relacji bazy danych na relacje obiektów, które dowolna aplikacja może w łatwy sposób modyfikować. Dzięki takiemu podejściu programiści nie muszą skupiać się na sposobie dostępu do danych, a mogą skoncentrować się na zarządzaniu samymi danymi.

Tworzenie własnych, prostych modeli

Aby stworzyć własny model, należy po dodaniu EDM do projektu wybrać opcję "Empty Model". Zostajemy przeniesieni do okna projektowania, gdzie możemy za pomocą menu dostępnego pod prawym klawiszem myszy dodać własne encje danych. W ten sam sposób możemy modyfikować właściwości, które zostaną zmapowane na odpowiednie kolumny bazy danych. Na property z kluczem głównym można także ustawić StoreGeneratedPattern na Identity, co spowoduje mapowanie na kolumnę z automatycznie generowanymi wartościami.

Po wybraniu opcji Generate Database from Script...( z menu dostępnego pod prawym przyciskiem myszy na powierzchni z encjami), możemy podać kilka opcji związanych z samym połączeniem, a następnie otrzymamy gotowy skrypt generujący odpowiednie tabele.


Tworzenie modelu z istniejącej bazy danych

Po dodaniu do projektu EDM, wybieramy opcję tworzenia modelu z istniejących tabel. Po podaniu connection string i wybraniu odpowiedniej bazy danych, możemy wybrać, które tabele, widoki i procedury chcemy zaimportować jako modele. Warto zaznaczyć opcję pluralizing, która spowoduje, że kontenery na nasze obiekty będą miały nazwy będące liczbami mnogimi od nazw tabel. Poza mapowaniem tabel i kolumn na obiekty i propercje, przeniesione zostaną również relacje między tabelami na relacje między obiektami.

Po poprawnym mapowaniu, można dodawać nowe dane do tabel poprzez encje w następujący sposób. 

using (var context = new EntityFrameworkRecipesEntities1())
{
    Poet p = new Poet()
                 {
                     FirstName = "Wiliam",
                     LastName = "Shakespeare",
                 };
    p.PoetId = 1;
    Poem poem = new Poem()
                    {
                        Title = "Hamlet"
                    };
    poem.Poet = p;
    poem.PoemId = 1;
    context.Poems.AddObject(poem);
    context.SaveChanges();
}

Co spowoduje dodanie po jednym rekordzie do obu tabel, połączonych kluczem obcym

czwartek, 15 listopada 2012

.NET Framework

Wszystko na temat Visual Studio, języka C# i technologii opartych na platformie .NET Framework: