Temat: Repozytorium encji + RepoLocator - jak to zrobić lepiej?

Na potrzeby pisanego prototypu przygotowuję sobie "środowisko" pracy, czyli strukturę projektu. Zatrzymałem się dla przemyślenia przy kwestiach zw. z modelem. Tej nocy :) zaimplementowałem repozytorium encji i ich locatora.

W przykładach mam typowe "podręcznikowe" encje: Employee, Store, Product, ale oczywiście w "realu" są inne i jest ich sporo więcej :) i dlatego chcę zautomatyzować tworzenie repozytoriów. Automatyczne tworzenie i dodawanie do locatora dotyczy "standardowych" encji, dla których wystarczy mi standardowy zestaw operacji z IRepository<T>. Oczywiście są także encje bardziej złożone i dotyczące ich repozytoria mają oferować dodatkowe funkcje. Wtedy trzeba je napisać ręcznie (dziedziczą po IRepository<T>) i trzeba je dodać ręcznie do kolekcji w locatorze.

Działa mi to poprawnie, natomiast to co skleciłem coś mi się wydaje przekombinowane i trochę "nie tego"... Czy mógłbym Was prosić o rzucenie okiem na całość i wyrażenie swoich sugestii (konstruktywnie) w zakresie ulepszenia tego rozwiązania, propozycji wykorzystania innych konstrukcji? Od dłuższego czasu (niedługo rok) praktycznie nie programuję na poważnie, więc - wstyd się przyznać - chwilowo nie rozwijam w tym kierunku no i mam trochę zaległości w nowościach (wiem, że są gotowe rozwiązania do takich zagadnień, np. Autofac).

1. Przykładowa encja
public class Employee : IModel
{
public virtual int Id { get; private set; }
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual Store Store { get; set; }
}


IModel jest tylko dla potrzeb wyszukiwania typu w pakiecie i jest pusty. Dla tych encji, które implementują IModel, będą utworzone i dodane standardowe repozytoria.
public interface IModel
{
}


2. Repozytorium.
a. Pusty interfejs dla potrzeb locatora
 public interface IRepository
{
}


b. Standardowy interfejs CRUD
public interface IRepository<T> : IRepository
{
IList<T> GetByExample(T example);
... i takie tam inne
void Add(T item);
void Remove(T item);
void Update(T item);
}


c. Fabryka repozytoriów dla każdej z encji
 public class RepositoryFactory<T> : IRepository<T>, IRepository
{
IDataContext dataContext;

public RepositoryFactory(IDataContext dataContext)
{
this.dataContext = dataContext;
}

public IList<T> GetByExample(T example)
{
IEnumerable<T> result = this.dataContext.GetByExample(example, typeof(T)).Cast<T>();

return result.ToList<T>();
}

..... etc.
}


d. Interfejs IDataContext
 public interface IDataContext
{
IList GetByExample(object example, Type type);
.... i takie tam
void Add(object item);
void Remove(object item);
void Update(object item);
}


