Wzorzec repozytorium

0

Witam
Piszę sobie aplikacje korzystająca z ORM(EF), głównie wzoruje się na książce ASP.NET MVC 3. Tam autor używa dla każdej tabeli repozytorium.
Np.

public interface IProductRepository
{
    IQueryable<Product> Products { get; }
}

public class EFProductRepository : IProductRepository
{
    private EFDbContext context = new EFDbContext();

    public IQueryable<Product> Products { get { return context.Products; } }
}

class EFDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
}

I tak to wygląda dla każdej tabeli, ale oczywiście w książce nie ma nic jak sobie radzić kiedy potrzeba stosować złączenia. Trochę uciążliwe było by tworzenie nowego repo dla dwóch tabel aby wykonać zapytanie ze złączeniem. Więc z tego powodu chce wrzucić całą bazę danych do jednego repo, ale czy takie coś ma w ogóle sens?

class EFDbContext : DbContext
{
    public DbSet<Network> Networks { get; set; }
    public DbSet<Algorithm> Algorithms { get; set; }
    public DbSet<Collection> Collections { get; set; }
    public DbSet<Set> Sets { get; set; }
    public DbSet<Input> Inputs { get; set; }
    public DbSet<Output> Outputs { get; set; }
}

interface ILynxRepository
{
    EFDbContext Context { get; }
    IQueryable<Algorithm> Algolithms { get; }
    IQueryable<Network> Networks { get; }
    IQueryable<Collection> Collections { get; }
    IQueryable<Set> Sets { get; }
    IQueryable<Input> Inputs { get; }
    IQueryable<Output> Outputs { get; }       
}

class EFLynxRepository : ILynxRepository
{
    EFDbContext context = new EFDbContext();

    public EFDbContext Context { get { return context; } }
    public IQueryable<Algorithm> Algolithms { get { return context.Algorithms; } }
    public IQueryable<Network> Networks { get { return context.Networks; } }
    public IQueryable<Collection> Collections { get { return context.Collections; } }
    public IQueryable<Set> Sets { get { return context.Sets; } }
    public IQueryable<Input> Inputs { get { return context.Inputs } }
    public IQueryable<Output> Outputs { get { return context.Outputs; } }
}

1

Nie, jedno repozytorium dla całej bazy nie ma sensu.

Złączenia zmieniają tylko tyle, że pobierasz jakąś encje nadrzędną z encją podrzędną. Np. Producenta z Towarami, Fakturę z Pozycjami, Książkę z Autorami, itd. Nie tworzy się w tym celu nowych repozytoriów, co najwyżej dopisuje metody: ProducerRepository.GetWithProducts(int id), InvoicesRepository.GetWithPositions(int id), BooksRepository.GetWithAuthors(int[] ids).

0

Może rzeczywiście to źle nazwałem, ale głownie chciałem uzyskać taką minimalną otoczkę na context, dla mnie bardzo wygodne jest robienie zapytań ze złączeniami w kontrolerach. Co myślisz o takim rozwiązaniu? http://www.nogginbox.co.uk/blog/mocking-entity-framework-data-context.
Czytałem o wzorcu Unity of Work, dzięki niemu możemy zrobić aby kilka repo miało swój wspólny context, jest dość skomplikowany, czy warto go zaimplementować ?
Te przykłady co podałeś to będą zwracać nowe obiekty, które nie mają reprezentacji w bazie danych ?

1

Kolega mając na myśli 'złączenia' miał na myśli chyba projekcje (coś jak jawne joiny z wyciągnięciem tylko wymaganych pól z kilku tabel).
Klasyczne repozytorium ukrywa ci to, że operujesz na bazie danych, więc nie powinno zwracać IQueryable, tylko IEnumerable.
Niektórzy jednak sobie zostawiają 'otwartą furtkę' i wystawiają IQueryable, jeżeli ktoś robi to celowo, to też jest ok.
Jeżeli robisz projekcje to musisz sobie stworzyć klasę, która opisze Ci obiekt, który będziesz zwracał. UnitOfWork daje ci to, że operacje na wszystkich twoich repozytoriach zostaną wykonane w jednej transakcji (gdy coś się nie uda wykonać, to nie wykona się nic). Tak naprawdę w Entity Framework samo DbContext to unitofwork, a DbSet'y można traktować jako repozytoria.

