Pokazywanie postów oznaczonych etykietą 70-516. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą 70-516. Pokaż wszystkie posty

niedziela, 10 lutego 2013

70-516 LINQ

O LINQu pisałem w wielu postach. Nie zamierzam więc rozpisywać się o nim, tylko zbiorę najważniejsze fakty i przykłady.

LINQ można użyć dla każdej kolekcji która implementuje IEnumerable lub generyczny IEnumerable<T>.

Zapytania LINQ można pisać w wersji SQLo podobnej. Różnicą jest to, iż rozpoczynamy od klauzuli FROM. Dalczego? Jeżeli nie zaczynalibyśmy od FROM, czyli wskazania źródła do którego się odwołujemy nie mielibyśmy wsparcia IntelliSense.

Opóźnione wykonanie zapytania - definicja LINQ jest traktowana jako zapytanie, które zostanie wykonane dopiero kiedy wynik będzie faktycznie potrzebny. Właściwość ta ma tę konsekwencję, że każde wywołanie zapytania będzie się odwoływało do źródła danych. Jeżeli chcemy przechować dane lokalnie i nie odwoływać się do źródła wielokrotnie, należy dane umieścić w kolekcji (np. Liście)


Cechy które składają się na LINQ:
  • inicjalizacja obiektów
  • niejawne typowanie - var; tylko jako zmienne lokalne; w deklaracji musi nastąpić ich inicjalizacja - nie może być to null;
  • typy anonimowe
  • wyrażenia lambda
  • metody rozszerzające - extension methods
Metody rozszerzające zawarte w interfejsie Enumerable:
  • All - sprawdza elementy kolekcji i zwraca prawdę jeżeli wszystkie elementy spełniają podane warunki
  • Any - sprawdza elementy kolekcji i zwraca prawdę jeżeli przynajmniej jeden element spełnia podane warunki
  • AsEnumerable - konwertuje dane do IEnumerable (np. z IQueryable)
  • AsQueryable - konwertuje do interfejsu IQueryble (np. z IEnumerable)
  • Average - średnia elementów
  • Cast - rzutuje elementy kolekcji na podany typ
  • Concat - pozwala łączyć ze sobą kolekcje, przykład:
    Code:
        class Program
        {
            static void Main(string[] args)
            {
                var personRepository = new PersonRepository();
                var collection1 = personRepository.GetPersonList1();
                var collection2 = personRepository.GetPersonList2();
                var combindedCollection = collection1.Concat(collection2).Concat(collection1).ToList();
            }
        }
    
        class Person
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }
    
        class PersonRepository
        {
            public IList<Person> GetPersonList1()
            {
                return new List<Person>
                           {
                               new Person {FirstName = "Jacek", LastName = "Kowalski"},
                               new Person {FirstName = "Marek", LastName = "Jackowski"}
                           };
            }
    
            public IList<Person> GetPersonList2()
            {
                return new List<Person>
                           {
                               new Person {FirstName = "Sebastian", LastName = "Kowalski"},
                               new Person {FirstName = "Seweryn", LastName = "Opolski"}
                           };
            }


  • Contains - sprawdza czy podany element znajduje się w kolekcji
  • Count - ilość elementów w kolekcji
  • DefaultIfEmpty - w przypadku gdy kolekcja nie zawiera żadnych elementów, zwraca jeden o domyślnej wartości (false, 0, null)
  • Distinct - usuwa duplikaty ze źródła
  • ElementAt(i) - zwraca element na danej pozycji w źródle
  • ElementAtOrDefault - jak wyżej z tą różnicą, że w przypadku nie znalezienia elementu nie zostanie rzucony wyjątek tylko zwrócona wartość domyślna
  • Except - zwraca elementy zawarte w pierwszej kolekcji a nieobecne w drugiej
  • First oraz FirstOrDefault - zwraca pierwszy element kolekcji
  • GroupBy - grupuje elementy według zadanego klucza, przykład:
    Code:
                var adventureWorksLtContext = new AdventureWorksLTContext();
                var customers = adventureWorksLtContext.Customers.ToList();
                IEnumerable<IGrouping<string, Customer>> customersGroupByFirstName = customers.GroupBy(x => x.FirstName);
                foreach (var group in customersGroupByFirstName)
                {
                    foreach (var customer in group)
                    {
                        Console.WriteLine("{0} {1}", customer.FirstName, customer.LastName);
                    }
                }


  • GroupJoin - zapytanie podobne do SQLowego Left Outer Join
  • Intersect - przecięcie zbiorów
  • Join - łączy dwa zbiory na zasadzie równości kluczy
  • Last oraz LastOrDefault - zwraca ostatni element kolekcji
  • LongCount - to samo co Count, tylko zwraca rezultat jako typ long
  • Max - zwraca największy element kolekcji
  • OfType<T> - zwraca elementy, które są typu T
  • Min - najmniejszy element kolekcji
  • OrderBy, OrderByDescending, ThenBy, ThenByDescending - sortowanie
  • Reverse - odwraca kolejność elementów w kolekcji
  • Select - projekcja rezultatu
  • SelectMany - jeden element wejściowy tworzy wiele wyjściowych. Szczególnie przydatny w przypadku pracy nad kolekcjami kolekcji - łączy wtedy kolekcje z obiektów w jedną wynikową
  • SequenceEqual - sprawdza czy obie kolekcje zawierają te same elementy, w tej samej kolejności
  • Single oraz SingleOrDefault - zwraca jeden element z kolekcji - jeżeli jest więcej elementów w kolekcji zostanie rzucony wyjątek, bądź zostanie zwrócona wartość domyślna
  • Skip - przeskakuje zadaną ilość elementów
  • SkipWhile - przeskakuje zadaną ilość elementów dopóki dany warunek jest spełniony
  • Sum - zwraca sumę 
  • Take - pobiera zadaną ilość elementów z kolekcji 
  • TakeWhile - to samo co Take, ale pobiera elementy spełniające warunek 
  • ToArray - konwertuje kolekcję na tablicę
  • ToDictionary - konwertuje kolekcję na słownik
  • ToList - konwertuje kolekcję do listy
  • ToLookUp - tworzy kolekcję grup
  • Union - łączy kolekcje, usuwa elementy powtarzające się
  • Where - filtruje elementy
  • Zip - łączy dwie kolekcje na podstawie zadanego warunku; liczba elementów jest równa mniejszej kolekcji

Zastosowania LINQ:
1. Stronicowanie:
Łączymy metody Skip i Take:
kolekcja.Skip(nr_strony * ilość_elementów_na_stronie).Take(ilość_elementów_na_stronie)

2. Złączenie równościowe - Inner Join

Code:
from a in collectionA
join b in collectionB
on a.Id equals b.IdA

3. Złączenia zewnętrzne - Outer Join

Code:
from a in collectionA
join b in collectionB
on a.Id equals b.IdA into c
from d in c.DefaultIfEmpty()

3. Iloczyn kartezjański - Cross Join

Code:
from a in collectionA
from b in collectionB


PLINQ - Parallel LINQ
PLINQ pozwala na użycie dodatkowych wątków w celu szybszego wykonania zadanego zadania. Aby uaktywnić wielowątkowe wykonywanie zapytania należy skorzystać z metody rozszerzającej AsParallel. Wykonane zachowanie nie zachowa kolejności wyników. Jeżeli chcemy aby kolejność została zachowana należy skorzystać z metody AsOrdered.

czwartek, 7 lutego 2013

Wrażenia po egzaminie 70-516

Egzamin już za mną więc przyszedł czas na krótkie podsumowanie.
70-516 dotyczy dostępu do źródeł danych. Głównym elementem, na który został położony największy nacisk w pytaniach to Entity Framework w wersji Database-First. Obecnie wydaje mi się osobiście, że to rozwiązanie Code First przeważa jeżeli chodzi o nowe projekty - to jednak tylko moja prywatna opinia na ten temat.
Pierwszych pięć pytań na egzaminie dotyczyło realnego schematu bazy, gdzie zostały podane dane skryptów, mapowań plików EF, kodu procedur SQL itp.
Kolejne pytania dotyczyły już ogólnie wiedzy na temat dostępu do danych.
Ogólnie poziom pytań był bardzo zróżnicowany - od bardzo prostych, do takich w których trzeba było się zastanowić głębiej nad odpowiedzią. 

Kończąc wywód - dwa MCTS-y w moich rękach. Czas na kolejny :)

