22 lutego 2012, 23:35 dario Komentarze (0)

Rozszerzanie IRepository

W poprzedniej notce pokazałem jak przy pomocy bardzo prostego interfejsu IRepository oraz QueryObject można zrealizować warstwę dostępu do danych. W zalezności od wielkości projektu może się okazać, że ciut brakuje funkcjonalności. Co zatem robić?

Z założenia nie chcemy już więcej zmieniać naszego interfejsu. Jak więc przykładowo wykonać operację pobrania rekordu z bazy i ustanowienia na nim blokady? W bazowej implementacji nie mamy takiej metody... i nie będziemy jej dodawać do interfejsu. Na szczęście można go łatwo rozszerzyć definiując metody rozszerzające (Extension Methods).

Powyższy problem można rozwiązać nastepująco:

 

public static class RepositoryExtensions
{
    public static T GetWithLock<T>(this IRepository<T> r, object id)
    {
        return r.Execute(new GetWithLockQuery<T>(id));
    }

    private class GetWithLockQuery<T> : IQuery<T, T> where T : class
    {
        private readonly object _id;

        public GetWithLockQuery(object id)
        {
            _id = id;
        }

        public T Build(ISession session)
        {
            return session.Get<T>(_id, LockMode.Upgrade);
        }
    }
}

 

Przykład użycia:

 

IRepository<User> repo...
var user = repo.GetWithLock(5);

Powyżej utworzyliśmy metodę rozszerzającą funkcjonalność naszego repozytorium o możliwość pobrania rekordu z jednoczesnym założeniem blokady. W ten sam sposób możemy dodawać kolejne potrzebne rozszerzenia w zależności od potrzeb w projekcie. Jak widać daje to nieograniczone możliwości. A nasz interface jest nadal prosty i... elastyczny.

 

Inny przykład? A jak bym chciał sobie w dowolnym miejscu w kodzie pisać zapytania korzystając z NHibernate.Linq? Nic łatwiejszego. :)

Dodajemy metodę rozszerzającą, która zwróci nam IQueryable<T> i po kłopocie. Oto przykład:

 

public static IQueryable<T> Queryable<T>(this IRepository<T> r) where T : class
{
    return r.Execute(new QueryableQuery<T>());
}

private class QueryableQuery<T> : IQuery<T, IQueryable<T>> where T : class
{
    public IQueryable<T> Build(ISession session)
    {
        return session.Query<T>();
    }
}

 

To w sumie by było na tyle w tym temacie. :)

Tagi:

NHibernate

13 lutego 2012, 21:44 dario Komentarze (0)

QueryObject Pattern i bardzo proste IRepository<T>

W poprzednim wpisie chciałem pokazać jak w dłuższej perspektywie opasłe obiekty repozytoriów są mało użyteczne i nie spełniają swoich teoretycznych założeń. Mimo to jednak postanowiłem zostawić w swojej infrastrukturze interfejs IRepository<T>, który w przedstawionej poniżej formie jest tak naprawdę fasadą dla ISession. NHibernate.ISession jaki znamy to bardzo duża klasa z wieloma metodami, które bardzo rzadko używamy, a niektóre wręcz wcale. Po co mają mi się "plątać" w kodzie?

public interface IRepository<TEntity>
{
    /// <summary>
    /// Get entity from data store.
    /// </summary>
    /// <param name="id">Identifier</param>
    /// <returns>Entity retrieved from data store.</returns>
    TEntity Get(object id);

    /// <summary>
    /// Add entity to data store.
    /// </summary>
    /// <param name="entity">Entity to add.</param>
    /// <returns>The same entity with filled identifier property.</returns>
    TEntity Add(TEntity entity);

    /// <summary>
    /// Delete entity from data store.
    /// </summary>
    /// <param name="entity">Entity to be deleted.</param>
    void Delete(TEntity entity);

    /// <summary>
    /// Find single entity in data store using given expression.
    /// </summary>
    /// <param name="expression">Expression used to find single entity.</param>
    /// <returns>Single entity.</returns>
    TEntity Single(Expression<Func<TEntity, bool>> expression);