1

Faktycznie źle zrozumiałem pytanie, swojego poprzedniego posta pisałem w kontekście eager loading, bo tak zrozumiałem problem ze złączeniami przy używaniu ORMa.

Jeśli natomiast chcemy robić projekcje, to musimy mieć jakieś modele (np. ProductWithProducerModel) i repozytoria do nich.
Trzeba sobie odpowiedzieć tylko na pytanie, jaki to ma sens i czy w ogóle ma jakiś sens.

A co do repozytoriów jeszcze: Wzorzec repozytorium

0

W tym temacie co podałeś, napisałeś że korzystasz bezpośrednio z context, czy mógł byś mi wytłumaczyć jak to robisz ? Czy za pomocą DI wstrzykujesz do kontrolera obiekt context który jest zbindowany z jakimś interfejsem np. IContext?

Czy mniej więcej wygląda to tak, powiem szczerze że nie mogę tego załapać.

public class SetController : Controller
{
    private IContext context;

    public SetController(IContext context)
    {
        this.context = context;
    }

    public ActionResult ListSetDetails(int SetID)
    {
        var size = context.Sets.Where(x => x.SetID == SetID)
        .Select(x => new { InputCount = x.InputCount, OutputCount = x.OutputCount })
        .FirstOrDefault();

        double[] inputs = context.Sets
        .Join(context.Collections, s => s.SetID, c => c.SetID, (s, c) => new { s, c })
        .Join(context.Inputs, sc => sc.c.CollectionID, i => i.CollectionID, (sc, i) => new { sc, i })
        .Where(sci => sci.sc.s.SetID == SetID)
        .Select(sci => sci.i.Value).ToArray();

        double[] outputs = context.Sets
        .Join(context.Collections, s => s.SetID, c => c.SetID, (s, c) => new { s, c })
        .Join(context.Outputs, sc => sc.c.CollectionID, o => o.CollectionID, (sc, o) => new { sc, o })
        .Where(sco => sco.sc.s.SetID == SetID)
        .Select(sco => sco.o.Value).ToArray();

        return View("ListSetDetails", new ListDetailsModel() {
            InputCount = size.InputCount,
            OutputCount = size.OutputCount,
            Inputs = inputs,
            Outputs = outputs
        });
    }
}

1
Bumcykowy napisał(a):

W tym temacie co podałeś, napisałeś że korzystasz bezpośrednio z context, czy mógł byś mi wytłumaczyć jak to robisz ? Czy za pomocą DI wstrzykujesz do kontrolera obiekt context który jest zbindowany z jakimś interfejsem np. IContext?

Chodziło mi o klasę reprezentującą model EF, czyli dziedziczącą po ObjectContext. Nie potrzeba do tego żadnego interfejsu, w DI można przecież zarejestrować obiekt konkretnej klasy (zresztą, można DI w ogóle nie używać).
I nie robię tego w kontrolerach, tylko w serwisach. Ja bardzo nie lubię mieszać obsługi www ze źródłem danych w jednej klasie, no ale niby w "prostych przypadkach" można.

0

Mając na myśli klasę reprezentującą model EF, chodzi ci o takie coś(wyczytałem że DbContext zastąpił ObjectContext w EF > 4.1)?

class EFDbContext : DbContext
{
    public DbSet<Set> Sets { get; set; }
    public DbSet<Collection> Collections { get; set; }
    public DbSet<Input> Inputs { get; set; }
    public DbSet<Output> Outputs { get; set; }
}

