Generyczne repozytoria- czy to w ogóle ma jakiś sens?

0

Przeglądałem dziś kilka kursów programowania Api w NetCore i wszędzie jeśli jest użyty Entity Framework pojawiają się repozytoria generyczne, czyli mamy

public interface IDataService<T>
    {
        Task<IEnumerable<T>> GetAll();
        Task<T> Get(int id);
        Task<T> Create(T entity);
        Task<T> Update(int id, T entity);
        Task<bool> Delete(int id);
    }

Czy to ma jakiś sens w praktycznym programowaniu? Bo dla mnie to istnieje tu tylko sens akademicki, pomimo ze autorzy kursu twierdzą ze pokazują przykład praktycznego zastosowania w aplikacji. Szczególnie nie pasują mi metody Get(int id) i GetAll(). Pierwsza dlatego ze przeważnie potrzeba więcej warunków, żeby określić co chcemy dostać, a druga, że bez sensu pobierać wszystko i też wedlug mnie powinna być sparametryzowana.
Czy stosować takie generyczne twory? Jeśli miałbym tego uzywać to sądzę ze byłoby tam co najmniej kilka metod Get z różnyumi parametrami. Czy dobrze wnioskuję?

4
kalimata napisał(a):

Bo dla mnie to istnieje tu tylko sens akademicki, pomimo ze autorzy kursu twierdzą ze pokazują przykład praktycznego zastosowania w aplikacji.

To nie ma sensu nawet akademickiego, a co dopiero praktycznego, ani w przypadku EF ani innych ORM. Unikaj takich tutoriale, najlepiej też ich autorów. W takich przypadkach zazwyczaj nieszczęścia chodzą parami i obok pewnie są inne raczyska jak repozytoria per tabela.

Przykładowe problemy z generycznymi repozytoriami:

  1. Rzadko które obiekty z bazy się usuwa. Będziesz rzucał tam jakieś wyjątki?
  2. GetAll będzie pakować całą bazę do aplikacji? Tego nawet nikt w tych ToDoAppach nie zauważa, bo danych jest mało, ale to nie zadziała w żadnej sensownej aplikacji.
1
Saalin napisał(a):

W takich przypadkach zazwyczaj nieszczęścia chodzą parami i obok pewnie są inne raczyska jak repozytoria per tabela.

Zaraz pewnie pojawi się magiczne słowo DDD, ale jak tak patrzę, to @somekind miał rację pisząc, że repozytoria bez DDD nie mają sensu. Jak mamy agregaty i jest repozytorium per agreagat, to jest ładnie. Gorzej, jak każda encja ma swoje repozytorium i te encje mają jeszcze referencje do innych encji, wtedy robi się bałagan, bo np. okazuje się, że jedną encję można zapisać przez 2 repozytoria.

0

Też bym nie chciał żeby tu pojawił się temat DDD ,bo w tym nie robie i z tego co tu na forum wyczytałem tylko tam jest sensowne uzywać okreslenia repozytorium.

Piszecie jak nie powinno się robić. A w takim razie jak **powinno **sie robić?

Czy jak mam np tabele User i Address to poprawne będzie zrobić:
Ja po swojemu bym to zrobił w sposob ze np mam tabele z użytkownikami to robie

public interface IUserService
    {
      
        Task<User> Get(int id);
         Task<User> GetByName(string name);
        Task<User> Create(User entity);
        Task<User> Update(int id, User entity);
        Task<bool> Delete(int id);
//... inne medoty z warunkami
    }

i w jakiejś implementacji tego interfejsu

public class UserService : IUserService
    {

private readonly DbContext context;

public UserService(DbContext c)
{
context = c;
}

  public async Task<User> GetByName(string name)
{
return await context.Users.FirstOrDefaultAsync(u=>u.Name==name);
}

// implementacja pozostałych elementów
    }

i podobnie obsługa adresów

0

@Saalin:

  1. GetAll będzie pakować całą bazę do aplikacji? Tego nawet nikt w tych ToDoAppach nie zauważa, bo danych jest mało, ale to nie zadziała w żadnej sensownej aplikacji.

Generalnie to nie jest problem, bo w parametrze Geta możesz przekazać listę Expression<Func<T, bool>> i zostaną wykorzystane twoje silnie typowane lambdy które możesz wykorzystać jako predykaty w Where, tak samo JOINy.

1

