Strategie unikania SQL w kontrolerach… lub ile metod powinienem zastosować w moich modelach?

17

Tak więc często spotykam się z sytuacją, w której moje modele zaczynają:

  • Wyhoduj potwory z mnóstwem metod

LUB

  • Pozwalają ci przekazywać im fragmenty SQL, dzięki czemu są wystarczająco elastyczne, aby nie wymagały miliona różnych metod

Załóżmy na przykład, że mamy model „widżetu”. Zaczynamy od kilku podstawowych metod:

  • get ($ id)
  • wstaw ($ record)
  • aktualizacja ($ id, $ record)
  • usuń ($ id)
  • getList () // pobierz listę widżetów

Wszystko w porządku i elegancko, ale potrzebujemy raportów:

  • listCreatedBetween ($ data_początkowa, $ data_końcowa)
  • listPurchasedBetween ($ data_początkowa, $ data_końcowa)
  • listOfPending ()

A potem raportowanie zaczyna się komplikować:

  • listPendingCreatedBetween ($ data_początkowa, $ data_końcowa)
  • listForCustomer ($ identyfikator_użytkownika)
  • listPendingCreatedBetweenForCustomer ($ identyfikator_użytkownika, $ data_początkowa, $ data_końcowa)

Możesz zobaczyć, gdzie to rośnie ... w końcu mamy tak wiele specyficznych wymagań dotyczących zapytań, że albo muszę wdrożyć mnóstwo metod, albo jakiś obiekt „zapytania”, który mogę przekazać do pojedynczego -> zapytania (zapytania $ zapytanie) metoda ...

... lub po prostu ugryź kulę i zacznij robić coś takiego:

  • list = MyModel-> zapytanie („data_początkowa> X I data_końcowa <T ORAZ w toku = 1 ORAZ identyfikator_użytkownika = Z”)

Istnieje pewna apelacja, aby mieć tylko jedną taką metodę zamiast 50 milionów innych bardziej szczegółowych metod ... ale czasami wydaje się „niewłaściwe” wrzucanie do kontrolera stosu tego, co w zasadzie jest SQL.

Czy istnieje „właściwy” sposób radzenia sobie z takimi sytuacjami? Czy wydaje się akceptowalne umieszczanie takich zapytań w ogólnej -> query () metodzie?

Czy są lepsze strategie?

Keith Palmer Jr.
źródło
W tej chwili mam do czynienia z tym samym problemem w projekcie innym niż MVC. Pojawia się pytanie, czy warstwa dostępu do danych powinna wyodrębniać każdą procedurę przechowywaną i pozostawić bazę danych warstwy logiki biznesowej agnostyczną, czy też warstwa dostępu do danych powinna być ogólna, kosztem warstwy biznesowej wiedząc coś o bazowej bazie danych? Być może rozwiązaniem pośrednim jest mieć coś takiego jak ExecuteSP (ciąg spName, parametry obiektu [] parametry), a następnie dołączyć wszystkie nazwy SP do pliku konfiguracyjnego, aby warstwa biznesowa mogła je odczytać. Jednak nie mam na to bardzo dobrej odpowiedzi.
Greg Jackson,

Odpowiedzi:

10

Wzorce architektury aplikacji korporacyjnych Martina Fowlera opisują szereg szablonów związanych z ORM, w tym użycie obiektu zapytania, co sugeruję.

Obiekty zapytania umożliwiają przestrzeganie zasady pojedynczej odpowiedzialności poprzez rozdzielenie logiki dla każdego zapytania na indywidualnie zarządzane i utrzymywane obiekty strategii. Albo kontroler może bezpośrednio zarządzać ich użyciem, lub przekazać to drugiemu kontrolerowi lub obiektowi pomocniczemu.

Czy będzie ich dużo? Na pewno. Czy niektóre można pogrupować w ogólne zapytania? Tak ponownie.

Czy można użyć wstrzykiwania zależności do tworzenia obiektów z metadanych? Tak właśnie działa większość narzędzi ORM.

Matthew Flynn
źródło
4

Nie ma właściwego sposobu, aby to zrobić. Wiele osób korzysta z ORM, aby pozbyć się całej złożoności. Niektóre bardziej zaawansowane ORM tłumaczą wyrażenia kodu na skomplikowane instrukcje SQL. ORM mają również swoje wady, jednak w przypadku wielu zastosowań korzyści przewyższają koszty.

Jeśli nie pracujesz z ogromnym zestawem danych, najprostszą rzeczą jest wybranie całej tabeli do pamięci i przefiltrowanie kodu.

//pseudocode
List<Person> people = Sql.GetList<Person>("select * from people");
List<Person> over21 = people.Where(x => x.Age >= 21);

W przypadku wewnętrznych aplikacji raportujących takie podejście jest prawdopodobnie w porządku. Jeśli zestaw danych jest naprawdę duży, zaczniesz potrzebować wielu niestandardowych metod, a także odpowiednich indeksów w tabeli.