poniedziałek, 4 lutego 2013

70-516 Szyfrowanie danych

1. Szyfrowanie symetryczne
 

Szyfrowanie symetryczne opiera się na jednym kluczu użytym zarówno do szyfrowania jak i deszyfrowania danych. Obecnie zalecane jest używanie algorytmu Rijndael - dokładny opis można znaleźć w wikipedii: http://en.wikipedia.org/wiki/Advanced_Encryption_Standard


Code:
    class Program
    {
        static void Main(string[] args)
        {
            using (var rm = new RijndaelManaged())
            {
                byte[] encrypted = EncryptRijndaelManaged("Ala ma kota", rm.Key, rm.IV);
                string decrypted = DecryptRijndaelManaged(encrypted, rm.Key, rm.IV);
                Console.WriteLine(decrypted);
            }
        }

        public static byte[] EncryptRijndaelManaged(string input, byte[] key, byte[] IV)
        {
            byte[] encrypted;
            using (var rm = new RijndaelManaged())
            {
                rm.Key = key;
                rm.IV = IV;
                ICryptoTransform encryptor = rm.CreateEncryptor(rm.Key, rm.IV);
                using (var ms = new MemoryStream())
                {
                    using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                    {
                        using (var swEncrypt = new StreamWriter(cs))
                        {
                            swEncrypt.Write(input);
                        }
                        encrypted = ms.ToArray();
                    }
                }
            }

            return encrypted;
        }

        public static string DecryptRijndaelManaged(byte[] input, byte[] key, byte[] IV)
        {
            string decrypted;
            using (var rm = new RijndaelManaged())
            {
                rm.Key = key;
                rm.IV = IV;
                ICryptoTransform decryptor = rm.CreateDecryptor(rm.Key, rm.IV);
                using (var ms = new MemoryStream(input))
                {
                    using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                    {
                        using (var swDecrypt = new StreamReader(cs))
                        {
                            decrypted = swDecrypt.ReadToEnd();
                        }
                    }
                }
            }

            return decrypted;
        }
    }



2. Szyfrowanie asymetryczne


Jest to dużo bardziej bezpieczne rozwiązanie. Tworzone są dwa klucze:

  • prywatny - trzymany na serwerze w bezpiecznym i niedostępnym miejscy
  • publiczny - wysyłany do zainteresowanych
Szyfrowanie odbywa się za pomocą jednego z tych kluczy, a odszyfrowanie za pomocą drugiego. Jednym z najpopularniejszych algorytmów asymetrycznych jest RSA. Przykład szyfrowania i deszyfrowania:

Code:
        public static byte[] EncryptRsa(string input)
        {
            const int rsa = 1;
            var cspParameters = new CspParameters(rsa);
            cspParameters.Flags = CspProviderFlags.UseMachineKeyStore;
            cspParameters.KeyContainerName = "My Keys";
            var rsaCryptoServiceProvider = new RSACryptoServiceProvider(cspParameters);
            byte[] encrypted = rsaCryptoServiceProvider.Encrypt(new UnicodeEncoding().GetBytes(input), true);

            return encrypted;
        }

        public static string DecryptRsa(byte[] input)
        {
            const int rsa = 1;
            var cspParameters = new CspParameters(rsa);
            cspParameters.Flags = CspProviderFlags.UseMachineKeyStore;
            cspParameters.KeyContainerName = "My Keys";
            var rsaCryptoServiceProvider = new RSACryptoServiceProvider(cspParameters);
            var decrypted = rsaCryptoServiceProvider.Decrypt(input, true);

            return new UnicodeEncoding().GetString(decrypted);
        }



3. Hashowanie
Hashowanie jest procesem jednokierunkowym, to znaczy hashujemy podany ciąg znaków zmieniając je w kod, jednak w drugą stronę nie jesteśmy już w stanie (w prosty sposób) odczytać stworzonego hasha.
Istnieje jednak ryzyko, że hashując np. hasło w bazie pojawią się dwa podobne wpisy. Rozwiązaniem tego problemu jest dodanie tzw. soli (Salt) którą dodaje się np. do hasła i taki ciąg znaków dopiero zostaje hashowany. Dzięki takiemu rozwiązaniu mamy znikome prawdopodobieństwo powtórzenia się hashy.
Obecnie zalecane jest stosowane algorytmu SHA256. Przykład użycia algorytmu:

Code:
        public static string Sha256Encryption(string input, string salt)
        {
            byte[] data = new UnicodeEncoding().GetBytes(input + salt);
            var sha256Managed = new SHA256Managed();
            var computeHash = sha256Managed.ComputeHash(data);

            return ToHex(computeHash);
        }

        private static string ToHex(byte[] computeHash)
        {
            var hex = new StringBuilder();
            foreach (var b in computeHash)
            {
                hex.AppendFormat("{0:x2}", b);
            }

            return hex.ToString();
        }



4. Cyfrowy podpis
Cyfrowy podpis gwarantuje, że wiadomość została niezmieniona i wysłana przez daną instytucję.
Przykład implementacji:

Code:
        public static byte[] CreateSignature(string input)
        {
            const int rsa = 24;
            var cspParameters = new CspParameters(24);
            cspParameters.Flags = CspProviderFlags.UseMachineKeyStore;
            cspParameters.KeyContainerName = "My Keys";
            var rsaCryptoServiceProvider = new RSACryptoServiceProvider(cspParameters);

            return rsaCryptoServiceProvider.SignData(new UnicodeEncoding().GetBytes(input), "SHA256");
        }

        public static bool CheckSign(string input, byte[] signature)
        {
            const int rsa = 24;
            var cspParameters = new CspParameters();
            cspParameters.Flags = CspProviderFlags.UseMachineKeyStore;
            cspParameters.KeyContainerName = "My Keys";
            var rsaCryptoServiceProvider = new RSACryptoServiceProvider(cspParameters);

            return rsaCryptoServiceProvider.VerifyData(new UnicodeEncoding().GetBytes(input), "SHA256", signature);
        }