e. Repo locator - zwłaszcza o niego mi chodzi :)
 public class RepositoryLocator
{
private Dictionary<Type, IRepository> repositories;

public RepositoryLocator(IDataContext context)
{
repositories = new Dictionary<Type, IRepository>();

// automatyczne odszukanie wszystkich encji, dla których ma być utworzone standardowe repozytorium
Assembly asm = Assembly.GetCallingAssembly();

IEnumerable<Type> entities = (from t in asm.GetTypes()
where t.GetInterface("IModel") != null
select t
);

foreach (Type m in entities)
{
// zbudowanie obiektu repozytorium.
Type repoFacType = typeof(RepositoryFactory<>);

// "ugenerycznienie" o typ encji
Type finalRepoFacType = repoFacType.MakeGenericType(new Type[1]{m});

object repository = Activator.CreateInstance(finalRepoFacType, new object[] { context });

repositories.Add(m, (IRepository) repository);
}
}

// wersja dla zwykłych repozytoriów - wskazanie przez typ encji
public IRepository<T> GetRepository<T>() where T : IModel // tylko dla std. encji
{
if (repositories.ContainsKey(typeof(T)))
return repositories[typeof(T)] as IRepository<T>;
else
return null;
}

// wersja dla osobnych repozytoriów - wskazanie przez typ repozytorium
public T GetRepositoryEx<T>() where T : class, IRepositoryEx // tylko dla niestd. repoz.
{
if (typeof(T).GetInterface("IRepositoryEx") != null)
{
Type t = ((IRepositoryEx)(Activator.CreateInstance(typeof(T), new object[] { null }))).EntityType;
if (repositories.ContainsKey(t))
return repositories[t] as T;
else
return null;
}
else
{
return null;
}
}


f. DataProvidera oszczędzę => NHibernate dla SQLite'a :)

g. Wykorzystanie w kodzie:
DataProvider dataProvider = new DataProvider(this.session);

RepositoryLocator repoLocator = new RepositoryLocator(dataProvider );
IList<Employee> employees =
repoLocator.GetRepository<Employee>().GetByExample(example);

albo
repoLocator.GetRepository<Employee>().Add(Janusz);


h. Specjalne repozytoria muszę napisać ręcznie

Najpierw specjalny interfejs dla nich dla potrzeb locatora:
public interface IRepositoryEx
{
Type EntityType { get; } // typ skojarzonej z nim encji
}


Potem interfejs tego repozytorium
 public interface IProductRepository : IRepository<Product>
{
IList<Product> GetByMoreSophisticatedExample(Product example, string somethingStupid);
}


Wreszcie klasa repozytorium
 public class ProductRepository : IRepositoryEx, IProductRepository
{
IDataContext dataContext;

public ProductRepository(IDataContext dataContext)
{
this.dataContext = dataContext;
}

public IList<Product> GetByMoreSophisticatedExample(Product example, string somethingStupid)
{
......... super-mega-wyrafinowane wyszukiwanie :]

return result.ToList<Product>();
}
....

public Type EntityType
{
get { return typeof(Product); }
}
}


dodać do kolekcji:
repoLocator.AddNewRepository(new ProductRepository(dataProvider), typeof(Product));


i wykorzystać potem:
IList<Product> products = ((ProductRepository)repoLocator.GetRepository<Product>()).GetByMoreSophisticatedExample(p, "coś tam, coś tam");

albo

IList<Product> products = repoLocator.GetRepositoryEx<ProductRepository>().GetByMoreSophisticatedExample(p, "cośtam");


PS: kod do nauki, nie ma obsługi błędów.
PS: pisane z przerwami między 1 a 4:40 dziś w nocy, więc bądźcie wyrozumiali :)Adrian Olszewski edytował(a) ten post dnia 23.07.10 o godzinie 04:48

konto usunięte

Temat: Repozytorium encji + RepoLocator - jak to zrobić lepiej?

Czy w prawdziwym projekcie nie będzie to tak, że większość repozytoriów z czasem będzie bardziej złożona i przekształci się w IRepositoryEx? Wtedy istnienie lokatora (który swoją drogą ogranicza możliwości testowania kodu, przynajmniej w takiej postaci) nie ma raczej sensu. Większość repozytoriów będziesz musiał dodać ręcznie. W swoich projektach, wykorzystuje stare,
dobre fabryki. Aktywnie pomaga mi w tym R# ze swoimi Live Templates.Jarosław D. edytował(a) ten post dnia 23.07.10 o godzinie 07:33

konto usunięte

Temat: Repozytorium encji + RepoLocator - jak to zrobić lepiej?

Taka mała uwaga:

W tym projekcie systemu będziesz zmuszony do zmiany kodu repozytorium z prawie każdym nowym typem zapytania. Czy na pewne tego chcesz?

Uważam, że dużo praktyczniejszym pomysłem byłoby udostępnienie swoich obiektów w postaci IQueriable<T> gdzie nowe kryteria lub sposób wyszukiwania danych będą oznaczały tylko inne zapytanie LINQ.

W taki sposób zapewnisz sobie (moim zdaniem) najwyższy poziom elastyczności warstwy DAL.

Zapytania LINQ jakie będą trafiały do Twoich obiektów eksponujących poszczególne encje systemu, będziesz mógł interpretować w postaci drzewka Expression a następnie albo przekazywać dalej (najłatwiej, jeśli używasz standardowych dostarczonych z .NET źródeł danych takich jak EF, WCF Data Services itd...) albo z nich budować wewnętrzne zapytania (co ogólnie jest dosyć rzadkim scenariuszem... ale prawdopodobnym, że możesz taki mieć/potrzebować) do swojego źródła danych.