W drugim temacie napisałeś że zwracasz IEnumerable, zamiast Queryable (czyli także nie zwracasz DbSet), z tego co wiem powoduje to pobranie całej tabeli i działanie na niej już w pamięci, czy to jest prawda? (http://patryknet.blogspot.com/2010/08/roznice-miedzy-ienumerable-i-iqueryable.html)

Mając na myśli te proste przypadki, w ogóle warto wstrzykiwać ten context? Czy nie lepiej go utworzyć statycznie? Czy praca z tym context wygląda tak jak pokazałem wcześniej, tylko bez interfejsu.

1
Bumcykowy napisał(a):

Mając na myśli klasę reprezentującą model EF, chodzi ci o takie coś(wyczytałem że DbContext zastąpił ObjectContext w EF > 4.1)?

Możliwe, że zastąpił, rzadko z EF korzystam.
I tak, chodzi mi o klasę zawierającą zbiór kolekcji dla klas odpowiadających tabelom z bazy oraz metodę SaveChanges.

z tego co wiem powoduje to pobranie całej tabeli i działanie na niej już w pamięci, czy to jest prawda?

Jeśli pobierzesz całą tabelę, to tak. Ja raczej pobieram tylko potrzebne rekordy.

IQueryable służy do budowy zapytań do bazy, po jego wykonaniu (np. poprzez wywołanie metody ToList) mamy już kolekcję IEnumerable, która dalej może być przetwarzana w aplikacji.

Mając na myśli te proste przypadki, w ogóle warto wstrzykiwać ten context? Czy nie lepiej go utworzyć statycznie?

Wstrzykiwać nie trzeba, można go po prostu utworzyć jako pole klasy, która go potrzebuje. Ale na pewno nie statycznie (nie static), bo to zapewne może spowodować problemy wydajnościowe.

Czy praca z tym context wygląda tak jak pokazałem wcześniej, tylko bez interfejsu.

Mniej więcej tak.

0

Rozumiem że to Enumerable zwracasz dopiero z serwisu, po odpytaniu DbSet, dzięki czemu zwracasz to czego potrzebujesz?

somekind napisał(a):

Nie potrzeba do tego żadnego interfejsu, w DI można przecież zarejestrować obiekt konkretnej klasy (zresztą, można DI w ogóle nie używać).

W przypadku gdy nie robimy testów jednostkowych nie było by to problemem, a jak to jest kiedy chcemy używać mockowania? Czy wtedy taki interfejs prosty by się nie przydał ? Czy może jest lepszy patent na testowanie context?

class ILynxContext
{
    DbSet<Set> Sets { get; set; }
    DbSet<Collection> Collections { get; set; }
    DbSet<Input> Inputs { get; set; }
    DbSet<Output> Outputs { get; set; }
    int SaveChanges();
}
1

Ja teraz dla szybkiego projektu stosowałem tak:

    public interface IUnitOfWork : IDisposable
    {
        void Commit();
        IDbSet<T> RepositoryOf<T>() where T : BaseEntity;
    }
    public class UnitOfWork : IUnitOfWork
    {
        IDbContext dbcontext;
        public UnitOfWork(IDbContext dbcontext)
        {
            this.dbcontext = dbcontext;
        }
        public void Commit()
        {
            dbcontext.SaveChanges();
        }

        public System.Data.Entity.IDbSet<T> RepositoryOf<T>() where T : Domain.BaseEntity
        {
            return dbcontext.Set<T>();
        }

        public void Dispose()
        {
            dbcontext.Dispose();
        }
    }

po BaseEntity dziedziczy każda encja.

0

@micc
Bardzo mi się podoba twoje rozwiązanie, tylko mam parę problemów. Mianowicie, nie wiem jak utworzyć mocka do tego, w ogóle nie idzie zbudować DbSet<T>. Z skąd wziąłeś IDbContext(teraz daje DbContext)? Czy BaseEntity to twoja klasa? Potrzebuje zrobić usuwanie wielu wierszy z np. Collection, gdzie mogę dodać metodę która to zrobi(potrzebuje skorzystać z (.Database.ExecuteSqlCommand());

1
Bumcykowy napisał(a):

Rozumiem że to Enumerable zwracasz dopiero z serwisu, po odpytaniu DbSet, dzięki czemu zwracasz to czego potrzebujesz?

Tak.

W przypadku gdy nie robimy testów jednostkowych nie było by to problemem, a jak to jest kiedy chcemy używać mockowania? Czy wtedy taki interfejs prosty by się nie przydał ? Czy może jest lepszy patent na testowanie context?

Wszelkie obliczenia i algorytmy wykonuję na już pobranych z bazy encjach (albo na jakichś innych klasach), więc testy odpowiedzialnych za to metod żadnego związku z DbContext nie mają.
Gdy zaś chcę testować jakiś pełen proces, łącznie z operacjami na bazie, to jest to wtedy test integracyjny, więc normalnie wypełniam bazę testowymi danymi, a potem test je przetwarza. W żadnym wypadku nie muszę DbContextu mockować.

0

Dzięki za pomoc, już wiem to co chciałem. Na razie korzystam z rozwiązania @micc, bo nie orientuje się w tych serwisach, ale na pewno później sobie to sprawdzę.

2

@Bumcykowy, ale w czym się tu orientować? :) Po prostu zamiast wpychać operacje na bazie w metody kontrolerów, przenosi się je do innych klas, z których kontrolery korzystają. Dzięki temu trzymamy się zasad SRP i SoC.

0

@somekind Z twojej wypowiedzi wnioskuje że to jest proste, ale nie mam pojęcia jak to ugryźć, szukałem trochę na ten temat ale nic konkretnego nie znalazłem. Byłbym wdzięczny jak byś mógł mnie jakoś naprowadzić, bądź dać link do fajnego opisu.

0

Wszelkie obliczenia i algorytmy wykonuję na już pobranych z bazy encjach (albo na jakichś innych klasach), więc testy odpowiedzialnych za to metod żadnego związku z DbContext nie mają.

Czyli, jeśli dobrze rozumiem, robisz coś takiego?

    public class MyContext : DbContext
    {
        public DbSet<Zamowienia> Zamowienia { get; set; }
    }
    public class Service
    {
        public IEnumerable FiltrujZamowienia(Filter filter)
        {
            return DB.Zamowienia.Where(el => el.El1 == filter.El1).ToList();
        }

        public void RobObliczenia(Filter filter)
        {
            var listaZamowien = FiltrujZamowienia(filter);
            //jakies obliczenia
        }
    }
1

Polecam ci poczytać Professional ASP.NET Design Patterns - S.Millett.
W prosty sposób, z praktycznymi przykładami wytłumaczone są najczęściej używane wzorce projektowe w aplikacjach webowych.

0

Rozszerzając pytanie @siararadek. Czy każdy kontroler ma swój własny serwis? Bo teraz zacząłem to pisać i jak by był jeden ogólny to by w ciul metod miał.

1
siararadek napisał(a):

Czyli, jeśli dobrze rozumiem, robisz coś takiego?

Nie, raczej tak:

public class ZamówieniaService
{
    DbContext context;
    ZamowieniaCalculationService calculationService;
    ZamowieniaImportService importService;

        // metoda wykorzystwana do uzupełniania jakiegoś Grida
        public IEnumerable PobierzOtwarteZamowienia(PagingParams @params)
        {
            var zamowienia = context.Zamowienia.Where(el => el.Status == ZamowienieStatus.Otwarty)
                 .Skip(@params.StartRow).Take(@params.PageSize).ToList();         
        }

        // metoda robiąca coś po kliknięciu w jakiś guzik
        public OperationResult PrzetworzOtwarteZamowienia(ParametryPrzetwarzaniaZamowien parametry)
        {
            var zamowienia = context.Zamowienia.Where(el => el.Status == ZamowienieStatus.Otwarty).ToList();

            foreach(var z in zamowienia)
            {
                 calculationService.ObliczCos(z); 
            }

            context.SaveChanges();

            return result;
        }       

         // wczytuje zamówienia i zapisuje do bazy
        public OperationResult ImportujZamowienia(string path, FileType fileType)
        {
             var zamowienia = importService.Import(path, fileType);
            foreach(var z in zamowienia)
            {
                 context.Zamowienia.Add(zamowienia); 
            }

            context.SaveChanges();
        }
    }

Oczywiście do tego obsługa błędów, logowanie do pliku, zwracanie użytkownikowi jakichś sensownych informacji, itp. (No i cały kod po angielsku. ;))