LINQ - użycie GroupJoin


Zadanie jest następujące:
Na podstawie poniższego schematu (baza AdventureWorksLT) wyznaczyć wartość zamówienia. Dodać kolumny z id klienta i zamówienia (bez uwzględnienia podatku, obniżek itp).





W zapytaniu wykorzystamy GroupJoin.

Zapytanie to zapiszemy w dwóch postaciach LINQ2SQ: metodowej oraz SQLo podobnej:

Code:
            var res = context.SalesOrderHeaders.
                              GroupJoin(context.SalesOrderDetails, header => header.SalesOrderID,
                                        detail => detail.SalesOrderID,
                                        (header, details) => new
                                                                 {
                                                                     CustomerId = header.CustomerID,
                                                                     OrderId = header.SalesOrderID,
                                                                     TotalPrice = details.Sum(x => x.UnitPrice * x.OrderQty)
                                                                 });


  Code:
             var res2 = from a in context.SalesOrderDetails
                       group a by a.SalesOrderID
                       into g
                       join b in context.SalesOrderHeaders on g.Key equals b.SalesOrderID
                       select new
                                  {
                                      CustomerId = b.CustomerID,
                                      OrderId = b.SalesOrderID,
                                      TotalPrice = g.Sum(x => x.UnitPrice*x.OrderQty)
                                  };





Obydwa zapytania zwracają te same rezultaty i są poprawne pod względem składni LINQ. Ciekawszym aspektem może być zapytanie SQL generowane przez te dwa zapytania. Dla pierwszego zapytania zostanie wygenerowany następujący kod SQL:
SELECT [Extent1].[SalesOrderID] AS [SalesOrderID], [Extent1].[CustomerID] AS [CustomerID],
(
SELECT
  
SUM([Filter1].[A1]) AS [A1]
  
FROM ( SELECT
      
[Extent2].[UnitPrice] *  CAST( [Extent2].[OrderQty] AS DECIMAL(19,0)) AS [A1]
      
FROM [SalesLT].[SalesOrderDetail] AS [Extent2]
      
WHERE [Extent1].[SalesOrderID] = [Extent2].[SalesOrderID]
  
)  AS [Filter1]) AS [C1]FROM [SalesLT].[SalesOrderHeader] AS [Extent1]



Drugie z zapytań wygenerowało następującego SQLa:


SELECT [Distinct1].[SalesOrderID] AS [SalesOrderID], [Extent2].[CustomerID] AS [CustomerID], [Extent2].[SalesOrderID] AS [SalesOrderID1],
(
SELECT
  
SUM([Filter1].[A1]) AS [A1]
  
FROM ( SELECT
      
[Extent3].[UnitPrice] *  CAST( [Extent3].[OrderQty] AS DECIMAL(19,0)) AS [A1]
      
FROM [SalesLT].[SalesOrderDetail] AS [Extent3]
      
WHERE [Distinct1].[SalesOrderID] = [Extent3].[SalesOrderID]
  
)  AS [Filter1]) AS [C1]FROM   (SELECT DISTINCT
  
[Extent1].[SalesOrderID] AS [SalesOrderID]
  
FROM [SalesLT].[SalesOrderDetail] AS [Extent1] ) AS [Distinct1]INNER JOIN [SalesLT].[SalesOrderHeader] AS [Extent2] ON [Distinct1].[SalesOrderID] = [Extent2].[SalesOrderID]


Rozwiązania podobne w składni a wynik zapytania SQL różny dla każdego z dwóch rozwiązań. Jeżeli weźmiemy pod uwagę wydajność to w przypadku pierwszego zapytania będzie ona większa. Jeżeli przyszłoby mi napisać samemu to zapytanie skorzystałbym z następującego zapytania:

SELECT header.SalesOrderID, header.CustomerID, details.TotalPriceFROM SalesLT.SalesOrderHeader header JOIN
  
(SELECT SUM(details.UnitPrice * details.OrderQty) AS TotalPrice, details.SalesOrderID
  
FROM SalesLT.SalesOrderDetail details
  
GROUP BY details.SalesOrderID) AS details ON header.SalesOrderID = details.SalesOrderID

środa, 30 stycznia 2013

70-516 Monitorowanie wydajności LINQu, Entity Framework

Używając LINQ2SQL oraz Entity Framework możemy na bieżąco monitorować wysyłane zapytania do bazy danych.
Aby logować zapytania wysyłane przez LINQ2SQL należy na obiekcie DataContext ustawić właściwość Log. Przypisać do niej możemy dowolny obiekt klasy dziedziczącej po TextWriter.
W książce do egzaminu podano bardzo dobry przykład stworzenia klasy dziedziczącej po TextWriter która wypisuje zapytania LINQ do okienka Output. Zaletą klasy jest możliwość włączania logowania za pomocą konfiguracji w App/Web .config Zrealizowane jest to za pomocą klasy BooleanSwitch
Przykład który znajduje się w książce:

Code:
    public class LinqToSqlTraceWriter : TextWriter
    {
        private BooleanSwitch logSwitch;
        public LinqToSqlTraceWriter(string switchName)
        {
            logSwitch = new BooleanSwitch(switchName, switchName);
        }

        public override Encoding Encoding
        {
            get { throw new SystemNotImplementedException(); }
        }

        public override void Write(char value)
        {
            if (logSwitch.Enabled)
            {
                Trace.Write(value);
            }
        }

        public override void Flush()
        {
            Trace.Flush();
        }

        public override void Close()
        {
            Trace.Close();
        }
    }

Przełącznik konfigurujemy w app/web configu:

Code:
  <system.diagnostics>
    <switches>
      <add name="LinqToSqlSwitch" value="1"/>
    </switches>
  </system.diagnostics>

Aby włączyć logowanie należy obiektowi DataContext.Log przypisać obiekt klasy LingToSqlTextWriter.

W przypadku Entity Framework treść zapytania wysyłanego do bazy danych otrzymujemy wywołując na obiekcie klasy ObjectQuery<T> metodę ToTraceString. Sposób w jaki to zrobić znaleźć można w innym moim wpisie: http://patryknet.blogspot.com/search?q=ToTraceString


Innym sposobem śledzenia wydajności jest zaimplementowanie Monitora wydajności:

Code:
            string catName = "LINQ";
            if (!PerformanceCounterCategory.Exists(catName))
            {
                PerformanceCounterCategory.Create(catName, "Category help",
                                                  PerformanceCounterCategoryType.SingleInstance, "LINQ2SQL", "Pomoc");
            }
            var performanceCounter = new System.DiagnosticsPerformanceCounter(catName, "LINQ2SQL", false);
            performanceCounter.IncrementBy(100);

wtorek, 29 stycznia 2013

70-516 Tworzenie WCF Data Services - OData

OData to protokół stworzony przez Microsoft, służący przesyłaniu danych za pomocą protokołu HTTP.
Obsługuje następujące metody:

  • GET - selekcja danych (SELECT)
  • POST - wysyłanie danych (INSERT)
  • PUT - uaktualnianie (UPDATE)
  • DELETE - usuwanei danych
