OData – nowy standard udostępniania i korzystania z danych, cz. 2a Udostępnij na: Facebook

Autor: Tomasz Wiśniewski

Opublikowano: 2011-01-31

Wstęp

W pierwszej części artykułu omówiony został protokół OData oraz problem, jaki stara się on rozwiązać w dzisiejszym świecie nowoczesnych aplikacji. W tej części omówimy strukturę serwisu, który wykorzystuje Open Data Protocol. Serwis ten jako źródło danych wykorzystuje szkoleniową bazę danych Northwind, a proces budowy takiego serwisu zostanie omówiony w jednej z kolejnych części tej serii artykułów.

Przeglądanie i budowa serwisu

Aby móc przeglądać serwis zbudowany za pomocą omawianego protokołu wystarczy dowolna przeglądarka internetowa, ponieważ – nawiązując do tego, co zostało napisane w części pierwszej –  protokół OData korzysta ze znanych standardów internetowych takich jak HTTP i ATOM/JSON. Dzięki temu po wejściu na adres serwisu (w opisywanym przypadku będzie to: https://localhost/ODataMSDN/NorthwindService.svc/ – jeśli nie jest zabezpieczony hasłem), użytkownik zobaczy dane w postaci przypominającej kanał informacji RSS:

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
 - <service xml:base="https://localhost/ODataMSDN/NorthwindService.svc/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app">
       - <workspace>
             <atom:title>Default</atom:title>
             - <collection href="Categories">
                    <atom:title>Categories</atom:title>
             </collection>
             - <collection href="CustomerDemographics">
                    <atom:title>CustomerDemographics</atom:title>
             </collection>
             - <collection href="Customers">
                    <atom:title>Customers</atom:title>
             </collection>
             - <collection href="Employees">
                    <atom:title>Employees</atom:title>
             </collection>
             - <collection href="Order_Details">
                    <atom:title>Order_Details</atom:title>
             </collection>
             - <collection href="Orders">
                    <atom:title>Orders</atom:title>
             </collection>
             - <collection href="Products">
             <atom:title>Products</atom:title>
             ...
       </workspace>
 </service>

Informacje, jakie zwraca serwis, do złudzenia przypominają kanał RSS. Póki co, nie zawiera on żadnych dodatkowych elementów, które byłyby wprowadzone dodatkowo, aby wyświetlić dane w powyższej postaci. Na uwagę zasługują węzły collection – posiadają one atrybut href, który określa, co należy dopisać do adresu bazowego, aby wyświetlić daną kolekcję. Adres bazowy to adres zdefiniowany w głównym węźle service w atrybucie xml:base. Kolekcje te są generowane przez serwis na podstawie modelu danych, który został zdefiniowany na jego potrzeby. Wycinek tego modelu w Visual Studio w pliku .edxm prezentuje się następująco:

**Rys.1.Wycinek modelu Entity Framework bazy Northwind.

Dopisując zatem do adresu bazowego słowo Employees, można wyświetlić kolekcję pracowników, którą przechowuje baza Northwind: https://localhost/ODataMSDN/NorthwindService.svc/Employees. Dla zaoszczędzenia miejsca poniższy wynik został ograniczony tylko do jednej encji:

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
 - <feed xml:base="https://localhost/ODataMSDN/NorthwindService.svc/" 
             xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservices"              
             xmlns:m="https://schemas.microsoft.com/ado/2007/08/dataservices/metadata" 
             xmlns="http://www.w3.org/2005/Atom">
       <title type="text">Employees</title>
       <id>https://localhost/ODataMSDN/NorthwindService.svc/Employees</id>
       <updated>2011-01-05T11:17:06Z</updated>
       <link rel="self" title="Employees" href="Employees" />
       - <entry>
             <id>https://localhost/ODataMSDN/NorthwindService.svc/Employees(1)</id>
             <title type="text" />
             <updated>2011-01-05T11:17:06Z</updated>
             - <author>
                    <name />
             </author>
             <link rel="edit" title="Employee" href="Employees(1)" />
             <link                                                              
                       rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Employees1" 
                       type="application/atom+xml;type=feed" 
                       title="Employees1"                  
                       href="Employees(1)/Employees1" />
             <link                      
                       rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Employee1" 
                       type="application/atom+xml;type=entry" 
                       title="Employee1"                  
                       href="Employees(1)/Employee1" />
             <link        
                       rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Orders"
                       type="application/atom+xml;type=feed" 
                       title="Orders"                      
                       href="Employees(1)/Orders" />
             <link               
                       rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Territories" 
                       type="application/atom+xml;type=feed" 
                       title="Territories"                 
                       href="Employees(1)/Territories" />
             <category term="NorthwindModel.Employee" 
                             scheme="https://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
             - <content type="application/xml">
                    - <m:properties>
                           <d:EmployeeID m:type="Edm.Int32">1</d:EmployeeID>
                           <d:LastName>Davolio</d:LastName>
                           <d:FirstName>Nancy</d:FirstName>
                           <d:Title>Sales Representative</d:Title>
                           <d:TitleOfCourtesy>Ms.</d:TitleOfCourtesy>
                           <d:BirthDate m:type="Edm.DateTime">1948-12-08T00:00:00</d:BirthDate>
                           <d:HireDate m:type="Edm.DateTime">1992-05-01T00:00:00</d:HireDate>
                           <d:Address>507 - 20th Ave. E. Apt. 2A</d:Address>
                           <d:City>Seattle</d:City>
                           <d:Region>WA</d:Region>
                           <d:PostalCode>98122</d:PostalCode>
                           <d:Country>USA</d:Country>
                           <d:HomePhone>(206) 555-9857</d:HomePhone>
                           <d:Extension>5467</d:Extension>
                           <d:Photo m:type="Edm.Binary">TUTAJ ZNAJDUJE SIĘ BINARNA REPREZENTACJA ZDJĘCIA, KTÓRA DLA OSZCZĘDNOŚCI MIEJSCA ZOSTAŁA ZASTĄPIONA TEKSTEM</d:Photo>
                           <d:Notes>Education includes a BA in psychology from Colorado State University in 1970. She also completed "The Art of the Cold Call." Nancy is a member of Toastmasters International.</d:Notes>
                           <d:ReportsTo m:type="Edm.Int32">2</d:ReportsTo>
                    </m:properties>
             </content>
       </entry>
       ...
</feed>
UWAGA!
Jeśli przeglądarka internetowa wykorzystywana do wyświetlenia serwisu nie pokazuje danych w powyższej postaci, to należy wyłączyć wbudowany w nią czytnik kanałów RSS. W przypadku przeglądarki Internet Explorer można to zrobić, wybierając opcję menu: Narzędzia ->Opcje internetowe. Następnie wybrać zakładkę Zawartość i w sekcji Źródła i obiekty Web Slice wybrać opcję Ustawienia. W oknie, które zostanie otwarte, w sekcji Zaawansowane należy odznaczyć opcję Włącz widok odczytywania źródła danych. Po tej zmianie należy jeszcze raz wejść na daną stronę serwisu.

Istotne  są dwie nowe przestrzenie nazw zdefiniowane w węźle feed, w atrybutach xmlns:d oraz xmlns:m. Odpowiadają one na wyświetlanie danych oraz określanie ich typów. Najważniejszym i podstawowym węzłem w serwisie implementującym OData jest węzeł entry, który definiuje encje lub klasę w modelu danych zasilającym serwis. Pierwszym najistotniejszym elementem jest <id>. Wybiegając trochę w przyszłość i do tego, co będzie opisywanie w dalszej części, czyli manipulowanie danymi wystawianymi przez serwis, widać dodanie do adresu bazowego ścieżki Employees(1). Informuje to serwis, że ma zwrócić element typu Employee o ID równym 1. Każda encja/klasa wystawiana za pomocą OData musi posiadać element jednoznacznie ją identyfikujący, w przeciwnym razie serwis w ogóle nie zadziała. Kolejne elementy, takie jak title, updated czy author, są związane ze standardem ATOM i ich wypełnienie powoduje, że czytnik RSS potrafi w lepszy sposób wyświetlić dane. Ciekawym elementem jest updated. W relacyjnych bazach danych bardzo często spotyka się w tabelach kolumnę informującą, kiedy ostatnio został zmodyfikowany dany rekord. Taką informację według najlepszych praktyk dobrze jest podpiąć do tego właśnie pola. Jeśli jednak w bazie danych nie ma takiej informacji, pole to zostanie wypełnione datą i czasem wykonania zapytania na danym obiekcie.

Następny blok elementów odnosi się do drugiego najważniejszego aspektu EDM (Entity Data Model), czyli asocjacji. W powyższym przykładzie dla pracownika o ID równym 1 jest ich pięć. Pierwszy <link> odnosi się do tego samego, o czym wspomniano przed chwilą, czyli wyświetlenia pojedynczej encji zamiast całej kolekcji. Kolejne dwa są to relacje do tej samej encji. Wynika to z faktu, że baza Northwind na potrzeby budowy hierarchii pracowników odwołuje się do klucza głównego tej samej tabeli, co nazywane jest self-reference. Nie ma to większego znaczenia dla  tego artykułu, dlatego zajmiemy się kolejnymi dwoma bliźniaczymi relacjami. Element <link> posiada przede wszystkim atrybut rel, w którym jest podane, do jakiej innej tabeli jest to relacja. Podany jest też typ oraz tytuł, ale najważniejszym atrybutem jest oczywiście href, który określa, co należy dodać do adresu bazowego, aby wyświetlić powiązane dane. Dlatego też po wpisaniu do przeglądarki adresu: https://localhost/ODataMSDN/NorthwindService.svc/Employees(1)/Orders otrzymamy wszystkie zamówienia realizowane przez danego pracownika. Taki adres URL zostanie przetłumaczony przez WCF Data Services, wykorzystane do budowy przykładowego serwisu na takie zapytanie:

SELECT [Extent1].[OrderID] AS [OrderID], [Extent1].[CustomerID] AS [CustomerID], [Extent1].[EmployeeID] AS [EmployeeID], -- TUTAJ INNE KOLUMNY FROM [dbo].[Orders] AS [Extent1] WHERE ([Extent1].[EmployeeID] IS NOT NULL) AND (1 = [Extent1].[EmployeeID])

gdzie wyróżniona 1 jest identyfikatorem danego pracownika.

Warto tutaj ponownie zwrócić uwagę na model EDXM w aplikacji, a poza tym na bazę danych, która ten model zasila. Wycinek modelu prezentuje poniższy rysunek:

**Rys.2.Właściwości nawigacyjne encji Employee.

Na tym przykładzie widać wyraźnie abstrakcję, jaką dostarczą Entity Framework względem bazy danych, ponieważ powyższe zapytanie SQL odnosi się do zupełnie innej tabeli. Tabela ta to Orders, a jej wybrane kolumny przedstawia rysunek 3:

**Rys.3.Kolumny tabeli Orders z zaznaczonym kluczem obcym do tabeli Employees.

Kolejną sekcją dla elementu entry jest content, czyli wartości dla poszczególnych właściwości danej encji. Dla przykładu weźmiemy dwie pierwsze, czyli EmployeeID i LastName. Pierwszą częścią elementu jest jego nazwa, która odpowiada nazwie właściwości zdefiniowanej w modelu danych. Następnie znajduje się opcjonalny atrybut. Opcjonalny dlatego, że jeśli dana właściwość jest typu string, to jest on pomijany, ale w przypadku EmployeeID, który jest innego typu, jest on jawnie podany poprzez atrybut m:type, a w tym przypadku jest to Edm.Int32. Jest to ciekawa cecha protokołu OData, która nazywa się samoopisujące właściwości, dzięki czemu odczytując takie właściwości zawsze można sprawdzić, jakiego są one typu. Open Data Protocol wspiera większość najpopularniejszych typów prostych, takich jak już wspomniany Int32, String, Decimal, Double, Boolean i wiele innych.

Wymienione wyżej elementy drzewa XML mają swoje odzwierciedlenie w modelu danych. Przykładowa encja Employee w tym modelu wygląda  następująco:

**Rys.4.Encja Employee w modelu danych aplikacji.

Widać na powyższym przykładzie odzwierciedlenie właściwości encji przez serwis 1:1, dzięki czemu korzystając z serwisu wystarczy znać model danych, aby móc operować na danych, o czym w dalszej części artykułu.

Powstaje jednak problem – co w sytuacji, gdy programista nie zna modelu danych, jaki zasila serwis. Dowiadywanie się o strukturze danych poprzez parsowanie konkretnych encji byłoby dość uciążliwym i pracochłonnym zadaniem. Dlatego serwisy wykorzystujące OData udostępniają dokument z metadanymi. Aby go wyświetlić, należy w przeglądarce dla danego serwisu dopisać ścieżkę $metadata:

https://localhost/ODataMSDN/NorthwindService.svc/$metadata.

Dokument taki składa się z dwóch części – pierwsza opisuje model danych, a druga kolekcje obiektów i na jakich typach encji z modelu dana kolekcja operuje.Spójrzmy na encję Employee:

- <EntityType Name="Employee">
       - <Key>
             <PropertyRef Name="EmployeeID" />
       </Key>
       <Property Name="EmployeeID" Type="Edm.Int32" Nullable="false" p8:StoreGeneratedPattern="Identity"                                              xmlns:p8="https://schemas.microsoft.com/ado/2009/02/edm/annotation" />
       <Property Name="LastName" Type="Edm.String" Nullable="false" MaxLength="20" Unicode="true" FixedLength="false" />
       ...
       <NavigationProperty Name="Orders" Relationship="NorthwindModel.FK_Orders_Employees" FromRole="Employees" ToRole="Orders" />
       ...
</EntityType>

Pierwszym ważnym elementem jest węzeł <Key>, który definiuje jaka właściwość danej encji jest jej unikalnym identyfikatorem. Następnie kolejno wypisane są właściwości encji w elementach <PropertyName>, które mogą posiadać różne zestawy atrybutów. W przypadku EmployeeID, który jak wiadomo jest kluczem głównym dla encji Employee, jest to Type określający tym właściwości, Nullable określający, czy dana właściwość może zawierać wartość pustą oraz, w tym przypadku, odwołanie do faktu, iż ten klucz główny jest generowany przez bazę SQL Server automatycznie poprzez Identity.

W przypadku drugiej właściwości – LastName – te atrybuty są inne. Zawiera ona m.in. informację o maksymalnej długości ciągu znaków – MaxLength, czy jest zapisywane w formacie Unicode i czy ma stałą długość – FixedLength.

Oprócz samych definicji właściwości prostych w encjach są też zdefiniowane właściwości nawigacyjne, czyli klucze obce (mówiąc w aspekcie relacyjnych baz danych), reprezentowane poprzez <NavigationProperty>. Najważniejsze atrybuty, jakie zawiera, to FromRole oraz ToRole, określające z której do której tabeli jest zdefiniowana dana relacja. Z właściwością określającą relacje wiąże się także kolejny element tej części dokumentu z metadanymi, a mianowicie <AssociationName>:

- <Association Name="FK_Orders_Employees">
       <End Role="Employees" Type="NorthwindModel.Employee" Multiplicity="0..1" />
       <End Role="Orders" Type="NorthwindModel.Order" Multiplicity="*" />
       - <ReferentialConstraint>
             - <Principal Role="Employees">
                    <PropertyRef Name="EmployeeID" />
             </Principal>
             - <Dependent Role="Orders">
                    <PropertyRef Name="EmployeeID" />
             </Dependent>
       </ReferentialConstraint>
 </Association>

Określone tutaj są takie informacje, jak nazwa danej relacji oraz pomiędzy jakimi tabelami ona występuje. Dodatkowo zdefiniowane są krotności danej relacji poprzez Multiplicity. Poza tym jest podane, od jakiej właściwości w danej tabeli zależy dana relacja, a w powyższym przykładzie jest to EmployeeID, ponieważ musimy znać identyfikator naszego pracownika, aby wyświetlić zamówienia, które on obsługuje.

Druga część dokumentu metadanych zawiera dwie podstawowe informacje. Pierwsza z nich określa, z jakiego typu encji będzie korzystała dana kolekcja, a zdefiniowane jest to np. dla kolekcji Employees w następujący sposób:

<EntitySet Name="Employees" EntityType="NorthwindModel.Employee" />

Druga informacja to zdefiniowanie asocjacji pomiędzy kolekcjami:

- <AssociationSet Name="FK_Orders_Employees" Association="NorthwindModel.FK_Orders_Employees">
       <End Role="Employees" EntitySet="Employees" />
       <End Role="Orders" EntitySet="Orders" />
 </AssociationSet>

Tym razem, zamiast definiowania pomiędzy jakimi encjami występuje relacja, zdefiniowane jest pomiędzy jakimi kolekcjami ona występuje.

Wszystkie dane, na podstawie których WCF Data Services generuje dokument z metadanymi, są brane z modelu danych. Począwszy od właściwości, ich nazw i typów, a skończywszy na krotnościach relacji pomiędzy encjami, co przedstawia poniższy rysunek:

**Rys.5.Encja Employee oraz encje powiązane.