Projektowanie, Programowanie, Codzienność – BeniaminZaborski.com

5 Maj 2013

XPO jak Yeti

Filed under: How Can I Take ... — Tagi: , , — Beniamin Zaborski @ 18:50

XPO czyli eXpressPersistent Objects to komercyjny ORM firmy DevExpress. W naszym kraju to nieco egzotyczny produkt i jest z nim trochę jak z mitycznym Yeti – wszyscy słyszeli, widziało niewielu. Z czego wynika ta niezbyt duża popularność tego ORM-a? Czy winne jest temu to, że trzeba za niego płacić? Jakie ma wady, a jakie są jego zalety? Na te pytania jak i na kilka innych postaram się odpowiedzieć w tym wpisie.

Chciałbym zaznaczyć, że nie jestem ani nie byłem w jakikolwiek sposób związany z firmą DevExpress. Postaram się dość obiektywnie, ale jednak w jakimś stopniu przez pryzmat mojego doświadczenia, bliżej przyjrzeć XPO. Dzięki ukończeniu projektu wykorzystującego XPO jak i dzięki moim wcześniejszym doświadczeniom z innymi ORM-ami jak NHibernate czy Entity Framework, mogę spojrzeć na XPO z dużo szerzej perspektywy.

Swego czasu popełniłem wpis na blogu dotyczący Entity Framework. Chciałbym się odnieść właśnie do tego wpisu, co umożliwi w jakimś stopniu porównanie XPO z Entity Framework. Użyję dokładnie tego samego modelu klas, który wykorzystałem we wspomnianym wpisie. Oto on w postaci diagramu klas UML:

Obrazek

Model klas to ta część która z punktu biznesowego powinna nas interesować najbardziej. Sytuacja idealna to taka w której model nie ma świadomości o istnieniu ORM-a. Niestety jak się okazuje nie każdy ORM daje nam to o czym marzymy. Do tego tematu jeszcze wrócimy w dalszej części.

Z przedstawionego modelu klas możemy wywnioskować, że będziemy musieli uporać się z mapowaniem: typów prostych jak string czy bool, typów referencyjnych, relacji one-to-many czy many-to-many. Celowo pominąłem kwestię mapowania dziedziczenia, gdyż we wpisie do którego się odnoszę również nie było o tym mowy.

Zanim przystąpimy do mapowania klas na tabele bazy danych trzeba wspomnieć o dwóch głównych koncepcjach obsługi operacji CRUD jakie daje nam XPO. Pierwsza koncepcja zakłada, że klasy encji biznesowych będą dziedziczyć po specjalnych klasach bazowych XPO. W tym momencie włącza się nam czerwona lampka … grhhh. Z powodów oczywistych takie podejście odrzucamy już na wstępie. Pisałem o tym kiedyś w tym wpisie.
Na szczęście (to się dopiero okaże) dokumentacja wspomina także o odmiennym podejściu zwanym session-less. Dzięki temu nasze encje biznesowe pozostaną POCO. To cieszy i jest na plus dla XPO. Oznaczać to będzie także, że wykonywanie operacji CRUD na obiektach będzie się odbywało za pomocą obiektu sesji, a nie jak w pierwszej koncepcji za pomocą dedykowanych do tego celu metod wewnątrz obiektów. No wszystko logiczne – przecież dokładnie tego oczekiwaliśmy.

Pora najwyższa zabrać się wreszcie za mapowanie klas na tabele bazy danych. Jedynym oficjalnym sposobem mapowania są atrybuty. Oczywiście zaletą takiego rozwiązania jest wygoda. Bezwzględnie wadą w takim podejściu jest „zabrudzenie” modelu „obcymi” atrybutami. Trochę żal że producent nie zdecydował się na dostarczenie alternatywnej i „czystszej” metody mapowania. Konkurencja w postaci twórców NHibernate czy Entity Framework przemyślała to zdecydowanie bardziej.
Kolejna wada XPO, chociaż nie jest to typowa przypadłość tego ORM-a, to potrzeba odwzorowywania kolekcji poprzez specjalny typ dostarczany przez XPO czyli XPCollection. W tej kwestii Entity Framework wymiata! Mając to na uwadze już wiemy, że będziemy musieli zmodyfikować nasz model klas zastępując typy wszystkich kolekcji IList typem XPCollection.