Ważnym aspektem jest udzielanie uprawnień dostępu do danych. Możemy przydzielić prawa zarówno na kolekcje zwracane przez serwis jak i operacje, które udostępnia.
Definicja typu wyliczeniowego odpowiadającego za przydział uprawnień:


Code:
  [Flags]
  public enum EntitySetRights
  {
    None = 0, //brak uprawnień do jakichkolwiek operacji
    ReadSingle = 1, //możliwość pobrania tylko jednego elementu z kolekcji
    ReadMultiple = 2, //Możliwość pobrania całej kolekcji
    AllRead = ReadMultiple | ReadSingle, //Odczyt zarówno pojedynczych rekordów jak i całych kolekcji
    WriteAppend = 4,//pozwolenie na dodawanie
    WriteReplace = 8,//pozwolenie na zmianę
    WriteDelete = 16,//pozwolenie na usuwanie
    WriteMerge = 32,//pozwala łączyć dane
    AllWrite = WriteMerge | WriteDelete | WriteReplace | WriteAppend,//wszystkie możliwości manipulacji danymi
    All = AllWrite | AllRead,//wszystkie operacje dozwolone
  }

Aby stworzyć prosty serwis OData postępujemy według instrukcji:
1. Tworzymy nowy projekt ASP.NET
2. Mapujemy za pomocą ORMa np. Entity Framework bazę danych/plik XML który chcemy udostępnić
3. Dodajemy do projektu nowy element typu WCF Data Service:


4. W moim przypadku użyłem bazy danych AdventureWorks. Za pomocą narzędzia Entity Framework Power Tools wygenerowałem model bazy danych i uzyskałem kontekst AdventureWorksLTContext
5. Dodany serwis WCF ma następującą definicję:


Code:
//------------------------------------------------------------------------------
// <copyright file="WebDataService.svc.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Data.Services;
using System.Data.Services.Common;
using System.Linq;
using System.ServiceModel.Web;
using System.Web;

namespace ODataService
{
    public class AdventureOData : DataService< T >
    {
        // This method is called only once to initialize service-wide policies.
        public static void InitializeService(DataServiceConfiguration config)
        {
            // TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
            // Examples:
            // config.SetEntitySetAccessRule("MyEntityset", EntitySetRights.AllRead);
            // config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
        }
    }
}

Aby udostępnić stworzone wersje do odczytu wystarczy w stworzonej klasie zmienić T na typ kontekstu oraz dodać uprawnienia:


Code:
    public class AdventureOData : DataService<AdventureWorksLTContext>
    {
        // This method is called only once to initialize service-wide policies.
        public static void InitializeService(DataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
            // config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
        }
    }

Dodając uprawnienia wprowadziłem gwiazdkę co oznacza iż ustawienie dotyczy wszystkich encji.
Teraz otwieramy przeglądarkę i zmieniamy ustawienie - dla przykładu posłużyłem się przeglądarką IE:


Należy odznaczyć zaznaczoną opcję. Po tej operacji możemy uruchomić naszą aplikację i w pasku przeglądarki wprowadzić adres:



Przykłady zapytań:
Wyciągnięcie klienta o id (klucz główny) = 10 realizujemy zapytaniem: /AdventureOData.svc/Customers(10)
Jeżeli klucz jest łańcuchem znakowym (string) wartość zawieramy w apostrofach, np. Encja('Ala')
Aby pobrać zamówienia klienta o Id 29736 należy użyć właściwości która, prowadzi do encji zamówień:
AdventureOData.svc/Customers(29736)/SalesOrderHeaders
Aby wyświetlić zawartość kolumny imię stosujemy zapytanie: AdventureOData.svc/Customers(29736)/FirstName

Dodatkowe przełączniki wprowadzamy według zapytania: AdventureOData.svc/Customers?$przelacznik
Typy przełączników:

  • $count - zlicza ilość elementów
  • $select=kolumna1,kolumna2 - wyświetla wybrane kolumny
  • $orderby = kolumna1 [desc], kolumna2 [desc] - sortowanie
  • $top=x - wyświetla x rekordów
  • $skip=10&$top=10 - stronicowanie, w takim przypadku warto posortować rekordy, aby dostawać na każdej stronie unikalne wartości
  • $filter - filtrowanie wyniku
  • $expand - wczesne ładowanie (eager loading) - po przecinkach wymieniamy właściwości odpowiadające za nawigację. Jeżeli wymieniona właściwość posiada kolejne referencje do innych encji wymieniamy je po ukośniku "/"
  • $metadata / ?wsdl - schema serwisu
Aby łączyć przełączniki ze sobą kolejne dodajemy po znaku ampersanda - &

Wartości, które można przekazać do filtra:
  • eq - równy
  • ne - nie równy
  • gt - większy od
  • ge - większy bądź równy
  • lt - mniejszy od
  • le - mniejszy lub równy
  • and - operator logiczny "i"
  • or - operator logiczny "lub"
  • Not - zaprzeczenie 
  • Add - matematyczne dodawanie
  • Sub - matematyczne odejmowanie
  • mul - matematyczne mnożenie
  • div - matematyczne dzielenie
  • mod - reszta z dzielenia
  • () - grupowanie wyrażeń
Przykład filtra: /AdventureOData.svc/Customers?$filter=CustomerID gt 10 and CustomerID lt 20

Standard OData oferuje także szereg funkcji umożliwiających na pracę z łańcuchami tekstowymi, datami, czasem, funkcjami matematycznymi oraz typami encji. Spis tych funkcji można znaleźć na stronie http://www.odata.org/documentation/uri-conventions

poniedziałek, 21 stycznia 2013

70-516 Transakcje

Transakcja to zbiór poleceń, które powinny być wykonane jako pojedyncza operacja, lub w przypadku błędu - wycofane w całości. Transakcja posiada cztery cechy tzw. ACID
  • atomowość (Atomicity) - wszystkie instrukcje muszą być wykonane jako jedna całość, lub też wszystko musi zostać cofnięte do stanu przed wykonywania transakcji 
  • spójność (Consistency) - po wykonaniu transakcji wszystkie więzy integralności muszą zostać zachowane
  • izolacja (Isolation) - w przypadku wykonywania więcej niż jednej transakcji na danych w tym samym czasie, nie powinny one o tym wiedzieć (uzależnione jest to także od poziomu izolacji)
  • trwałość (Durability) - zatwierdzone transakcje nie mogą spowodować uszkodzenia danych w przypadku nagłego braku zasilania.
W przypadku jednoczesnego dostępu do danych, mamy do czynienia z przypadkiem optymistycznym i pesymistycznym dostępu:
  • optymistyczne algorytmy - zakładają brak zakładania blokad podczas dostępu do danych
  • pesymistyczne algorytmy - nakładają blokady podczas dostępu do danych
