Entity Framework Entities - niektóre dane z serwisu WWW - najlepsza architektura?


10

Obecnie używamy Entity Framework jako ORM w kilku aplikacjach internetowych i do tej pory nam to odpowiadało, ponieważ wszystkie nasze dane są przechowywane w jednej bazie danych. Używamy wzorca repozytorium i mamy usługi (warstwa domeny), które z nich korzystają, i zwracają jednostki EF bezpośrednio do kontrolerów ASP.NET MVC.

Jednak pojawił się wymóg korzystania z zewnętrznego interfejsu API (za pośrednictwem usługi internetowej), który zapewni nam dodatkowe informacje dotyczące użytkownika w naszej bazie danych. W naszej lokalnej bazie danych użytkowników będziemy przechowywać zewnętrzny identyfikator, który możemy podać API, aby uzyskać dodatkowe informacje. Dostępnych jest sporo informacji, ale dla uproszczenia jedna z nich dotyczy firmy użytkownika (imię i nazwisko, kierownik, pokój, stanowisko, lokalizacja itp.). Informacje te będą wykorzystywane w różnych miejscach w naszych aplikacjach internetowych, a nie w jednym miejscu.

Więc moje pytanie brzmi: gdzie jest najlepsze miejsce do wypełnienia tych informacji? Ponieważ jest używany w różnych miejscach, nie jest sensowne pobieranie go na zasadzie ad hoc, gdziekolwiek korzystamy z aplikacji internetowej - więc warto zwrócić te dodatkowe dane z warstwy domeny.

Początkowo myślałem o stworzeniu klasy modelu opakowania, która zawierałaby encję EF (EFUser) i nową klasę „ApiUser” zawierającą nowe informacje - a kiedy dostajemy użytkownika, otrzymujemy EFUser, a następnie otrzymujemy dodatkowe informacje z interfejsu API i wypełnij obiekt ApiUser. Jednak chociaż byłoby to dobre w przypadku pozyskiwania pojedynczych użytkowników, przewraca się, gdy uzyskuje się wielu użytkowników. Nie możemy trafić do interfejsu API podczas uzyskiwania listy użytkowników.

Moją drugą myślą było po prostu dodanie metody singleton do encji EFUser, która zwraca ApiUser, i po prostu zapełnienie jej w razie potrzeby. To rozwiązuje powyższy problem, ponieważ uzyskujemy do niego dostęp tylko wtedy, gdy jest to potrzebne.

Lub ostatnią myślą było zachowanie lokalnej kopii danych w naszej bazie danych i zsynchronizowanie jej z interfejsem API, gdy użytkownik się zaloguje. Jest to minimalna praca, ponieważ jest to tylko proces synchronizacji - i nie mamy narzutu związanego z uderzeniem DB i API za każdym razem, gdy chcemy uzyskać informacje o użytkowniku. Oznacza to jednak przechowywanie danych w dwóch miejscach, a także oznacza, że ​​dane są nieaktualne dla każdego użytkownika, który nie logował się przez dłuższy czas.

Czy ktoś ma jakieś porady lub sugestie, jak najlepiej poradzić sobie z tego rodzaju scenariuszem?


it's not really sensible to fetch it on an ad-hoc basis-- Dlaczego? Ze względu na wydajność?
Robert Harvey,

Nie mam na myśli uderzania interfejsu API na zasadzie ad hoc - mam na myśli utrzymanie istniejącej struktury encji, a następnie wywoływanie interfejsu API ad-hoc w aplikacji internetowej, gdy jest to potrzebne - miałem na myśli, że to nie będzie rozsądne, ponieważ należałoby to zrobić w wielu miejscach.
stevehayter

Odpowiedzi:


3

Twój przypadek

W twoim przypadku wszystkie trzy opcje są wykonalne. Myślę, że najlepszą opcją jest prawdopodobnie zsynchronizowanie źródeł danych w miejscu, którego aplikacja asp.net nawet nie zna. Oznacza to, że za każdym razem unikaj dwóch pobrań na pierwszym planie, po cichu synchronizuj interfejs API z db). Więc jeśli jest to realna opcja w twoim przypadku - mówię, zrób to.

Rozwiązanie, w którym pobieranie odbywa się „raz”, jak sugeruje inna odpowiedź, nie wydaje się zbyt opłacalne, ponieważ nie utrzymuje żadnej odpowiedzi w dowolnym miejscu, a ASP.NET MVC po prostu pobiera pobieranie dla każdego żądania w kółko.

Unikałbym singletonu, nie sądzę, że to dobry pomysł z wielu typowych powodów.

Jeśli trzecia opcja nie jest wykonalna - jedną z opcji jest leniwe załadowanie jej. Oznacza to, że klasa musi rozszerzyć encję i niech trafi do interfejsu API w razie potrzeby . To bardzo niebezpieczna abstrakcja, ponieważ jest jeszcze bardziej magiczna i nieoczywista.