Aby poinformować XPO o tym iż dana klasa ma być mapowana na tabelę bazy danych należy oznaczyć ją atrybutem Persistent z opcjonalnym parametrem określającym nazwę tabeli w bazie danych. Dokładnie ten sam atrybut można zaaplikować dowolnej właściwości podając opcjonalnie nazwę kolumny tabeli. Mapowanie właściwości typów prostych jak i referencyjnych jest banalne. A co z kolekcjami? Tu zaczynają się schody. Oczywiście pomijając to, że wszystkie nasze kolekcje muszą być typu XPCollection (to już zaakceptowaliśmy – czyżby?), to musimy te kolekcje jeszcze zainicjować. Na nieszczęście potrzebny do tego będzie nam obiekt sesji. Hmmm … zatem stwierdzenie session-less wydaje się być trochę na wyrost. Niestety tak jest i to boli!

Poniżej przedstawiam kompletny model klas dostosowany do wymogów XPO wraz z pełnym mapowaniem:

    [Persistent(„Komputery”)]
    public class Komputer : BusinessEntity
    {
        public Komputer() : base() { }
        public Komputer(Session session) : base(session) { }
        [Persistent(„Nazwa”), Size(120)]
        public string Nazwa { get; set; }
        [Persistent(„AdresIP”), Size(14)]
        public string AdresIP { get; set; }
        [Association(„Uzytkownik-Komputery”)]
        public Uzytkownik Uzytkownik { get; set; }
    }
    [Persistent(„Role”)]
    public class Rola : BusinessEntity
    {
        public Rola() : base() { }
        public Rola(Session session) : base(session)
        {
            this.uprawnienia = new XPCollection<Uprawnienie>(session, this, session.GetClassInfo<Rola>().GetMember(„Uprawnienia”));
        }
        [Persistent(„Nazwa”), Size(120)]
        public string Nazwa { get; set; }
        private XPCollection<Uprawnienie> uprawnienia;
        [Association(„Role-Uprawnienia”)]
        public XPCollection<Uprawnienie> Uprawnienia
        {
            get { return uprawnienia; }
        }
    }
    [Persistent(„Uprawnienia”)]
    public class Uprawnienie : BusinessEntity
    {
        public Uprawnienie() : base() { }
        public Uprawnienie(Session session) : base(session)
        {
            this.role = new XPCollection<Rola>(session, this, session.GetClassInfo<Uprawnienie>().GetMember(„Role”));
        }
        [Persistent(„Nazwa”), Size(120)]
        public string Nazwa { get; set; }
        private XPCollection<Rola> role;
        [Association(„Role-Uprawnienia”)]
        public XPCollection<Rola> Role
        {
            get { return role; }
        }
    }
    [Persistent(„Uzytkownicy”)]
    public class Uzytkownik : BusinessEntity
    {
        public Uzytkownik() : base() { }
        public Uzytkownik(Session session) : base(session)
        {
            this.komputery = new XPCollection<Komputer>(session, this, session.GetClassInfo<Uzytkownik>().GetMember(„Komputery”));
        }
        [Persistent(„Login”), Size(120)]
        public string Login { get; set; }
        [Persistent(„CzyAktywny”)]
        public bool CzyAktywny { get; set; }
        [Persistent(„Rola”)]
        public Rola Rola { get; set; }
        private XPCollection<Komputer> komputery;
        [Association(„Uzytkownik-Komputery”), Aggregated]
        [Delayed]
        public XPCollection<Komputer> Komputery
        {
            get { return komputery; }
        }
    }

Na powyższym listingu można dostrzec coś o czym jak dotąd nie wspomniałem – atrybut Association. Jest to atrybut który musi być zaaplikowany na obu końcach relacji one-to-many oraz many-to-many. Mało tego, gdy chcemy dodać kolekcję elementów pewnego typu do jednej z klas zostaniemy zmuszeni do dodania do klasy będącej elementem tej kolekcji wskazania na obiekt zawierający tą kolekcję. Przykładem może być właściwość Uzytkownik w klasie Komputer, której tak naprawdę nie chciałem tam mieć. Nie wygląda to zbyt dobrze, gdyż płacimy często dość wysoką cenę za dostosowywanie modelu do wymagów XPO. Mnie osobiście nie łatwo się z tym pogodzić i zaliczam to do jednych z najpoważniejszych ograniczeń XPO.