W wielu aplikacjach kompletna izolacja danych nie ma sensu. Z jednej strony zapewnia ona maksymalny poziom bezpieczeństwa, z drugiej powoduje nie potrzebne przestoje. Jeżeli zmniejszymy jej poziom może dojść do anomalii. Rodzaje anomalii:
  • dirty read - brudne czytanie - transakcja odczytała dane zmienione przez inną nie zatwierdzoną jeszcze transakcję. Problem może się objawić w przypadku, gdy ta nie zatwierdzona transakcja zostanie odwołana. 
  • nonrepeatable read - występuje gdy jedna transakcja odczytuje te same dane dwukrotnie, a pomiędzy tymi odczytaniami inna transakcja modyfikuje te dane.
  • phantom read - podczas wielokrotnych odczytów danych przez jedną transakcję, inne dodają lub usuwają wiersze. W wyniku tego mamy do czynienia z większą bądź mniejszą ilością zwracanych danych po każdym odczycie.
Poziomy izolacji w MS SQL



Opis poszczególnych poziomów:
  • read uncomitted - brak izolacji
  • read committed with locks - poziom domyślny dla MS SQL - nie pozwala odczytywać danych nie zatwierdzonych przez transakcję
  • read committed with snapshots - tylko zatwierdzone zmiany są widoczne w transakcji. Trzymana jest wersja wiersza w pamięci i zostanie użyta tymczasowa baza danych. Zaletą jest brak blokad.
  • repetable read - tryb najlepszy w przypadku gdy w jednej transakcji chcemy odczytać i zapisać dane. Inne transakcje muszą zaczekać do momentu gdy zakończy działanie transakcja z tym poziomem izolacji
  • snapshot - tworzy kopię danych na których będzie pracować transakcja. Istnieje jednak duże prawdopodobieństwo, że podczas zatwierdzania transakcji dojdzie do konfliktu z powodu działania na tych samych danych przez inne transakcje
  • serializable - zakłada blokadę na wierszach; nie można dodawać, usuwać ani modyfikować zablokowanych danych aż do momentu zaakceptowania transakcji. Najbardziej restrykcyjny poziom, należy go używać tylko wtedy gdy jest to koniecznie wymagane

Tworzenie transakcji
W MS SQL tworzymy transakcję w następujący sposób:

SET XACT_ABORT ON --powoduje przechwytywanie wszystkich bledów powyzej poziomu 21, czyli fatalnychBEGIN TRY
  
BEGIN TRANSACTION  --rozpoczecie transakcji
   --operacje na danych
  
COMMIT TRANSACTION --zatwierdzenie transakcjiEND TRYBEGIN CATCH
  
ROLLBACK TRANSACTION --cofniecie zmian wprowadzonych przez transakcje, jezeli zostanie przchwycony wyjatekEND CATCH


W C# tworzymy ją w ten sposób:

Code:
            using (var connection = new SqlConnection(connectionStringSettings.ConnectionString))
            {
                connection.Open();
                using (var transaction = connection.BeginTransaction())
                {
                    try
                    {
                        using (var command = connection.CreateCommand())
                        {
                            command.Transaction = transaction;
                            //Operacje
                        }
                        transaction.Commit();
                    }
                    catch (Exception ex)
                    {
                        transaction.Rollback();
                        Console.WriteLine(ex.Message);
                    }
                }
            }


Poziom izolacji w SQLu ustawiamy za pomocą komendy:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ

W C# ustawiamy poziom izolacji w metodzie BeginTransaction.


TransactionScope
Klasa ta pozwala w prosty sposób tworzyć bloki transakcyjne dla aplikacji działających z bazą MS SQL.
Kod:
Code:
            using (var transaction = new TransactionScope())
            {
                using (var connection = new SqlConnection(connectionStringSettings.ConnectionString))
                {
                    connection.Open();
                    using (var command = connection.CreateCommand())
                    {
                        //Operacje
                    }
                    transaction.Complete();
                }
            }

Kod sporo uproszczony, jednak dostępny tylko dla bazy MS SQL. W przypadku innych baz należy korzystać z wcześniej podanego rozwiązania

niedziela, 20 stycznia 2013

70-516 Pobieranie i zapis danych do bazy - DbCommand, DbDataAdapter, MARS, UDT

Wykonywanie poleceń na bazie danych najlepiej przeprowadzić przy użyciu klasy DbCommand. Klasa ta do swojego działania wymaga otwartego połączenia. Do połączenia można dodać parametry z wartościami. W zależności od providera parametry mogą wymagać poprawnych nazw.
DbCommand posiada następujące metody:
  • ExecuteNonQuery - wywołanie metody, która nie zwraca rezultatu - jedyną odpowiedzią jako otrzymamy w tym przypadku od serwera bazy danych to ilość zmienionych rekordów
  • ExecuteReader - zwraca klasę umożliwiającą odczyt danych w jedną stronę. Obiekt który zostanie zwrócony po wywołaniu tej metody to DbDataReader. Klasa ta posiada metodę Read, która zwraca czy są jeszcze dane do odczytania. Jeżeli nie otrzymamy wartość null.
  • ExecuteScalar - pozwala na odebranie pojedynczej wartości 

MARS - Multiple Active Result Sets
Ustawienie tej metody do połączenia z bazą danych umożliwia uruchamianie wielu DbDataReader-ów jednocześnie. Jeżeli nie włączymy tej technologi, na raz na serwerze będzie mógł działać tylko jeden DbDataReader na raz. Aby włączyć tę technologię należy do connection string dodać: MultipleActiveResultSets=True
Bez MARS można sobie poradzić tworząc większą ilość połączeń. Ta technologia w zamierzeniu ma ułatwić programiście tworzenie wielu zapytań do bazy danych, wykonywanych w tym samym czasie.


DbDataAdapter
Obiekt ten służy do pobierania i uaktualnia danych pomiędzy bazą a obiektem DataTable. DbDataAdapter posiada właściwości umożliwiające pobieranie, usuwanie, uaktualnianie i usuwanie danych z bazy:
  • SelectCommand
  • InsertCommand
  • UpdateCommand
  • DeleteCommand
DataAdapter sam zarządza połączeniem - otwiera i zamyka je automatycznie. Za pomocą metody Fill pozwala załadować dane do obiektów DataTable, DataSet. Chcąc korzystać z tych poleceń należy je uzupełnić albo ręcznie albo za pomocą obiektu DbCommandBuilder.
Aby zwiększyć wydajność zapisu danych do bazy danych, można ustawić właściwość UpdateBatchSize. Domyślnie wysyłany jest na raz jeden rekord do bazy. Zwiększając tę wartość, na raz zostanie wysłana większa ilość rekordów.


UDTs
Począwszy od MS SQL 2005 jest możliwość używania .NET w środowisku MS SQL. Dzięki temu, możemy napisać kod w C# który zostanie uruchomiony na serwerze. Dzięki wsparciu dla SQLCLR użytkownik może tworzyć własne typy i struktury danych, które zostaną przechowane na serwerze.
Należy pamiętać, że po stronie zarówno klienta, jak i serwera muszą być te same biblioteki DLL zawierające informacje o typach.
Przykładowo stworzymy program, który może przechowywać dane geograficzne (położenie):
Definicja tabeli w bazie danych:

CREATE TABLE Locations(ZipCode CHAR(5),Location Geography);

Następnie w projekcie dodajemy referencję do biblioteki Microsoft.SqlServer.Types. Na mojej maszynie biblioteka ta znajduje się pod ścieżką C:\Program Files (x86)\Microsoft SQL Server\110\SDK\Assemblies