Chyba sprowadza się to do kilku pytań:

  • Jak często zmieniają się dane wywołania API? Nie często? Trzecia opcja. Często? Nagle trzecia opcja nie jest zbyt opłacalna. Nie jestem pewien, czy jestem tak przeciwny rozmowom ad-hoc jak ty.
  • Ile kosztuje połączenie API? Czy płacisz za połączenie? Czy oni są szybcy? Wolny? Jeśli są szybkie, nawiązywanie połączenia za każdym razem może działać, jeśli są powolne, musisz mieć jakieś prognozy i wykonywać połączenia. Jeśli kosztują pieniądze - to duża zachęta do buforowania.
  • Jak szybki musi być czas reakcji? Oczywiście szybsze jest lepsze, ale w niektórych przypadkach warto poświęcić szybkość dla uproszczenia, zwłaszcza jeśli nie jest to bezpośrednie zadanie dla użytkownika.
  • Czym różnią się dane API od twoich danych? Czy to dwie koncepcyjnie różne rzeczy? Jeśli tak, może być jeszcze lepiej odsłonić interfejs API na zewnątrz, niż zwrócić wynik API z wynikiem bezpośrednio i pozwolić drugiej stronie wykonać drugie wywołanie i zarządzać nim.

Kilka słów o rozdzieleniu obaw

Pozwólcie mi spierać się z tym, co Bobson mówi o rozdzieleniu obaw tutaj. Ostatecznie - umieszczenie tej logiki w takich bytach narusza tak samo rozdzielenie obaw.

Posiadanie takiego repozytorium narusza podział problemów równie źle, poprzez umieszczenie logiki centrycznej prezentacji w warstwie logiki biznesowej. Twoje repozytorium nagle zdaje sobie sprawę z rzeczy związanych z prezentacją, takich jak sposób wyświetlania użytkownika w kontrolerach asp.net mvc.

W tym powiązanym pytaniu zadałem pytanie o dostęp do encji bezpośrednio z kontrolera. Pozwól mi zacytować jedną z odpowiedzi:

„Witamy w BigPizza, niestandardowym sklepie z pizzą, czy mogę przyjąć zamówienie?” „Cóż, chciałbym zjeść pizzę z oliwkami, ale sos pomidorowy na górze i ser na dole i piec w piekarniku przez 90 minut, aż będzie czarny i twardy jak płaska skała z granitu”. „OK, proszę pana, niestandardowe pizze to nasz zawód, damy radę”.

Kasjer idzie do kuchni. „Przy kasie jest psycholog, chce zjeść pizzę z… to skała granitu z… poczekaj… najpierw musimy mieć imię”, mówi kucharzowi.

„Nie!”, Krzyczy kucharz, „nie znowu! Wiesz, że już tego próbowaliśmy”. Bierze stos papieru z 400 stronami: „tutaj mamy skałę z granitu z 2005 roku, ale… nie miała oliwek, ale papryka zamiast… lub tutaj jest najlepszy pomidor… ale klient tego chciał pieczone tylko pół minuty. ” „Może powinniśmy to nazwać TopTomatoGraniteRockSpecial?” „Ale to nie bierze pod uwagę sera na dole ...” Kasjer: „To właśnie ma wyrazić Special”. „Ale ukształtowanie skały pizzy w kształcie piramidy byłoby również wyjątkowe”, odpowiada kucharz. „Hmmm… to trudne…”, mówi zrozpaczony kasjer.

„CZY MOJA PIZZA JUŻ JEST W PIEKARNIKU?”, Nagle krzyczy przez drzwi kuchni. „Zatrzymajmy tę dyskusję, po prostu powiedz mi, jak zrobić tę pizzę, nie będziemy mieli takiej pizzy po raz drugi”, decyduje kucharz. „OK, to pizza z oliwkami, ale sos pomidorowy u góry i ser na dole i piecz w piekarniku przez 90 minut, aż będzie czarny i twardy jak płaska skała z granitu”.

(Przeczytaj resztę odpowiedzi, to naprawdę ładne imo).

Naiwnością jest ignorowanie faktu, że istnieje baza danych - istnieje baza danych i bez względu na to, jak bardzo chcesz ją wyodrębnić, nigdzie się nie zmierza. Twoja aplikacja będzie wiedziała o źródle danych. Nie będziesz w stanie „zamienić go na gorąco”. ORM są przydatne, ale przeciekają ze względu na to, jak skomplikowany jest problem, który rozwiązują oraz z wielu powodów związanych z wydajnością (na przykład Select n + 1).


Dziękujemy za bardzo dokładną odpowiedź @Benjamin. Początkowo zacząłem tworzyć prototyp, korzystając z powyższego rozwiązania Bobsona (nawet zanim opublikował swoją odpowiedź), ale poruszasz kilka ważnych kwestii. Aby odpowiedzieć na pytania: - Wywołanie API nie jest bardzo drogie (są bezpłatne, a także szybkie). - Niektóre części danych będą zmieniać się dość regularnie (niektóre nawet co kilka godzin). - Szybkość jest dość ważna, ale odbiorcy aplikacji są tacy, że błyskawiczne szybkie ładowanie nie jest absolutnym wymogiem.
stevehayter