@WeiXiao: a przekazywanie Expression<Func<T,bool>> nie uzależnia Cię przypadkiem od ORMa, który siedzi pod spodem? Bo jak byś chciał użyć Dappera albo jakąś funkcję SQLową odpalić, to już nie tak łatwo by było przekazać tam ten expression afaik.

0

@nobody01: to wygeneruj sobie sql z expressionów i cześć ;)

masz rację

3

Repozytorium operuje na obiektach domenowych, nie zaś obiektach z ORM, to jest różnica ;)

0

per encję bazodanową mało sensowne, na domenie tak - a jakie słowo ktoś do tego dorobi ... czy DAO, czy repozytorium, generalnie te słowa są nadużywane / używane często nieprawidłowo. Ważny sens.

Nowoczesne metody dostępu do baz danych są na tyle generyczne same w sobie, że dodatkowy poziom dostępu do encji bazodanowej nie ma sensu

0

Czyli najlepiej zrobić tak jak napisałem w drugim przykładzie,?

public async Task<User> GetByName(string name)
{
return await context.Users.FirstOrDefaultAsync(u=>u.Name==name);
}
3

To przekazywanie expression do generycznego repo to kolejny dowód, że generyczne repo to rak. W czym takie generyczne repo jest lepsze od wstrzykiwania całego dbContext

0
nobody01 napisał(a):

@WeiXiao: a przekazywanie Expression<Func<T,bool>> nie uzależnia Cię przypadkiem od ORMa, który siedzi pod spodem? Bo jak byś chciał użyć Dappera albo jakąś funkcję SQLową odpalić, to już nie tak łatwo by było przekazać tam ten expression afaik.

Drzewo wyrażeń nie uzależnia cię od niczego. Na bazie drzewa możesz sobie wygenerować ręcznie SQL który odpalisz przez ADO.NET albo dappera albo złożysz z niego IQueryable i wykonasz przy użyciu EF.

1

Według mnie: żadnych generycznych repozytoriów, żadnych expressionów. Najprościej jak to tylko możliwe, bez przekombinowywania.

1

@maszrum:

no w całości nie uciekniesz od expressionów, bo są bardzo fajne do dynamicznych predykatów.

to tylko funkcja, nic strasznego.

0

@WeiXiao: masz na myśli używane expressionów do jakichś wyszukiwarek czy w jakiejś logice biznesowej?

0

@nobody01:

w zwykłym searchu dla usera, gdzie pola są opcjonalne

0

@WeiXiao: A to nie jest tak, że to zadziała tylko w bardzo prostych przypadkach? Bo jakbym miał przy użyciu tego robić jakiegoś joina z kilkunastoma tabelami z dziwnymi warunkami, autoryzacją, funkcjami okienkowymi, to jednak bym wolał czystego sqla :)

0
Aleksander32 napisał(a):

Repozytorium operuje na obiektach domenowych, nie zaś obiektach z ORM, to jest różnica ;)

A widziałeś żeby ktoś przepisywał obiekty z ORM na obiekty domenowe i w drugą stronę? No bo jak np przepiszesz w Hibernate (NHibernate pewnie podobnie) to tracisz co najmniej połowę zalet ORMa czyli śledzenie zmian

0

@nobody01:

a czemu JOINy miałyby być problemem dla EF Cora?

jak chcesz robić dziwne rzeczy, to pewnie będzie znacznie trudniej

0

EF Core raczej słabo sobie radził z joinami afaik, jakieś dziwne subquery generował :/
Do tego zmiany między wersjami: https://github.com/dotnet/efcore/issues/18022

0
KamilAdam napisał(a):
Aleksander32 napisał(a):

Repozytorium operuje na obiektach domenowych, nie zaś obiektach z ORM, to jest różnica ;)

A widziałeś żeby ktoś przepisywał obiekty z ORM na obiekty domenowe i w drugą strone? No bo jak np przepiszesz w Hibernate (NHibernate pewnie podobnie) to tracisz co najmniej połowę zalet ORMa czyli śledzenie zmian

A to główna zaletą ORM nie jest mapowanie relacji na obiekty? Bo to mapowanie obiektów domenowych na bazodanowe i odwrotnie to widzę praktycznie w każdej aplikacji.

1

Hmm, to co te obiekty domenowe robią, że nie da się ich zmapować np. przez ef core? Tu jest dość złożony przykład imo i działa: https://github.com/kgrzybek/modular-monolith-with-ddd/blob/master/src/Modules/Meetings/Domain/Meetings/Meeting.cs