Kod w C# pozwalający na dodanie informacji o położeniu:

Code:
            using (var connection = new SqlConnection(db2ConnectionString))
            {
                var command = connection.CreateCommand();
                command.CommandText = "INSERT INTO Locations VALUES(@zip, @loc)";
                var zip = new SqlParameter("@zip", "34120");
                var loc = new SqlParameter("@loc", SqlDbType.Udt);
                loc.UdtTypeName = "geography";
                loc.Value = SqlGeography.STGeomFromText(new SqlChars("POINT(41.44 78.21)"), 4326);
                command.Parameters.AddRange(new[] {zip, loc});
                connection.Open();
                command.ExecuteNonQuery();
            }

sobota, 19 stycznia 2013

70-516 Połączeniowy model danych - łączenie do źródeł

Za pomocą tzw. providerów możemy łączyć się do różnych źródeł danych w celu pobrania z nich danych, jak i zapisania ich.
Microsoft udostępnia kilka gotowych implementacji, jak i gamę interfejsów umożliwiających napisanie własnego providera. Dostępne providery:
  • OleDb - SQL Server, SyBase, DB2/400. Microsoft Access
  • Odbc - połączenie do wielu źródeł danych
  • SQL Server - MS SQL
W sieci można znaleźć wiele gotowych providerów.


Obiekt połączenia  - DbConnection
Aby móc pobierać i wysyłać dane należy stworzyć połączenie do źródła danych. Podczas tworzenia obiektu połączenia należy podać ConnectionString, który wskazuje gdzie znajduje się źródło i przekazuje dodatkowe parametry. Po zakończeniu pracy z źródłem, połączenie należy zamknąć aby zwolnić używane zasoby.
Przykład stworzenia połączenia:

Code:
            var sqlConnection = new SqlConnection("Server=localhost;Database=AdventureWorks;Trusted_Connection=True;");
            sqlConnection.Open();
            //Operacje na połączeniu
            sqlConnection.Close();

Warto sobie zapisać adres strony http://www.connectionstrings.com. Zawiera ona potrzebne dane do połączenia z bazami danych, plikami danych oraz innymi źródłami.


Przechowywanie ConnectionString-a
ConnectionString najlepiej przechowywać w jednej z podanych lokalizacji:
  • app.config
  • web.config
  • machine.config
Pliki te to XMLe. Dzięki temu w przypadku zmian, nie będzie wymagana ponowna kompilacja oprogramowania.
Dodatkowo dzięki klasie ConfigurationManager w łatwy sposób wydobędziemy informacje potrzebne do połączenia z bazą danych. 
Przykład:

Code:
  <connectionStrings>
    <clear/>
    <add name="db" providerName="System.Data.SqlClient" connectionString="Server=localhost;Database=AdventureWorksLT;Trusted_Connection=True;"/>
  </connectionStrings>

Pobranie tak zdefiniowane ConnectionString-a:

Code:
            var connectionStringSettings = ConfigurationManager.ConnectionStrings["db"];
            using (var connection = new SqlConnection(connectionStringSettings.ConnectionString))
            {
                //operacje
            }




Connection Pool
Tworzenie połączenia do bazy danych jest kosztowne. Aby zaradzić temu problemowi oferowany jest connection pool. Wyobrazić sobie można go jako cache o określonej wielkości. Gdy aplikacja potrzebuje połączenia, sprawdza czy w connection poolu istnieje wolne. Jeżeli tak zwraca je. Jeżeli nie istnieje wolne, ale nie osiągnięto jeszcze maksymalnej wielkości app poola, połączenie zostanie stworzone i zwrócone. Ostania ewentualność to brak wolnych połączeń i osiągnięcie maksymalnej wielkości connection poola. W takim przypadku żądanie o połączenie zostanie umieszczone w kolejce i po zwolnieniu przez inną aplikację połączenia - przydzielone do oczekującej aplikacji.
Connection pool kontrolowany jest przez parametry przekazywane do connection string-a. Możemy między innymi ustalić minimalną i maksymalną wielkość cache, czy połączenie ma być resetowane.

Aby connection pool działał, muszą być spełnione następujące warunki:
  • connection string dla każdego połączenia musi być takie same (dotyczy także wielkości znaków)
  • user id dla każdego procesu musi być takie same (jeżeli nie logujemy się za pomocą loginu i hasła, zostanie pobrany login konta systemowego)
  • ID procesu musi być taki sam
Jak widać z powyższej listy mamy do czynienia z wieloma ograniczeniami. Kiedy więc connection pool jest najbardziej opłacalny? W przypadku aplikacji webowych, gdzie łączymy się do serwera bazy danych zawsze za pomocą tego samego użytkownika.
Fakty odnoście connection pool:
  • Cache tworzony jest po stronie klienta aplikacji - aplikacja serwerowa nie wie o nim. 
  • connection pool tworzony jest po pierwszym utworzeniu połączenia i otarciu go
  • połączenie w connection pool przebywa domyślnie od 4 - 8 minut. Jeżeli chcemy, aby zawsze było dostępne jedno lub więcej należy zdefiniować tę liczbę w connection stringu
  • domyślnie connection pool ustawiony jest na maksymalnie 100 połączeń. Ilość użytych połączeń można sprawdzić w narzędziu Performance Monitor
  • connection pool można wyłączyć, jednak nie jest to zalecane i może znacząco obniżyć wydajność naszej aplikacji
  • czyścić connection pool możemy za pomocą statycznych metod połączenia: ClearPool oraz ClearAllPools

piątek, 18 stycznia 2013

70-516 Serializacja obiektów ADO.NET, bindowanie do kontrolek

W tym artykule skupimy się na serializacji obiektów ADO.NET, przechowywaniu w kolumnach typów złożonych oraz bindowaniu w aplikacjach do danych. 

Serializacja
Obiekt DataTable możemy zapisać do pliku XML, jak i zwykłego strumienia. Metoda która do tego służy to WriteXml:

Code:
simpleDataSet.Tables["Client"].WriteXml("plik.xml");

Nazwa pliku nie może zawierać spacji, a jeżeli nawet podalibyśmy nazwę ze spacjami zostanie ona zmieniona na znak podkreślenia - "_".
Zawartość generowanego XMLa można kontrolować za pomocą własnej schemy dla XMLa oraz ustawiając odpowiednie właściwości kolumn obiektu DataTable.
Za pomocą właściwości ColumnMapping możemy ustawić czy dana kolumna ma być atrybutem, elementem czy też w ogóle może się nie pojawić:

Code:
clientTable.Columns["Id"].ColumnMapping = MappingType.Attribute;

Możliwe wartości typu wyliczeniowego ColumnMapping:
  • Attribute - kolumna jako atrybut
  • Element - kolumna jako element
  • Hidden - dane z tej kolumny nie zostaną zapisane do XMLa
  • SimpleContent - dane zapisane są jako zwykły string bez dodatkowych tagów

Po zapisaniu danych należy zapisać schemę - XSD. Plik ten zawiera także XML, który jest definicją typów danych użytych w DataTable:

Code:
simpleDataSet.Tables["Client"].WriteXmlSchema("schema.xml");