    /// <summary>
    /// Find list of entities in data store using given expression.
    /// </summary>
    /// <param name="expression">Expression used to find entities.</param>
    /// <returns>List of entities.</returns>
    IEnumerable<TEntity> List(Expression<Func<TEntity, bool>> expression);

    /// <summary>
    /// Execute query and return defined type of result.
    /// </summary>
    /// <typeparam name="TResult">Result defined in query.</typeparam>
    /// <param name="query">Query to execute.</param>
    /// <returns>Result defined in query.</returns>
    TResult Execute<TResult>(IQuery<TEntity, TResult> query);
}

Jak widać interface jest bardzo prosty i łatwo rozszerzalny. Zdefiniujmy sobie zatem jeszcze jeden brakujący interface IQuery<,>.

public interface IQuery<TEntity, TResult>
{
    TResult Build(ISession session);
}

Uchhhh... Skomplikowany... ;) Żarcik, ale do rzeczy.

IRepository<T> posiada kilka prostych metod dających możliwość pobrania obiektu z bazy, usunięcia, czy dodania nowego. Oprócz tego można utworzyć na szybko nieskomplikowane zapytanie posługując się metodami Single czy List. Przykład prostego zapytania, które wyszukuje użytkownika z podanym adresem email:

IRepository<User> repo....
User u = repo.Single(x => x.Email == "developer@dario-g.com");

Podobnie można skontruować zapytanie zwracające listę używając metody List. Jak już jesteśmy przy tworzeniu zapytań to wróćmy zatem do sposobu ich zapisu i wykonywania.

Metoda Execute służy właśnie do tego, aby wykonywać zapytania zdefiniowane w osobnych obiektach QueryObject. Zdefiniujmy sobie zatem jakieś zapytanie.

namespace Domain.Security.Queries
{
    public class FindActiveUserByEmailQuery : IQuery<User, User>
    {
        public string Email { get; set; }

        public User Build(ISession session)
        {
            return session.QueryOver<User>()
                .Fetch(x => x.Roles)
                .Where(x => x.Email == Email)
                .And(x => x.IsActive == true)
                .SingleOrDefault();
        }
    }
}

Zapytanie nie jest trudne do zrozumienia, ale bardzo ładnie pokazuje w jaki sposób się tworzy obiekt Query. Widać jak na dłoni, że rezultatem takiego zapytania może być dowolna struktura. Może to być pojedynczy obiekt jak w przykładzie powyżej, lista IList<User>, czy także zwykła tablica obiektów User[]. Co więcej. Nic nie stoi na przeszkodzie, aby zapytanie było skonstruowane przez:

  • session.Query<> (NHibernate.Linq)
  • session.CreateCriteria
  • session.CreateQuery
  • session.CreateSQLQuery

Specjalnie zostawiłem klauzulę namespace. Chciałem pokazać, że wszystkie obiekty zapytań możemy sobie ładnie pogrupować, aby łatwiej było je odnaleźć podczas pisania kodu. To jeszcze dla uzupełnienia przykład jak wywołać takie zapytanie:

var query = new FindActiveUserByEmailQuery() { Email == "developer@dario-g.com" };
var user = repo.Execute(query);

Można spokojnie pogodzić ze sobą te dwa wzorce i znacznie ułatwić sobie pracę. Ja już zauważyłem, że mam większą swobodę, kolejne funkcjonalności robi mi się szybciej, a struktura projektu jest bardziej przejrzysta.

W kolejnym wpisie o tym co jeszcze można dodatkowo z IQuery<,> zrobić. :)

Tagi:

NHibernate

9 lutego 2012, 12:18 dario Komentarze (2)

Używać czy nie używać Respository Pattern?

O to jest pytanie. Używać czy nie używać Repository Pattern? Ostatnio troszkę się nad tym zastanawiałem i doszedłem do solidnego wniosku: tak i nie. :)

Wiadomo, że ktoś zawsze powie: "to zależy". A ja mu przytaknę. W malutkim projekcie nie ma sensu. W większym? Chcąc być purystą powinienem zrealizować cały dostęp do danych poprzez wartwę realizowaną na bazie Repository Pattern. Czy to ma sens? Co jeśli chcę tylko wyciągnąć imię i nazwisko ostatnio dodanego użytkownika? Czy na prawdę muszę pisać te wszystkie interfejsy, implementacje dla tak prostego zapytania, którego testowanie ma nawet znikomy sens? Czy repozytoria z szeregiem metod w stylu:

  • FindBy
  • FindByEmail
  • FindByNIP
  • FindByNIPWithSpecialSomething