Trzeba się trochę nakonfigurować, ale to chyba i tak lepsze, niż utrzymywanie dwóch warstw obiektów: https://github.com/kgrzybek/modular-monolith-with-ddd/blob/master/src/Modules/Meetings/Infrastructure/Domain/Meetings/MeetingEntityTypeConfiguration.cs

1

To czy można zmapowac to jedno, a czy się powinno to drugie. Jak już prześcigamy się w złych pomysłach to już wole generyczne repozytoria.

1

@Saalin: A dlaczego takie mapowanie uważasz za złe?

1

Przecież mapujesz co chcesz i jak chcesz. Po co trzymać jeszcze osobnę libkę z jakimiś POCOsami.
Chyba że komuś płaca za LOC

3

@nobody01: polecam poczytać o clean architecture i Dependency Inversion Principle. Jak masz aplikację biznesowa która nie jest CRUDem to model danych (zapisu) powinien zależeć od encji domenowych i logiki biznesowej a nie odwrotnie! To czy korzystasz z bazy dnaych SQL, NoSQL, Ramu, plików, Resta, kolejek itp nie powinno miec znaczenia dla logiki biznesowej...

A widziałeś żeby ktoś przepisywał obiekty z ORM na obiekty domenowe i w drugą stronę?

Widziałem

1

@Aleksander32: czytałem :) pokaż te encje, której wg ciebie ef core nie da rady zmapowac :) od razu uprzedzam że z prywatnymi polami, niemutowalnymi kolekcjami i value objectami nie będzie problemów. I to bez żadnych atrybutów.

1

@kalimata: tak, tzw generyczne repozytorium ma sens jeśli wiesz co czego może posłużyć, kiedy i jak używać.
To nie powinna być domyślna warstwa DAO ale czasami jest przydatne.
Przymusu używania nie ma. Np. Jak masz dużo małych tabel slownikowych.

2
kalimata napisał(a):

Czy to ma jakiś sens w praktycznym programowaniu? Bo dla mnie to istnieje tu tylko sens akademicki, pomimo ze autorzy kursu twierdzą ze pokazują przykład praktycznego zastosowania w aplikacji.

No praktyczne programowanie polega na tym, że każdy programista musi co najmniej raz w tygodniu napisać bezużyteczny wrapper, i generyczne repozytorium pomaga to osiągnąć. ;)
Ale nie jest to ani pragmatyczne, ani sensowne, no chyba, że nie masz ORMa ani żadnego gotowego dostępu do miejsca przechowywania danych, to wtedy takie bazowe DAO będzie miało sens (aczkolwiek nie ma najmniejszego powodu wyzywać porządnego DAO od repozytoriów).

Jeśli miałbym tego uzywać to sądzę ze byłoby tam co najmniej kilka metod Get z różnyumi parametrami.

No, tylko wtedy nie będzie to już generyczne.

kalimata napisał(a):

i w jakiejś implementacji tego interfejsu

Jak to w "jakiejś"? A będzie więcej niż jedna? Po co w ogóle taki interfejs?
Ten kod jest w moim odczuciu dość nienaturalny, ten Twój serwis to tak naprawdę jakiś wrapper na ORMa. Ja w miejscu, w którym mam logikę aplikacji (czyli serwisie aplikacyjnym bądź mediatorowym handlerze) użył dbContextu i zrobił, co potrzebuję.

KamilAdam napisał(a):

A widziałeś żeby ktoś przepisywał obiekty z ORM na obiekty domenowe i w drugą strone? No bo jak np przepiszesz w Hibernate (NHibernate pewnie podobnie) to tracisz co najmniej połowę zalet ORMa czyli śledzenie zmian

Ale przecież nie trzeba mapować, bo obiekty domenowe mogą być używane przez ORMa. Ale aby to było możliwe, to domena musi spełniać warunek bycia persistence ignorance, a ORM nie może mieć żadnych wymagań co do encji, które mapuje. A nie każdy ORM na to pozwala.
Problem jest wtedy, gdy obiekty z ORMa są używane jako domenowe, czyli de facto model biznesowy operuje na strukturze bazy danych.

Saalin napisał(a):

A to główna zaletą ORM nie jest mapowanie relacji na obiekty? Bo to mapowanie obiektów domenowych na bazodanowe i odwrotnie to widzę praktycznie w każdej aplikacji.