Dane możemy także zapisać razem ze schemą tabeli w jednym pliku. Wybór należy do nas.
Dane wczytuje za pomocą metody ReadXml. Jeżeli schema była w innym pliku wczytujemy ją za pomocą metody ReadXmlSchema:

Code:
            DataTable dt = new DataTable();
            dt.ReadXmlSchema("schema.xml");
            dt.ReadXml("plik.xml");


Obsługa DataSet-u przebiega w ten sam sposób. Przykładowe zapisanie i wczytanie danych z plików XML wygląda następująco:

Code:
            simpleDataSet.WriteXml("dataset_data.xml");
            simpleDataSet.WriteXmlSchema("dataset_schema.xml");

            DataSet ds = new DataSet();
            ds.ReadXmlSchema("dataset_schema.xml");
            ds.ReadXml("dataset_data.xml");



DataSet DiffGram
DiffGram to zserializowany DataSet do pliku zawierający dane o wersji wiersza. Rozwiązanie to może się przydać w przypadku gdy rzadko łączymy się z bazą danych i przechowujemy DataSet lokalnie na dysku. Jeżeli dokonamy zmian, następnie zapiszemy je do pliku a później odczytamy, stracimy informacje o zmianach. Nie będziemy w stanie uaktualnić danych w bazie danych.
Przykład tworzenia DiffGram-a:

Code:
simpleDataSet.WriteXml("Diff_Gram.xml", XmlWriteMode.DiffGram);

Wczytując (deserializując) dane należy podać odpowiednią wartość XmlReadMode:
  • Auto - plik XML zostanie sprawdzony i zostanie wybrana najlepsza metoda odczytu
  • DiffGram - zachowuje się w taki sam sposób jak metoda Merge
  • Fragment - wczytuje plik XML jako część, oznacza to że plik XML może zawierć wiele elementów głónych
  • IgnoreSchema - nie deserializuje schematu
  • InferSchema - schemat DataSet-u jest tworzony na podstawie danych zawartych w pliku
  • InferTypedSchema - schemat DataSet-u tworzony jest na podstawie danych zawartych w pliku, jeżeli nie jest w stanie jednoznacznie wyznaczyć typu - użyty zostanie typ string
  • ReadSchema - wczytuje dane wraz z schematem


Binarna serializacja DataSet 
Serializacja binarna przydatna jest w przypadku gdy chcemy aby plik wynikowy był jak najmniejszy. Serializacja przebiega w taki sam sposób. Należy jednak wcześniej ustawić właściwość RemotingFormat na Binary. Jeżeli tego nie zrobimy, plik wynikowy będzie zawierał w swoim wnętrzu plik XML.
Serializacja powinna wyglądać w następujący sposób:

Code:
            simpleDataSet.RemotingFormat = SerializationFormat.Binary;
            using (var fs = new FileStream("dane.bin", FileMode.Create))
            {
                var binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(fs, simpleDataSet);
            }



Odczyt danych za pomocą DataTableReader
Dane z DataTable oraz DataSet można odczytać za pomocą klasy DataTableReader. Klasa ta pozwala tylko na odczyt wierszy za pomocą metody Read. Jeżeli wiersz zostanie dodany w trakcie iteracji po danych to w zależności od tego czy zostanie dodany przed wskaźnikiem aktualnej pozycji czy też za nim zostanie wyświetlony bądź też nie. Kiedy DataTableReader dojdzie do końca tabeli, metoda Read zwróci null. Po tej operacji dodanie danych nie spowoduje ich odczytania.
Jeżeli iterujemy DataSet, dane będą odczytywane najpierw z pierwszej tabeli a następnie za pomocą metody NextResult możemy przejść do kolejnej tabeli.
Przykład odczytu DataSet:

Code:
            DataTableReader dr = simpleDataSet.CreateDataReader();
            while (dr.Read())
            {
                Console.WriteLine(dr["Id"] + " " + dr["FirstName"]);
            }
            dr.NextResult();
            while (dr.Read())
            {
                Console.WriteLine(dr["Id"] + " " + dr["CreationDate"]);
            }



Przechowywanie typów złożonych w kolumnach DataTable
W kolumnie DataTable można przechowywać typy specjalne, takie jak obiekty klasy, dane XML. Kolumna taka może przechować dowolny typ który jest referencyjny - wyjątkiem jest klasa string.
Przykład w jaki sposób można przechować w kolumnie obiekty typu Computer w tabeli User:

Code:
            DataTable dtSpecializedColumn = new DataTable("User");
            dtSpecializedColumn.Columns.Add("Id", typeof (int));
            dtSpecializedColumn.Columns.Add("FirstName", typeof (string));
            dtSpecializedColumn.Columns.Add("LastName", typeof (string));
            dtSpecializedColumn.Columns.Add("Computer", typeof (Computer));

            dtSpecializedColumn.Rows.Add(new object[]
                            {
                                1, "Jacek", "Kowalski",
                                new Computer {Id = 1, GraphicCard = "Ati", Processor = "Intel", UsbAmount = 10}
                            });
            Console.WriteLine(((Computer)dtSpecializedColumn.Rows[0]["Computer"]).UsbAmount);



Bindowanie w WindowsForms
Kontrolki w WindowsForms można bindować do obiektów implementujących IList, IListSource, IBindingList, IBindingListView. Właściwości które używamy podczas bindowania:
  • DataSource - źródło implementujące jeden z wcześniej wymienionych interfejsów
  • DataMember - jeżeli źródło zawiera wiele elementów np. DataSet zawiera wiele obiektów DataTable, należy wskazać, którą tabelę chcemy wyświetlić. 
  • DisplayMember - dla kontrolek typu lista wskazuje która kolumna ma być wyświetlana 
  • ValueMember - wskazuje, wartość która zostanie pobrana z kontrolki typu lista; najczęściej jest to klucz główny

Bindowanie w ASP.NET
W przypadku ASP.NET, właściwości w kontrolkach webowych mają takie same znaczenie jak dla WindowsForms.
Należy pamiętać, że po bindowaniu musi zostać wywołana metoda DataBind. Jeżeli tego zrobimy dane nie zostaną dodane do kontrolki.


Bindowane w WPF
W przypadku WPFa możemy bindować do praktycznie dowolnego obiektu. Główną właściwością do której następuje bindowanie jest ItemsSource.


Źródła do pobrania: https://skydrive.live.com/redir?resid=BCCBFB57B3C672D0!150&authkey=!AJunqZlTIcRyX-s

czwartek, 17 stycznia 2013

70-516 DataSet

DataSet można sobie wyobrazić jako pamięciową bazę danych. Obiekt ten nie oferuje oczywiście takich aspektów jak transakcje.
Obiekt ten przechowuje w sobie tabele jako obiekty klasy DataTable oraz relacje pomiędzy nimi przedstawione jako obiekty DataRelation.
DataSet umożliwia ponadto:
  • kopiowanie danych jak i schematu do nowych DataSet-ów
  • łączenie z innym DataSet-em
  • śledzenie i reagowanie na zmiany
DataSet można stworzyć zarówno w kodzie jak i dostarczając XML z konfiguracją.
Przykład tworzenia DataSet-a zawierającego tabele klientów i zamówień, oraz połączenie pomiędzy tymi tabelami:

Code:
            var clientDataSet = new DataSet("ClientOrders");
            
            var clientTable = new DataTable("Client");
            clientTable.Columns.Add(new DataColumn("Id", typeof (int)));
            clientTable.Columns.Add("FirstName", typeof (string));
            clientTable.Columns.Add("LastName", typeof(string));
            clientTable.Columns.Add("BirthYear", typeof(int));
            clientTable.PrimaryKey = new DataColumn[] {clientTable.Columns["Id"]};

            var orderTable = new DataTable("Order");
            orderTable.Columns.Add("Id", typeof (int));
            orderTable.Columns.Add("CreationData", typeof (DateTime));
            orderTable.Columns.Add("Price", typeof (decimal));
            orderTable.Columns.Add("ClientId", typeof (int));
            orderTable.PrimaryKey = new DataColumn[] {orderTable.Columns["Id"]};

            clientDataSet.Tables.AddRange(new DataTable[] {clientTable, orderTable});
            clientDataSet.Relations.Add("client_order", clientTable.Columns["Id"], orderTable.Columns["ClientId"]);


Lepszym rozwiązaniem od powyższego, jest stworzenie tzw. typowanego DataSet-u. Tworzymy go dziedzicząc po klasie DataSet a następnie tworząc właściwości odpowiadające poszczególnym tabelą. Zaletą tego rozwiązania jest późniejsze wsparcie IntelliSense podczas odnoszenia się do tabel.

Drugim sposobem tworzenia typowanego DataSet-a jest stworzenie pliku XML zawierającego specyfikację. XML-a można stworzyć ręcznie ale istnieje też narzędzie umożliwiające graficzne tworzenie reprezentacji DataSet-a. Aby uruchomić narzędzie wystarczy dodać do projektu nowy plik typu DataSet:







Po dodaniu nowych plików zostanie otwarty edytor gdzie w graficzny sposób stworzymy schemat:





Relacje
Za relacje pomiędzy tabelami odpowiada obiekt typu DataRelation. Za pomocą obiektu relacji możemy nawigować w obie strony: od tabeli nadrzędnej do podrzędnej jak i od podrzędnej do nadrzędnej.
Przykład nawigacji (na końcu zostanie udostępniony kod do pobrania z całością omawianego tutaj materiału):

Code:
            //1. Od tabeli nadrzędnej do podrzędnej
            var childRows = simpleDataSet.Tables["Client"].Rows[0].GetChildRows("client_order");
            foreach (var childRow in childRows)
            {
                Console.WriteLine("{0} {1} {2}", childRow["Id"], childRow["CreationDate"], childRow["Price"]);
            }
            Console.WriteLine("----------------------------");
            //2. Od podrzędnej do nadrzędnej
            var parentRow = simpleDataSet.Tables["Order"].Rows[0].GetParentRow("client_order");
            Console.WriteLine("{0} {1} {2}", parentRow["Id"], parentRow["FirstName"], parentRow["LastName"]);


Klucze obce
Podczas tworzenia relacji stworzyliśmy klucz obcy w tabeli Order. Za tworzenie klucza odpowiada jeden z parametrów funkcji Add dla kolekcji relacji:

Code:
clientDataSet.Relations.Add("client_order", clientTable.Columns["Id"], orderTable.Columns["ClientId"], true);

Domyślnie parametr ten jest ustawiony na true czyli klucz obcy jest tworzony za każdym razem.


Kaskadowe uaktualnianie i usuwanie danych
Klucze obce wymuszają zachowanie integralności pomiędzy tabelą nadrzędną a podrzędną. Wiersze podrzędne nie mogą zostać stworzone, dopóki wiersz nadrzędny nie będzie istniał. W bazach danych jak i w obiekcie DataRelation można ustawić tryb kaskadowego usuwania danych. W tym trybie, gdy zostanie usunięty wiersz z tabeli nadrzędnej, wiersze z tabeli podrzędnej zostaną usunięte automatycznie.
Typ wyliczeniowy Rule zawiera inne możliwości dla reguł usuwania jak i uaktualnia danych:
  • Cascade - domyślne ustawienie - uaktualnia lub kasuje potomne wiersze w momencie zmiany unikalnego klucza, bądź usunięcia wiersza nadrzędnego 
  • None - spowoduje rzucenie wyjątku w przypadku próby usunięcia wiersza nadrzędnego lub próby zmiany jego wartości
  • SetDefault - ustawia wartość domyślną dla kolumny klucza obcego
  • SetNull - ustawia null dla klucza obcego
Aby ustawić inną wartość dla klucza głównego, np. aby rzucał wyjątkiem, jeżeli w tabeli istnieją podrzędne wiersze, należy pobrać klucz główny i zmienić jego właściwość DeleteRule/UpdateRule:

Code:
            ((ForeignKeyConstraint)orderTable.Constraints["client_order"]).DeleteRule = Rule.None;
            ((ForeignKeyConstraint)orderTable.Constraints["client_order"]).UpdateRule = Rule.None;



Łączenie DataSet-ów
Przykładem zastosowania powyższej operacji jest pobranie z bazy danych, a następnie dodanie danych pobranych z pliku. DataSet umożliwia łączenie danych z obiektami typu DataSet, DataTable oraz DataRow za pomocą metody Merge:

Code:
            var copyOfDataset = simpleDataSet.Copy();
            copyOfDataset.Tables["Client"].Rows[2]["FirstName"] = "Kamil";
            copyOfDataset.Tables["Order"].Rows.Add(9, DateTime.Parse("2013-01-26"), 256.78, 2);

            simpleDataSet.Merge(copyOfDataset, false, MissingSchemaAction.AddWithKey);

Parametry które przyjmuje metoda Merge:
  • pierwszy parametr to DataSet z którym ma nastąpić połączenie
  • drugi parametr mówi o tym czy zmiany dokonane w drugim DataSet mają nadpisać dane w pierwszym. W podanym wyżej przykładzie wiersz gdzie zmieniamy imię na Kamil nie ma stanu Unchanged - w takim przypadku flaga ustawiona na true sprawiłaby, że dane nie zostały by uaktualnione
  • trzeci parametr to wartość typu wyliczeniowego MissingSchemaAction - ustawienie to odpowiada w jaki sposób mają zostać potraktowane nowe dane.
Typ wyliczeniowy MissingSchemaAction:
  • Add - dodaje wymagane obiekty DataTable oraz DataColumn do schematu
  • AddWithKey - dodaje tabele, kolumny oraz klucz główny
  • Error - jeżeli dana kolumna nie istnieje w DataSecie docelowym, zostanie rzucony wyjątek
  • Ignore - dane przechowywane w kolumnach, które nie występują w źródle nie zostaną dodane
Należy pamiętać o tym aby dodawać klucz główny do tabeli, którą zamierzamy łączyć z naszym DataSetem. Jeżeli tego nie zrobimy, wiersze zamiast ulec uaktualnieniu zostaną powielone (wiersze z drugiej tabeli zostaną skopiowane jako nowe rekordy).

Link do pobrania źródeł: http://sdrv.ms/ZYw9oE