Obiekt dostępu do danych (DAO)

Kontynuacja treści o warstwie dostępu do danych. Dzisiaj powiem o Data Access Objects i jak mają się one do omówionych wcześniej repozytoriów. Wspomnę o różnych podejściach przy pobieraniu danych i odpowiem na pytanie czy w ogóle potrzebujemy DAO.


Data Access Object

W dzisiejszych czasach bardzo często widuję repozytoria, które tworzone są na bardzo niskim poziomie abstrakcji. Poprzednim razem pokazywałem przykład przerośniętego repozytorium, które obsługiwało generację zapytań do bazy danych. Nie jest ważne to, czy był tam czysty SQL, czy też używane były metody generujące, pochodzące z ORM. Problemem jest fakt, że taka logika nie leży w gestii repozytorium. Niskopoziomowe generowanie zapytań i ich wykonywanie powinno być obsługiwane poprzez obiekty dostępowe - Data Access Objects. Następnie, stworzone DAO powinno być wstrzyknięte do repozytorium, które udostępni przyjemny w obsłudze danych interfejs. 

Jak wygląda DAO?

Obiekt dostępowy może bazować na zapytaniach SQL, bądź metodach ORM. Poniżej ukazałem przykład DAO, który używa funkcji ORM do generowania i uruchamiania zapytań do bazy.

export class TagDAO {
  private static QUERY_ALIAS = 'tags';

  findOneById = (id: string): Promise<TagEntity> =>
    TagEntity.createQueryBuilder(TagDAO.QUERY_ALIAS)
      .leftJoinAndSelect(`${TagDAO.QUERY_ALIAS}.site`, 'site')
      .leftJoinAndSelect('site.pages', 'pages')
      .where(`${TagDAO.QUERY_ALIAS}.id = :id`, { id })
      .getOne();

  findAll = (): Promise<TagEntity[]> =>
    TagEntity.createQueryBuilder(TagDAO.QUERY_ALIAS)
      .leftJoinAndSelect(`${TagDAO.QUERY_ALIAS}.site`, 'site')
      .leftJoinAndSelect('site.pages', 'pages')
      .getMany();
}
W przypadku braku ORM lub konieczności napisania zapytania ręcznie, takie DAO może przyjąć następującą postać. 

import { getManager } from 'typeorm';

export class TagDAO {
  private readonly entityManager = getManager();

  findOneById = (id: string): Promise<any> => 
    this.entityManager.query(`select * from tags where id = :id`, [id]);
}
DAO wykorzystujące czyste zapytania SQL zazwyczaj zwraca raw-object. Koniecznym okazać się więc może mapowanie surowych obiektów do postaci encji. Takie mapowanie możemy zrobić w ciele metody repository, przy użyciu dodatkowego mappera, bądź konstruktora encji (przykład tutaj).

Sam obiekt dostępowy powinien być używany jedynie poprzez repozytorium, co podkreśli jego najniższe położenie w warstwie dostępu do danych. Repozytorium zaś możemy wstrzykiwać w kontrolerach czy serwisach, ponieważ udostępnia ono spójny i jasno zdefiniowany interfejs, służący do odczytu lub modyfikacji danych.

Używać DAO czy Repository?

Obiekty dostępowe w zamyśle mają dostarczać niskopoziomową obsługę dla np. baz danych. W innych technologiach możemy spotkać się z podejściem, gdzie DAO istnieją samoistnie (bez repozytoriów) i są wstrzykiwane bezpośrednio do serwisów. Z drugiej strony barykady spotkać możemy pominięcie Data Access Object i sprowadzenie repozytoriów do obsługi ich zadań. Takie podejście widoczne jest między innymi w TypeORM, z którego skorzystałem w swoich przykładach. 

Po takim słowotoku można zadać sobie pytanie, czy repozytoria nie są więc tym samym co DAO? Odpowiedź będzie bardzo prosta: nie, nie są. Granica między nimi jest bardzo cienka, a różnica wynika bardziej z technologii i narzędzi, które używamy. Obiekt dostępowy daje nam więcej swobody, ponieważ może operować na więcej niż jednym typie danych. Wiadomo jednak, że taka wolność bardzo szybko może przerodzić się w nieład. Krąży też mit, że repozytorium musi być zawsze zbudowane na DAO, co nie jest prawdą, a pokazane przeze mnie przykłady to potwierdzają.

Zaraz, zaraz, najpierw przez dwa wpisy mówisz, aby budować repozytoria, które używają Data Access Objects, a teraz temu przeczysz? Stosowałem wiele podejść, samych DAO, samych repozytoriów i zauważyłem, że najczystszy i łatwo utrzymywalny kod powstaje, jeśli repozytoria są małymi klasami, które nie dotykają bezpośrednio niskiego poziomu. To odseparowanie pozwala na dobrze widoczną transparentność poszczególnych warstw oraz może ułatwić testowanie. Ostateczna decyzja, jak zaimplementować DAL (Data Access Layer) zależy oczywiście od Ciebie. 

Podsumowując




Polecane wpisy:

DRY: Don't Repeat Yourself

Zasada DRY, to po przełożeniu na język polski: nie powtarzaj się. Brzmi bardzo banalnie, jednak dosyć często okazuje się, że mamy problem z jej stosowaniem w kodzie. Dziś dowiesz się, jak nie łamać tej reguły, a co za tym idzie nie powtarzać się.

Sprawdź ten wpis

YAGNI: You aren't gonna need it

YAGNI to kwintesencja zasad clean code. Dotyczy ona bezużyteczności kodu, a dokładniej, konieczności usuwania tych fragmentów, które nie są potrzebne. W myśl "You aren't gonna need it" nie powinniśmy tworzyć niczego więcej, niż to, co jest potrzebne.

Sprawdź ten wpis

KISS: keep it simple, stupid!

Zasada KISS: "Keep it simple, stupid", może zostać dosłownie przetłumaczona na: "rób to prosto, głupku". Mówi ona o tym, abyśmy tworzyli kod w jak najprostszy i najbardziej czytelny sposób. Już dziś sprawdź, czego się wystrzegać, aby spełniać KISS.

Sprawdź ten wpis

Wzorce projektowe

Wzorce projektowe zostały stworzone po to, aby nie wymyślać przysłowiowego koła na nowo. Znajomość wzorców projektowych i umiejętność ich stosowania pozwala na szybkie rozwiązywanie problemów. Wpis ten radzi, jakie wzorce zastosować u siebie.

Sprawdź ten wpis

Wzorzec singleton

Kolejny kreacyjny wzorzec projektowy omawiany na łamach tego bloga: singleton. Wzorzec dookoła którego narosło wiele mitów i legend. Dziś o tym dlaczego singleton jest antywzorcem, jakie problemy powoduje oraz kiedy warto po niego sięgnąć.

Sprawdź ten wpis

Autor wpisu:

Gabriel Ślawski

Fanatyk czystego i prostego kodu. Zwolennik podejść DDD oraz Modular Monolith. Na codzień pracuje jako programista i architekt. Po godzinach spełnia się w projektach open source, udziela się na blogach oraz czyta książki o kosmosie i astrofizyce.