@stevehayter W takim przypadku najprawdopodobniej wykonałbym wywołania interfejsu API po stronie klienta. Jest tańszy i szybszy i daje lepszą kontrolę.
Benjamin Gruenbaum,

1
W oparciu o te odpowiedzi mniej skłaniam się ku przechowywaniu lokalnej kopii danych. Właściwie skłaniam się do oddzielnego udostępniania interfejsu API i w ten sposób przetwarzam dodatkowe dane. Myślę, że może to być dobry kompromis między prostotą rozwiązania @ Bobsona, ale dodaje również pewien stopień separacji, w którym jestem nieco bardziej komfortowy. Przyjrzę się tej strategii w moim prototypie i zdam relację z moich odkryć (i przyznam nagrodę!).
stevehayter

@BenjaminGruenbaum - Nie jestem pewien, czy podążę za twoją argumentacją. W jaki sposób moja sugestia informuje repozytorium o prezentacji? Jasne, zdaje sobie sprawę, że uzyskano dostęp do pola wspieranego przez API, ale nie ma to nic wspólnego z tym, co widok robi z tymi informacjami.
Bobson

1
Zdecydowałem się przenieść wszystko na stronę klienta - ale jako metodę rozszerzenia na EFUser (która istnieje w warstwie prezentacji, w osobnym zestawie). Metoda po prostu zwraca dane z interfejsu API i ustawia singleton, aby nie był powtarzany. Żywotność obiektów jest tak krótka, że ​​nie mam problemu z używaniem tutaj singletona. W ten sposób istnieje pewien stopień separacji, ale nadal mam wygodę pracy z jednostką EFUser. Dziękujemy wszystkim respondentom za pomoc. Zdecydowanie ciekawa dyskusja :).
stevehayter,

2

Przy odpowiednim rozdzieleniu problemów nic powyżej poziomu Entity Framework / API nie powinno nawet wiedzieć, skąd pochodzą dane. O ile wywołanie interfejsu API nie jest drogie (pod względem czasu lub przetwarzania), dostęp do danych, które go wykorzystują, powinien być tak przejrzysty, jak dostęp do danych z bazy danych.

Najlepszym sposobem na wdrożenie tego byłoby dodanie do EFUserobiektu dodatkowych właściwości, które w razie potrzeby leniwie ładują dane API. Coś takiego:

partial class EFUser
{
    private APIUser _apiUser;
    private APIUser ApiUser
    {
       get { 
          if (_apiUser == null) _apiUser = API.LoadUser(this.ExternalID);
          return _apiUser;
       }
    }
    public string CompanyName { get { return ApiUser.UserCompanyName; } }
    public string JobTitle{ get { return ApiUser.UserJobTitle; } }
}

Zewnętrznie, po raz pierwszy albo CompanyNameczy JobTitlejest używany nie będzie pojedynczy wywołanie API wykonany (a zatem małe opóźnienie), ale wszystkie kolejne połączenia, dopóki obiekt jest niszczony będzie tak szybkie i proste jak dostęp do bazy danych.


Dzięki @ Bobson ... to była właściwie początkowa trasa, którą zacząłem schodzić (z kilkoma metodami rozszerzeń dodanymi w celu masowego ładowania szczegółów list użytkowników - na przykład wyświetlanie nazwy firmy dla listy użytkowników). Wydaje się, że do tej pory dobrze odpowiada moim potrzebom - ale Benjamin poniżej porusza kilka ważnych kwestii, więc zamierzam kontynuować ocenę w tym tygodniu.
stevehayter

0

Jednym z pomysłów jest modyfikacja ApiUser, aby nie zawsze zawierała dodatkowe informacje. Zamiast tego umieścisz metodę na ApiUser, aby ją pobrać:

ApiUser apiUser = backend.load($id);
//Locally available data directly on the ApiUser like so:
String name = apiUser.getName();
//Expensive extra data available after extra call:
UserExtraData extraData = apiUser.fetchExtraData();
String managerName = extraData.getManagerName();

Możesz to również nieco zmodyfikować, aby użyć leniwego ładowania dodatkowych danych, abyś nie musiał wyodrębniać UserExtraData z obiektu ApiUser:

//Extra data fetched on first get:
String managerName = apiUser.lazyGetExtraData().getManagerName();

W ten sposób, gdy masz listę, dodatkowe dane nie będą domyślnie pobierane. Ale nadal możesz uzyskać do niego dostęp podczas przeglądania listy!


Nie jestem pewien, co masz na myśli - w backend.load () już ładujemy - więc na pewno załadowałoby to dane API?
stevehayter,

Mam na myśli, że powinieneś poczekać z dodatkowym ładowaniem, aż zostanie wyraźnie zażądane - leniwe ładowanie danych API.
Alexander Torstling,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.