Dan
źródło
1
+ 1 za „Nie ma właściwego sposobu, aby to zrobić”
oz.
1
Niestety filtrowanie poza zestawem danych nie jest tak naprawdę opcją nawet w przypadku najmniejszych zestawów danych, z którymi pracujemy - jest po prostu zbyt wolny. :-( Dobrze słyszeć, że inni napotykają na ten sam problem. :-)
Keith Palmer Jr.
@KeithPalmer z ciekawości, jak duże są twoje stoły?
dn
Setki tysięcy wierszy, jeśli nie więcej. Zbyt wielu, by filtrować z akceptowalną wydajnością poza bazą danych, zwłaszcza w architekturze rozproszonej, w której bazy danych nie znajdują się na tym samym komputerze co aplikacja.
Keith Palmer Jr.
-1 dla „Nie ma właściwego sposobu, aby to zrobić”. Istnieje kilka poprawnych sposobów. Podwojenie liczby metod podczas dodawania funkcji tak, jak robił to PO, jest podejściem nieskalowalnym, a zaproponowana tutaj alternatywa jest równie nieskalowalna, tylko pod względem wielkości bazy danych, a nie liczby funkcji zapytania. Istnieją podejścia skalowalne, zobacz inne odpowiedzi.
Theodore Murdock
4

Niektóre ORM pozwalają konstruować złożone zapytania, zaczynając od podstawowych metod. Na przykład

old_purchases = (Purchase.objects
    .filter(date__lt=date.today(),type=Purchase.PRESENT).
    .excude(status=Purchase.REJECTED)
    .order_by('customer'))

jest doskonale poprawnym zapytaniem w Django ORM .

Chodzi o to, że masz jakiegoś konstruktora zapytań (w tym przypadku Purchase.objects), którego wewnętrzny status reprezentuje informacje o zapytaniu. Metody takie jak get, filter, exclude, order_bysą ważne i powrócić nowy kreator zapytań ze zaktualizowaną statusu. Obiekty te implementują interfejs iterowalny, dzięki czemu po wykonaniu iteracji zapytanie jest wykonywane, a wyniki zapytania są konstruowane do tej pory. Chociaż ten przykład pochodzi z Django, zobaczysz tę samą strukturę w wielu innych ORM.

Andrea
źródło
Nie widzę, jaką to ma przewagę nad czymś takim jak old_purchases = Purchases.query ("date> date.today () AND type = Purchase.PRESENT AND status! = Purchase.REJECTED"); Nie redukujesz złożoności ani niczego nie abstrakujesz, po prostu przekształcając operatory AND i OR w metody AND i OR - zmieniasz tylko reprezentację AND i OR, prawda?
Keith Palmer Jr.
4
Właściwie to nie. Abstrahujesz od SQL, co daje wiele korzyści. Po pierwsze, unikasz wstrzyknięcia. Następnie możesz zmienić bazową bazę danych, nie martwiąc się o nieco inne wersje dialektu SQL, ponieważ ORM obsługuje to za Ciebie. W wielu przypadkach możesz również umieścić backend NoSQL bez zauważenia. Po trzecie, te konstruktory zapytań są obiektami, które można przekazywać jak wszystko inne. Oznacza to, że Twój model może skonstruować połowę zapytania (na przykład możesz mieć kilka metod w najczęstszych przypadkach), a następnie można go dopracować w kontrolerze, aby obsłużył ...
Andrea
2
... najbardziej konkretne przypadki. Typowym przykładem jest zdefiniowanie domyślnego porządku dla modeli w Django. Wszystkie wyniki zapytania będą zgodne z tą kolejnością, chyba że określono inaczej. Po czwarte, jeśli kiedykolwiek będziesz potrzebować denormalizować swoje dane ze względu na wydajność, musisz jedynie zmodyfikować ORM zamiast przepisywać wszystkie zapytania.
Andrea
+1 W przypadku dynamicznych języków zapytań, takich jak wspomniany, oraz LINQ.
Evan Plaice,
2

Istnieje trzecie podejście.

Twój konkretny przykład wykazuje wykładniczy wzrost liczby potrzebnych metod wraz ze wzrostem liczby wymaganych funkcji: chcemy możliwości oferowania zaawansowanych zapytań, łącząc każdą funkcję zapytania ... jeśli to zrobimy przez dodanie metod, mamy jedną metodę dla podstawowe zapytanie, dwa jeśli dodamy jedną opcjonalną funkcję, cztery jeśli dodamy dwie, osiem jeśli dodamy trzy, 2 ^ n jeśli dodamy n funkcji.

Jest to oczywiście niemożliwe do utrzymania poza trzema lub czterema funkcjami, i jest nieprzyjemny zapach wielu blisko powiązanych kodów, które są prawie wklejone między metodami.

Można tego uniknąć, dodając obiekt danych do przechowywania parametrów i dysponując jedną metodą, która buduje zapytanie w oparciu o zestaw podanych parametrów (lub nie podanych). W takim przypadku dodanie nowej funkcji, takiej jak zakres dat, jest tak proste, jak dodanie obiektów ustawiających i pobierających zakres dat do obiektu danych, a następnie dodanie odrobiny kodu, w którym budowane jest sparametryzowane zapytanie:

if (dataObject.getStartDate() != null) {
    query += " AND (date BETWEEN ? AND ?) "
}

... i gdzie parametry są dodawane do zapytania:

if (dataObject.getStartDate() != null) {
    preparedStatement.setTime(dataObject.getStartDate());
    preparedStatement.setTime(dataObject.getEndDate());
}

Takie podejście pozwala na liniowy wzrost kodu w miarę dodawania funkcji, bez konieczności zezwalania na dowolne, nieparametryzowane zapytania.

Theodore Murdock
źródło
0

Myślę, że ogólną zgodą jest utrzymanie jak największego dostępu do danych w twoich modelach w MVC. Jednym z innych założeń projektowych jest przeniesienie niektórych bardziej ogólnych zapytań (tych, które nie są bezpośrednio związane z modelem) na wyższy, bardziej abstrakcyjny poziom, na którym można zezwolić na użycie go również w innych modelach. (W RoR mamy coś, co nazywa się frameworkiem). Jest jeszcze jedna rzecz, którą musisz wziąć pod uwagę i jest to możliwość utrzymania twojego kodu. W miarę rozwoju projektu, jeśli masz dostęp do danych w kontrolerach, będzie to coraz trudniejsze do wyśledzenia (obecnie mamy do czynienia z tym problemem w dużym projekcie) Modele, chociaż zaśmiecone metodami, zapewniają jeden punkt kontaktu dla każdego kontrolera, który może skończyć zapytania z tabel. (Może to również prowadzić do ponownego użycia kodu, co z kolei jest korzystne)

Ricketyship
źródło
1
Przykład tego, o czym mówisz ...?
Keith Palmer Jr.
0

Interfejs warstwy usług może mieć wiele metod, ale wywołanie bazy danych może mieć tylko jedną.

Baza danych ma 4 główne operacje

  • Wstawić
  • Aktualizacja
  • Usunąć
  • Pytanie

Inną opcjonalną metodą może być wykonanie operacji na bazie danych, która nie wchodzi w zakres podstawowych operacji DB. Nazwijmy to Wykonaj.

Wstawianie i aktualizacje można łączyć w jedną operację o nazwie Zapisz.

Wiele twoich metod to kwerendy. Możesz więc stworzyć ogólny interfejs, który zaspokoi większość natychmiastowych potrzeb. Oto przykładowy ogólny interfejs:

 public interface IDALService
    {
        DataTransferObject<T> Save<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Search<T>(DataTransferObject<T> Dto) where T: IPOCO;
        DataTransferObject<T> Delete<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Execute<T>(DataTransferObject<T> Dto) where T : IPOCO;
    }

Obiekt do przesyłania danych jest ogólny i zawiera wszystkie filtry, parametry, sortowanie itp. Warstwa danych byłaby odpowiedzialna za parsowanie i wyodrębnianie tego oraz konfigurowanie operacji w bazie danych za pomocą procedur składowanych, sparametryzowanych sql, linq itp. Tak więc SQL nie jest przekazywany między warstwami. Jest to zwykle czynność ORM, ale możesz wykonać własne i mieć własne mapowanie.

Więc w twoim przypadku masz Widżety. Widżety będą implementować interfejs IPOCO.

Tak więc w twoim modelu warstwy usług miałby getList().

Musiałby warstwę mapowania obsłużyć tranforming getListdo

Search<Widget>(DataTransferObject<Widget> Dto)

i wzajemnie. Jak inni wspominali, czasami dzieje się to za pośrednictwem ORM, ale ostatecznie kończy się to dużą ilością kodu typu „kocioł”, szczególnie jeśli masz setki tabel. ORM magicznie tworzy sparametryzowany SQL i uruchamia go w bazie danych. Jeśli tworzysz własne, dodatkowo w samej warstwie danych, maperzy będą potrzebni do skonfigurowania SP, linq itp. (Zasadniczo sql idzie do bazy danych).

Jak wspomniano wcześniej, DTO jest przedmiotem złożonym z kompozycji. Być może jednym z zawartych w nim obiektów jest obiekt o nazwie QueryParameters. Byłyby to wszystkie parametry zapytania, które byłyby ustawione i używane przez zapytanie. Innym obiektem byłaby lista zwróconych obiektów z zapytań, aktualizacji, ext. To jest ładowność. W takim przypadku ładunkiem byłaby lista widżetów.

Tak więc podstawową strategią jest:

  • Połączenia w warstwie serwisowej
  • Przekształć wywołanie warstwy usług do bazy danych przy użyciu pewnego rodzaju repozytorium / mapowania
  • Połączenie z bazą danych

W twoim przypadku myślę, że model może mieć wiele metod, ale optymalnie chcesz, aby wywołanie bazy danych było ogólne. Nadal kończy się wiele kodu mapowania typu kocioł (szczególnie z SP) lub magicznego kodu ORM, który dynamicznie tworzy dla Ciebie sparametryzowany SQL.

Jon Raynor
źródło