W obecnym dizajnie nie będziesz miał możliwość (a jestem niemalże na 99% pewny, że w każdym systemie w pewnym momencie jego dojrzałości zachodzi taka potrzeba) stworzenia czegoś takiego:

from entity in DAL.Encje
where entity.TimeStamp > DateTime.Now && entity.Location.StartsWith("cośtam") && entity.Name.Substring(1,4) == "abcd"
select entity;

dodaj do tego scenariusze z grupowaniem, sortowaniem, itd... :)

1 zapytanie : jedna metoda w DAL to imho jest na dłużą metę udręką i dla osoby zajmującą się samą warstwą DAL jak i dla osoby która ten DAL konsumuję i jednocześnie tworzysz niebezpieczne założenie, że zawsze te osoby mają wgląd w swój kod i możliwość zmiany.

Kolejna uwaga:
Do "oznaczania" typów lepiej jest nie używać interfejsów bo to nie jest ich przeznaczenie. Lepszym. praktyczniejszym i dającym więcej możliwości pomysłem byłoby użycie atrybutów który służą właśnie do opisywania typów.

Mam nadzieję, że pomogłem ;)

Temat: Repozytorium encji + RepoLocator - jak to zrobić lepiej?

Dzięki serdeczne za poświęcenie czasu i pomoc :)

Karim, masz rację. Co prawda aplikacja jest bardzo prosta od strony bazodanowej i tak na 80% mógłbym polegać na "wbudowanych zapytaniach", to Twoją sugestię wdrożę dla nauki, ze względu na inne planowane przeze mnie prace.

Trochę mnie przeraziło "ulinqowienia" zapytań, bo na cały prototyp mam razem 3 tygodnie (z czego 1.5 już za mną) ale na szczęście znalazłem... Linq 2 NHibernate (dzięki, nieśmiertelny Ayende :) ).

Co prawda nie widzę, aby od lipca 2009 roku było rozwijane, ale nie mam już czasu. Jeśli natknę się na błąd w bibliotece, będzie kombinował workaround, a może i nawet sam dokonam poprawek (są źródła), jeśli już koniecznie będę musiał. Ci, co odziedziczą ewentualnie po mnie projekt i będą rozwijać prototyp, zmienią sobie najwyżej providera danych, na takiego, dla którego praca z Linq będzie bezproblemowa, np. EF + SQL Server.

Niestety, z uwagi na fakt, że potrzebuję, aby w prototypie była to baza SQLite (świetna to testów, ale i produkcyjnie dla małych projektów), nic innego niż NH mi raczej (poza ADO.NET) nie zostaje, a nie mam czasu ani chęci na pisanie selektów. Ważniejsza jest w tym projekcie ergonomia interfejsu, a na tę mam mało czasu już.

Tak, że siadam do testów, czy to w ogóle działa i pozwala na podstawowe zapytania (zawsze to więcej, niż mam obecnie) i jeśli tak, to ten problem mam z głowy, dzięki jeszcze raz za sugestię, Karim :)

Gdyby ktoś potrzebował - trochę tutoriali:
http://codethatworks.blogspot.com/2009/11/linq-to-nhib...
http://www.tobinharris.com/past/2008/8/17/linq-for-nhi...

PS: jakie to miłe :)
 var result = from c in session.Linq<Person>()
where c.PESEL.StartsWith("81")
group c by c.LastName into g
select new { FirstName = g.Key, Count = g.Count() };


... ale o orderby względem agregatu można pomarzyć (null) :]
I to zarówno poprzez orderby jak i OrderByDescending(p => p.Count). Poza tym let też nie działa...

Na szczęście orderby na polach klasy działa.

EDIT:
Jedyne, co można zrobić z "orderbajem" po agregacie, to...
class A
{
public string name;
public int count;
public a(string name, int count)
{
this.name = name;
this.count = count;
}
}

List<A> tmp = new List<A>();
foreach (var i in result) // result z powyższego LINQa
{
tmp.Add(new A(i.FirstName, i.Count));
}

...i dopiero na takiej liście "zdetaczowanych" obiektów można pracować wykorzystując metody listy generycznej albo LINQ

var sorted = from x in (tmp.AsQueryable<A>())
... co tam jeszcze ewentualnie potrzeba
orderby x.count
select x;