Bumcykowy napisał(a):

Rozszerzając pytanie @siararadek. Czy każdy kontroler ma swój własny serwis? Bo teraz zacząłem to pisać i jak by był jeden ogólny to by w ciul metod miał.

Oczywiście, że ma własny serwis. I to czasem nie jeden, wszystko zależy od skomplikowania i potrzeb. Ponadto są serwisy pomocnicze, jak ten do importu plików z przykładu powyżej.

Kontroler odpowiada tylko za komunikację z użytkownikiem, czyli uzupełnianie widoków danymi i odbieranie poleceń. Wszystkie operacje "biznesowe" przekazuje do serwisów, które wykonują konkretne obliczenia, operacje na bazie danych, i przetwarzają encje na viewmodele (bo kontrolery i widoki operują na viewmodelach). Serwisy współpracują ze sobą w celu wykonania tych operacji.

Dzięki temu mamy separację między bazą, a GUI, większą testowalność, no i takie serwisy nie muszą być elementem aplikacji webowej, łatwo z nich zrobić np. webserwisy.

No i w sumie nie bardzo widzę miejsca na Repozytorium przy moim podejściu, ale co kto lubi. :)

0

Bardzo fajnie to opisałeś, już to sobie zaimplementowałem. Na razie ciężko mi idzie podział tych serwisów, ale z czasem pewnie to przyjdzie. Dodałem jedynie interfejs po to aby ten serwis dało się fajnie mockować.