naprawdę są tworem wielokrotnego użytku? Z doświadczenia swojego i nie tylko wiem, że dana metoda bardzo rzadko jest wykorzystywana w więcej niż jednym miejscu w aplikacji.

Przeszukując czeluścia internetu dokopałem się do kilku fajnych spostrzeżeń. Podstawowym była zmiana podejścia samego Ayende co do stosowania IRepository<T>. Wcześniejsze moje doświadczenie w głównej mierze opiera się na tym co publikował Ayende, a to właśnie w publikowanym przez Niego Rhino.Commons znalazłem dosyć rozbudowany "Repository Pattern". Dosyć opasły jak teraz widzę, ale wcześniej mi to także nie przeszkadzało.

Repository Pattern ma teoretycznie pozwolić na odseparowanie się od warstwy dostępu do danych. Co więcej niektóre implementacje znalezione w necie mają pozwolić na "łatwą podmianę ORM'a". Huh...

Osobiście używam NHibernate od kilku lat i jak narazie nie widzę alternatywnego ORM'a, który spełniał by moje oczekiwania. Nie zamierzam podmieniać go "w locie". NHibernate oprócz generatora SQLa (bazowa funkcjonalność) posiada wiele innych funkcjonalności (zalet), od których nie powinno się odseparowywać.

Bardzo fajnie odpowiedział Procent na zadane pytanie w serwisie devpytania: "Repository pattern - czy używać?". Podoba mi się to podejście z jednym małym wyjątkiem. Chciałbym zachować bardzo minimalny poziom abstrakcji nad samą implementacją ISession. Dlaczego? Otóż obejrzyjcie definicję tego interfejsu. Jest... co tu dużo mówić... WIELKA. Większość metod jest niepotrzebna w normalnej pracy. Po co mi taka lista w IntelliSense? Ja potrzebuję zaledwie kilku metod: zbuduj querę, dodaj nowy obiekt, usuń obiekt, pobierz obiekt. Resztą opiekuje sie moja infrastruktura.

I tu dochodzę do swoich wniosków.

Po pierwsze nie chcę całkowitej separacji od wszystkich dobrodziejstw, które oferuje mi NHibernate. NHibernate to jest moja warstwa dostępu do danych i chcę do niej łatwo sięgać kiedy tylko potrzebuję bez dodatkowych i zbędnych warstw.

Po drugie mimo wszystko postanowiłem, że zostawię sobie generyczny Repository<T>, który będzie służył mi jedynie do tego, aby ukryć implementację opasłego ISession. Dodatkowo pozwoli mi na łatwie dodanie specyficznych funkcjonalności w mojej infrastrukturze.

No dobra, a co z zapytaniami, które już tak trywialne (jak opisany wyżej przykład) nie są? Dobrze by było, aby ich nie rozrzucać w kodzie gdzie popadnie. Tu z pomocą przychodzi Query Object Pattern.

... ale o tym w kolejnym wpisie. :)

Tagi:

NHibernate

8 marca 2011, 23:07 dario Komentarze (0)

FluentNHibernate i konwencja PrimaryKey

Od pewnego czasu zamiast stosowania mapowań za pomocą plików XML zacząłem używać FluentNHibernate. Mimo początkowego sceptycznego podejścia przełamałem się, a dobitnie nastąpiło to wtedy, kiedy zacząłem używać konwencji.

Jedną z fajniejszych, która odrazu spowodowała pozytywne nastawienie do FNH jest konwencja IIdConvention. Pozwala na zautomatyzowanie mapowania klucza głównego. Dlaczego? Od zawsze stosuję klucze pojedyncze oparte na typie int lub long, gdzie wartość jest wyliczana algorytmem hilo. Otóż nie bardzo mi się podoba kiedy NHibernate niezależnie od typu obiektu nadawał im wartość z tej samej puli, zaczynając od jakiejś z góry nieustalonej liczby.