W przypadku niewielkiej liczby obiektów, które zwróci zapytanie i konieczności pracy na grupach, wygodniej będzie zapytać o wszystkie obiekty (ew. zawężone wherem), skopiować je w pętli do listy nowych obiektów i dopiero na niej grupować, sortować, etc. Ale do takich operacji użyję raczej dedykowanej metody z repozytorium, która zrobi co trzeba przez Criteria/UDP/TSQL bez pie...nia się w takie zabawy jak powyżej.

Mam ten zaj... komfort, że dla moich potrzeb to w zupełności wystarczy, ale w poważniejszych zastosowaniach --> Why Linq2NHibernate isn't ready for production useAdrian Olszewski edytował(a) ten post dnia 24.07.10 o godzinie 23:44

konto usunięte

Temat: Repozytorium encji + RepoLocator - jak to zrobić lepiej?

My pleasure ;)

http://relinq.codeplex.com/

To może okazać się bardzo pomocne, chodź nie nie wiem na ile dokładnie zostało przetestowane.Karim Agha edytował(a) ten post dnia 24.07.10 o godzinie 10:59

konto usunięte

Temat: Repozytorium encji + RepoLocator - jak to zrobić lepiej?

Adrian Olszewski:
public class Employee : IModel {
public virtual int Id { get; private set; }
}
lekko OT, ale w jakim celu przewidujesz override na tych properties i w ogóle dziedziczenie z Employee?
Przemysław Czatrowski

Przemysław Czatrowski Engine Programmer,
CD Projekt RED

Temat: Repozytorium encji + RepoLocator - jak to zrobić lepiej?

maciek kański:
Adrian Olszewski:
public class Employee : IModel {
public virtual int Id { get; private set; }
}
lekko OT, ale w jakim celu przewidujesz override na tych properties i w ogóle dziedziczenie z Employee?

Wydaje mi się, że "virtual" zostało dodane na potrzeby NHibernate

Temat: Repozytorium encji + RepoLocator - jak to zrobić lepiej?

maciek kański:
Adrian Olszewski:
public class Employee : IModel {
public virtual int Id { get; private set; }
}
lekko OT, ale w jakim celu przewidujesz override na tych properties i w ogóle dziedziczenie z Employee?

Hej :)
Virtual ze względu na NH (link)
A dziedziczenie po IModel jedynie na potrzeby automatycznego wyszukiwania encji w pakiecie. Równie dobrze można było wyprowadzać wszystkie encje z jakiegoś abstrakcyjnego typu bazowego, np. EntityBase (zawierającego np. tylko ID) i również po nim wyszukiwać. W sumie tak byłoby ładniej - i tak zaraz zrobię :) Pewnie można to zrobić lepiej, ale to działa :)

konto usunięte

Temat: Repozytorium encji + RepoLocator - jak to zrobić lepiej?

Adrian Olszewski:
Virtual ze względu na NH
Dzięki; o ile dobrze bloga zrozumiałem rezygnując z lazy loading można robić normalne klasy sealed, ale to jak pisałem offtopic.

A dziedziczenie po IModel jedynie na potrzeby automatycznego wyszukiwania encji w pakiecie.
Tutaj bym głosował za Karimem aby atrybutu użyć, ale sam M$ przez lata używa takich markerów (najsłynniejszy INamingContainer).

Co do meritum nie mam nic do dodania:)

konto usunięte

Temat: Repozytorium encji + RepoLocator - jak to zrobić lepiej?

Virtual ze względu na NH
Właśnie zapoznałem się ze wsparciem "plain-old" CLR objects w Entity Framework 4.0 i tam się to samo pojawia - aby property obsługiwało lazy loading i/lub change tracking musi być virtual: http://msdn.microsoft.com/en-us/library/dd468057.aspx
Remigiusz Cieślak

Remigiusz Cieślak Software Developer

Temat: Repozytorium encji + RepoLocator - jak to zrobić lepiej?