Wspomnieć trzeba jeszcze o atrybucie Aggregated, który jest w pewnym stopniu odpowiednikiem właściwości cascade z NHibernate. Nałożenie tego atrybut na właściwość typu referencyjnego lub kolekcję oznaczać będzie, że obiekty te będą zapisywane wraz z zapisem obiektu głównego. W takim wypadku nie będzie wymagany zapis np. poszczególnych elementów kolekcji.

XPO obsługuje także mechanizm lazy-loading, czyli ładowanie właściwości na żądanie przy pierwszym dostępnie do nich. Zachowanie XPO w tym względzie jest jednak zupełnie odmienne od tego które znamy z Entity Framework. W XPO domyślnie lazy-loding jest wyłączony i aby wskazać, że dana właściwość ma być doładowywana na żądanie należy oznaczyć ją atrybutem Delayed.

Bardzo bogato wygląda natomiast pobieranie danych w XPO. Mamy tu do dyspozycji tzw. operatory kryteriów, LINQ czy nawet zapytania SQL. Mnie najbardziej chyba ucieszył mechanizm Linq-to-XPO, chociaż muszę przyznać, że operatory kryteriów – po opanowaniu ich – także dają spore możliwości.

Aby rozpocząć manipulowanie obiektami trzeba zainicjować wspominaną już tutaj niejednokrotnie sesję. Sposobów na to jest kilka, ale jednym z prostszych jest utworzenie DataLayer, a następnie przekazanie go do konstruktora sesji, jak poniżej:

IDataLayer dataLayer = XpoDefault.GetDataLayer(connectionString, AutoCreateOption.DatabaseAndSchema);
Session session = new Session(dataLayer);

Nie napisałem dotąd nic o tworzeniu samej bazy danych oraz tworzeniu obiektów wewnątrz niej jak tabele, klucze główne, klucze obce, itp. Podczas tworzenia obiektu dataLayer, XPO pozwala nam określić jego zachowanie w kwestii automatycznego tworzenia obiektów w bazie danych.

Zachowanie XPO wymuszamy z pomocą typu wyliczeniowego AutoCreateOption:

DatabaseAndSchema – tworzy bazę danych oraz pełny schemat bazy nawet z kluczami obcymi

None – nie robi nic, tzn. nie tworzy automatycznie bazy danych, ani żadnych obiektów w niej

SchemaOnly – tworzy tylko schemat w bazie danych, ale tylko jeśli baza istnieje

SchemaAlreadyExists – nie weryfikuje schematu bazy pod kątem zgodności z aktualnym stanem obiektów

W sytuacji gdy całą pracę związaną z tworzeniem schematu bazy danych przerzucimy na XPO to warto zaznaczyć, że XPO całkiem sprawnie radzi sobie z modyfikacjami schematu na podstawie wprowadzonych zmian w modelu klas.

Pełny projekt prostej aplikacji konsolowej na której bazuje ten opis wykonanej w C# dla VS 2012 można pobrać stąd.

Podsumowując moje odczucia co do XPO to na pewno do zalet zaliczył bym prostotę tworzenia mapowania poprzez atrybuty, mnogość sposobów pobierania danych oraz dobry mechanizm tworzenia i aktualizacji schematu bazy danych. Główne wady to obsługa kolekcji i podejście session-less (trochę jakby oszukane).

Względnie nieduża popularność XPO jako ORM-a wynikać może właśnie z tych kilku ale jednak znaczących wad. Poza tym odnoszę wrażenie, że producent traktuje XPO jako dodatek do całej reszty swoich produktów, a nie główny produkt który chce sprzedawać.

Po bliższym zapoznaniu się z XPO sami musicie zdecydować czy jest to ORM na którym możecie oprzeć Wasz następny projekt. Napewno jest warty zainteresowania, ale być może sprawdzi się tylko w pewnej specyficznej grupie projektów.

Blog na WordPress.com.