Na początku w tabeli hibernate_unique_key definiowałem dla każdego typu osobną kolumnę, gdzie był trzymany parametr next_hi, ale to była utopia. Później znalazłem opcję, że można doklejać własny WHERE do zapytania odpytującego tabelę hibernate_unique_key. Niestety tworzenie pliku mapowania pochłaniało kilka linii na samo skonfigurowanie klucza głównego. Milion możliwośći na błąd...

Dzięki FNH i konwencjom mam spokój i o nic nie muszę się martwić. Dodatkowo każdy typ posiada swoją pulę w wierszach. Struktura tabeli hibernate_unique_key jest następująca:

CREATE TABLE [dbo].[hibernate_unique_key](
	[entity] [nvarchar](50) NOT NULL,
	[next_hi] [int] NOT NULL,
 CONSTRAINT [PK_hibernate_unique_key] PRIMARY KEY CLUSTERED 
(
	[entity] ASC
)
) ON [PRIMARY]

 

A teraz konwencja:

public class PrimaryKeyConvention : IIdConvention
{
    public bool Accept(IIdentityInstance id)
    {
        return true;
    }

    public void Apply(IIdentityInstance id)
    {
        id.Column("ID");
        id.UnsavedValue("0");
        id.GeneratedBy
            .HiLo("20", x => 
            {
                x.AddParam("where", string.Format("entity = '{0}'", id.EntityType.Name));
            });
    }
}

... i już nie muszę klepać tych opasłych linii kodu dla każdej klasy. Podłączam konwencję i robi się samo :)

Ok, no prawie. Jedyne co muszę zrobić to wraz z nowym typem muszę dodać kolejny wiersz do tabeli hibernate_unique_key, gdzie w kolumnie entity znajdzie się nazwa typu obiektu.

PS
Muszę pomyśleć nad jakimś automatycznym uzupełnianiu tej tabeli przy starcie aplikacji :)


Tagi:

NHibernate

24 września 2009, 00:42 dario Komentarze (0)

HiLo - błąd NHibernate "error performing isolated work"

Od roku piszę projekty posługując się NHibernate. A tu dopiero mój pierwszy post o tym super narządku :) Post tym bardziej niefortunny, gdyż dotyczy błędu na jaki można się niechący natknąć i stracić dobrą godzinę na jego rozwiązanie. NHibernate pozwala na definiowanie różnego typu generatory wartości klucza głównego. Oprócz generatorów natywnych, czyli wartości generowanych przez bazy danych są także takie jak: hilo, guid, guid.comb....

W swoich projektach wykorzystuję hilo, który generuje wartość typu int. W aplikacjach webowych lepiej wygląda taki int w URLu niż GUID. :) Standardowo NHibernate do wygenerowania wartości używa prostej, skromnej tabelki:

create table hibernate_unique_key (
    next_hi INT 
)

Możemy także użyć własnej specjalnej tabeli do wygenerowywania wartości dla wybranych obiektów.

<id name="ID" column="Id">
    <generator class="hilo">
        <param name="table">EntityIdentifiers</param>
        <param name="column">DocumentItemID</param>
        <param name="max_lo">10</param>
    </generator>           
</id>

Dzięki temu mamy zdefiniowany dla naszego obiektu własny licznik generowanych wartości. Bardzo przydatne kiedy obiekt danego typu rotuje w aplikacji bardzo często. Mój błąd pojawił się wtedy, gdy utworzyłem kolejny obiekt, który korzysta z tej tabeli i potrzebował kolejnej własnej kolumny. Utworzyłem ją, ale strzeliłem literówkę. Za żadne skarby nie wiedziałem co jest grane. NHibernate się zepsuł. :/ Dopiero po godzinie sprawdzania wszystkiego pokolei doszedłem do właściwej przyczyny błędu.

Mało mówiący komunikat błędu zmylił mnie od samego poczatku. StackTrace zaprowadził mnie do generatorów, ale ja poprostu nie widziałem tej literówki. Teraz na takie przypadki piszę specjalne testy. :)

Tagi:

NHibernate

O autorze

Dariusz Gil - projektant i programista aplikacji internetowych budowanych na platformie Microsoft w technologii ASP.NET (C#) oraz MS SQL Server. Obecnie właściciel (narazie :)) jednoosobowej firmy Softio.

Filtruj używając APML