0

A powiedz @somekind, jak testujesz te metody? Mockujesz sobie context.Zamowienia?

0

calculationService.ObliczCos(z); testuję po prostu przekazując obiekt Zamowienia, tu nie trzeba nic mockować.
Testowanie czegoś takiego jak ZamówieniaService.PrzetworzOtwarteZamowienia niekoniecznie ma sens, ale jeśli koniecznie chcę, to tak jak pisałem wcześniej, robię to integracyjnie. Wstawiam testowe dane do bazy, wywołuję metodę, i sprawdzam efekty.

0

Nie wiem czy to jeszcze podchodzi pod ten temat, ale pal sześć.

Wstawiam testowe dane do bazy, wywołuję metodę, i sprawdzam efekty.

  1. Wprowadzasz te dane raz, czy masz skrypt do każdego testu, który wprowadza przed odpaleniem i usuwa po?
  2. Testujesz same serwisy czy całą ścieżkę - zapytania do kontrolerów?
0
siararadek napisał(a):
  1. Wprowadzasz te dane raz, czy masz skrypt do każdego testu, który wprowadza przed odpaleniem i usuwa po?

Skrypty w sensie SQL? To takich nie mam, używam ORMa. Dane wstawiam w TextFixtureSetup albo w TestSetup, i nie usuwam tylko odtwarzam strukturę bazy, albo kasuję plik z bazą (jeśli testy odpalam na SQLExpress). Testy można też odpalać na SQLite w trybie in memory, ale nie mam z tym doświadczeń.

  1. Testujesz same serwisy czy całą ścieżkę - zapytania do kontrolerów?

Raczej same serwisy. Nie wiem, co rozumiesz przez "zapytania do kontrolerów"?

3

Niektórzy testują wszystko, niektórzy tylko logikę biznesową.
Generalnie jak ktoś chce robić "po bożemu" to powinien testować wszystko.
Jak w robocie przy jakimś projekcie wymagane jest pełne pokrycie testami to robie sobie następujące projekty:

Project.Web.Tests - testy na warstwie webowej, m.in. na kontrolerach(czy dla odpowiednich parametrow zwrócono odpowiedni widok/nastąpilo wymagane przekierowanie, testy routingów itp.), reszta warstw jest mockowana.
Project.Application.Tests - testy m.in. serwisów, uprawnień, autoryzacji itp. reszta warstw jest mockowana
Project.Domain.Tests - testy na obiektach typowo biznesowych - nie ma tu zależności do innych warstw, ani dostępu do zewn. źródeł danych, więc nie ma też czego mockować.

Project.Persistence.Tests - podstawowe testy na normalnej bazie danych, sprawdzające (dla każdej encji), czy można rekord pomyślnie zapisać, zaktualizować, wczytać i czy będą po tych operacjach poprawne wartości. + kilka pomniejszych testów dla sprawdzenia czy poprawnie działa warstwa infrastruktury.

Wtedy jak gdzies test sie wywali, to dokladnie wiadomo w jakiej warstwie i w jakim miejscu występuje babol.

1 użytkowników online, w tym zalogowanych: 0, gości: 1