A w ilu przypadkach wynika to z czegoś innego niż "każda warstwa operacyjna musi mieć swoją warstwę struktur danych" albo "mamy słabego ORMa"? Bo tylko te pozostałe mogą być sensowne. :)

urke napisał(a):

Przecież mapujesz co chcesz i jak chcesz. Po co trzymać jeszcze osobnę libkę z jakimiś POCOsami.

Chyba że komuś płaca za LOC

Tyle, że to nie zawsze są POCO, bo np. technologia składowania wymaga, aby to nie były POCO. To jeden przypadek, dla którego oddzielenie persistence modeli ma sens.
Drugim mogą być znaczące różnice między logiką biznesową, a potrzebami składowania. Jeśli te modele różnią się znacząco, to jest sens mieć oddzielną libkę.

W tej kwestii bardzo unikałbym twierdzenia, że któreś z tych dwóch podejść jest zawsze złe, albo zawsze dobre. To jest mocno zależne od konkretnej sytuacji.

nobody01 napisał(a):

@Aleksander32: czytałem :) pokaż te encje, której wg ciebie ef core nie da rady zmapowac :) od razu uprzedzam że z prywatnymi polami, niemutowalnymi kolekcjami i value objectami nie będzie problemów. I to bez żadnych atrybutów.

Ta nadzieja w to, że Microsoft kiedyś stworzy działającego ORMa przypomina mi te kobiety z podbitymi oczami, które wybaczają, bo "łobuz kocha najbardziej".

using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;

namespace EntityFrejmłork
{
    public class Wreszcie
    {
        [Fact]
        public void UmieMapowaćPola()
        {
            string entryTitle = Guid.NewGuid().ToString();

            using (var context = new AppDbContext())
            {
                var entry = new Entry { Title = entryTitle };
                context.Entries.Add(entry);

                entry.AddTag("Microsoft");
                entry.AddTag("nie");
                entry.AddTag("umie");

                context.SaveChanges();
            }

            using (var context = new AppDbContext())
            {
                var entry = context.Entries.Single(x => x.Title == entryTitle);

                entry.AddTag("w");
                entry.AddTag("Ormy");

                context.Invoking(c => c.SaveChanges()).Should().Throw<InvalidOperationException>();
            }
        }

        [Fact]
        public void RadziSobieZOdłączonymObiektem()
        {
            Info detached;
            using (var context = new AppDbContext())
            {
                detached = context.Infos.Find(2);
            }

            using (var context = new AppDbContext())
            {
                var entry = context.Entries.Find(3);
                entry.Info = detached;
                context.Invoking(c => c.SaveChanges()).Should().NotThrow();
            }
        }
    }

    public class AppDbContext : DbContext
    {
        public DbSet<Entry> Entries { get; set; }
        public DbSet<Tag> Tags { get; set; }
        public DbSet<Info> Infos { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=EfSsie;Trusted_Connection=True;MultipleActiveResultSets=true");
        }
    }

    public class Entry
    {
        private readonly ICollection<Tag> _tags = new List<Tag>();
        public virtual IReadOnlyList<Tag> Tags => _tags.ToList();
        public virtual Info Info { get; set; }

        public int Id { get; set; }
        public string Title { get; set; }

        public void AddTag(string name)
        {
            if (_tags.Count > 3) // Ważna logika biznesowa
                throw new InvalidOperationException();

            _tags.Add(new Tag { Name = name, Entry = this }); ;
        }
    }

    public class Tag
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Entry Entry { get; set; }
    }

    public class Info
    {
        public int Id { get; set; }
        public string Content { get; set; }
        public Entry Entry { get; set; }
        public int EntryId { get; set; }    // w TVP mówili, że już niepotrzebne
    }
}

screenshot-20210220034234.png

3

@somekind:
Test 1) Dlaczego chcesz, żeby ef core (w sumie nawet nie ef core a sama aplikacja) tam rzucił wyjątek, skoro nie zaciągnąłeś _tags? Jak _tags są częścią agregatu Entry, to powinny być owned types, żeby ef core je zawsze zaciągał. No chyba, że masz gdzieś jeszcze włączony lazy loading, ale nie widzę go tutaj (samo virtual nie sprawi, że lazy loading zadziała). https://www.learnentityframeworkcore.com/lazy-loading
Test 2) Potrzebne była Ci kiedykolwiek taka funkcjonalność? Zazwyczaj chyba używa się instancji db contextu per request (scoped).

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