Adrian Olszewski:
W przykładach mam typowe "podręcznikowe" encje: Employee, Store, Product, ale oczywiście w "realu" są inne i jest ich sporo więcej :) i dlatego chcę zautomatyzować tworzenie repozytoriów. Automatyczne tworzenie i dodawanie do locatora dotyczy "standardowych" encji, dla których wystarczy mi standardowy zestaw operacji z IRepository<T>. Oczywiście są także encje bardziej złożone i dotyczące ich repozytoria mają oferować dodatkowe funkcje. Wtedy trzeba je napisać ręcznie (dziedziczą po IRepository<T>) i trzeba je dodać ręcznie do kolekcji w locatorze.
Zastanowiłbym się tutaj nad sensem takiego uniwersalnego repozytorium. Niemalże pewnym jest, że dla każdego aggregate root'a (wybaczcie angielską nazwę, ale nie znam polskiego odpowiednika) w twojej domenie będziesz musiał zaimplementować dużo bardziej skomplikowaną logikę pobierania i zapisywania obiektów. Przede wszystkim, radziłbym przemyśleć kwestię, które obiekty rzeczywiście potrzebują repozytoriów, a które powinny być obsługiwane razem z innymi obiektami (poleciłbym tutaj książkę Erica Evansa na temat DDD).
Karim A.:
Taka mała uwaga:

W tym projekcie systemu będziesz zmuszony do zmiany kodu repozytorium z prawie każdym nowym typem zapytania. Czy na pewne tego chcesz?

Uważam, że dużo praktyczniejszym pomysłem byłoby udostępnienie swoich obiektów w postaci IQueriable<T> gdzie nowe kryteria lub sposób wyszukiwania danych będą oznaczały tylko inne zapytanie LINQ.

W taki sposób zapewnisz sobie (moim zdaniem) najwyższy poziom elastyczności warstwy DAL.
Podejście takie może rzeczywiście pomóc, ale używane bez umiaru może być równie szkodliwe. Często używane query lepiej wrzucić do repo niż za każdym razem powielać kod zapytania. Oczywiście taka metoda w repo z często używanym query może nadal zwracać IQueryable, aby doprecyzować zapytanie jeśli zajdzie taka potrzeba.
maciek kański:
Virtual ze względu na NH
Właśnie zapoznałem się ze wsparciem "plain-old" CLR objects w Entity Framework 4.0 i tam się to samo pojawia - aby property obsługiwało lazy loading i/lub change tracking musi być virtual: http://msdn.microsoft.com/en-us/library/dd468057.aspx
W przypadku lazy loading chyba nie ma innej opcji niż virtual na property, bo praktycznie wszystkie tego typu rozwiązania bazują na dynamic proxy. Istnieje również możliwość modyfikacji ILa zaraz po skompilowaniu (np. PostSharp), ale to już zdecydowanie mniej wygodne rozwiązanie, gdyż wymaga dodania odpowiednich targetów do definicji builda. Z kolei modyfikacja ILa w trakcie ładowania dll'ki odpada, bo wymaga zdjęcia podpisu z takiej dll.Remigiusz Cieślak edytował(a) ten post dnia 11.08.10 o godzinie 22:28

Temat: Repozytorium encji + RepoLocator - jak to zrobić lepiej?

Remigiusz Cieślak:
Zastanowiłbym się tutaj nad sensem takiego uniwersalnego repozytorium. Niemalże pewnym jest, że dla każdego aggregate root'a (wybaczcie angielską nazwę, ale nie znam polskiego odpowiednika) w twojej domenie będziesz musiał zaimplementować dużo bardziej skomplikowaną logikę pobierania i zapisywania obiektów. Przede wszystkim, radziłbym przemyśleć kwestię, które obiekty rzeczywiście potrzebują repozytoriów, a które powinny być obsługiwane razem z innymi obiektami (poleciłbym tutaj książkę Erica Evansa na temat DDD).

W moim przypadku podejście z uniwersalnymi repo, LINQ i kilkoma parametryzowanymi metodami do agregatów sprawdziło się w 100%. Natomiast zagadnienie pozostaje aktualne na przyszłość, zatem dzięki za wszelkie wskazówki :)
Oczywiście taka metoda w repo z często używanym query może nadal zwracać IQueryable, aby doprecyzować zapytanie jeśli zajdzie taka potrzeba.

Zgadzam się, tak zrobiłem właśnie przy kilku repo, dla których domyślne zapytanie standardowo wymaga podania kilku parametrów, m.in. podania daty i pewnych domyślnych flag.

Dzięki za odzew.



Wyślij zaproszenie do