Sytuacja kobiet w IT w 2024 roku
5.10.20214 min
Wiktor Feduń

Wiktor FeduńJunior Python Developer

Wzorce projektowe do pracy z bazą danych w Pythonie

Poznaj 2 wzorce projektowe, które ułatwią Ci pracę z bazami danych w Pythonie.

Wzorce projektowe do pracy z bazą danych w Pythonie

W świecie przyjaznych frameworków i pomocnych ORM-ów mogłoby się wydawać, że programista backendowy żyje w programistycznym raju. W swojej aplikacji ma wszystko poukładane, przystrzyżone i ogólnie rzecz biorąc - czyste. Niestety, rzeczywistość często jest daleka od tego ideału, a nasze bugi potrafią nas czasem bardzo zaskoczyć. Na szczęście nie wszystko stracone! Przecież mamy wzorce projektowe, które powstały właśnie dlatego, że wielu programistów zmagało się z podobnymi problemami. W tym artykule przybliżę Wam dwa wzorce w Pythonie, które pomogły mi w pracy z bazami danych.

Jednostka pracy

Pierwszym wzorcem, który pomógł mi rozwiązać wiele problemów z sesją bazy danych i transakcjami, jest wzorzec “jednostka pracy” (ang. unit of work). Jest on dla mnie dość ważny, ponieważ pomógł mi uporządkować obsługę bazy danych raz na zawsze. Przez moje  nieumiejętne zarządzanie sesjami zaczęły mi się pojawiać problemy z wykorzystywaną pamięcią (za dużo otwartych transakcji = wyczerpywanie pamięci). Dlatego z dużym entuzjazmem przyjąłem ten wzorzec i zacząłem go używać. 


Jednostka pracy jest po prostu abstrakcją operacji atomowych na bazie danych. W tym miejscu, zanim jeszcze przejdziemy do kodu, powinienem wprowadzić sformułowania, których będę używał:

  • Warstwa baz danych - to część aplikacji, która przechowuje szczegóły implementacyjne bazy danych - głównie zarządzanie sesją i operacje na bazie danych. 
  • Model domeny - jest to model biznesowy, rozwiązywanie problemu na wysokim poziomie abstrakcji, czyli to, co szef ma w głowie, kiedy myśli o swoim biznesie (raczej nie myśli, czy programiści używają postgresa czy mysql).
  • Operacje atomowe - albo zlecona operacja na bazie udaje się w całości albo wcale. Pozwala to uniknąć problemów, kiedy część operacji się wykonała i pozostajemy z niespójnym stanem.


Używamy abstrakcji po to, żeby oddzielić model domeny od warstwy baz danych, bo niedobrze jest gdy logika biznesowa zależy od szczegółów implementacji bazy danych. W zasadzie chcemy, aby było na odwrót - żeby nasz ORM zależał od logiki biznesowej (wiedział o niej). Wtedy możemy bez problemu zmienić bazę danych, bo model naszej domeny nie jest od niej bezpośrednio zależny. Z tego właśnie powodu na początek tworzymy abstrakcję dla podstawowych operacji na bazie danych. Przejdźmy do pierwszego kawałka kodu:

# Potrzebujemy silnika bazy i sesji
def _get_db_session() -> Session:
   session_maker = sessionmaker(bind=engine)
   return session_maker()



# Poniżej jest nasz unit of work, ale jeszcze niewykorzystany, użyjemy go dopiero przy wzorcu repozytorium

@contextmanager
def db_session_scope(commit: bool = True) -> Session:
   session = _get_db_session()
   try: 
       yield session
       if commit:
           session.commit()
   except Exception:
       session.rollback()
       raise
   finally:
       session.close()


Stworzyliśmy prosty manager kontekstu i to, co robi, to głównie zarządzanie sesją. Zauważcie, że na początku przyjmuje on argument commit, tak żebyśmy mogli go użyć przy zapisywaniu danych do bazy, a jeśli chcemy tylko coś wyciągnąć z bazy, to nie musimy używać commita. Kiedy transakcja się nie uda, sesja się “zwija”, co jest zachowaniem, którego byśmy oczekiwali, a na samym końcu jest zamykana. 

Istotnym jest, że “jednostka pracy” jest jedynym wejściem do naszej bazy. Pobiera ona sesje i później wszystkie metody związane z bazą korzystają właśnie z naszej “jednostki”. Będziemy jej używać razem z wzorcem repozytorium i dlatego często można się spotkać z określeniem “współpracownicy” (collaborators) dla tych dwóch wzorców, ponieważ mają na celu osiągnąć określony cel przez współpracę. Jakie są korzyści jednostki pracy?

  • Prosty interfejs do współpracy z bazą danych - upraszcza kod i zwiększa jego reużywalność.
  • Sposób na zapisywanie danych, tak że jeśli coś się nie uda, nie zostajemy z niespójnym stanem.  


Przejdźmy teraz do tego, jak użyć wzorca “jednostka pracy”. Użyjemy go w naszym “repozytorium”. 

Repozytorium

Wzorzec Repozytorium jest abstrakcją magazynu danych. Jest to kolejna abstrakcja, która ma za zadanie ukryć szczegóły implementacji bazy danych. Żeby zobrazować, czym jest ten wzorzec, możemy pomyśleć o najprostszym repozytorium, które ma tylko dwie metody: add(), służącą do dodawania nowych elementów, i get(), służącą do pobierania wcześniej dodanych elementów.

Jak wyglądałby nasz przykład? Użyję tutaj wymyślonej klasy object, ale jest to po prostu klasa z naszej domeny biznesowej. Dla sklepu może to być Product czy Client, dla aplikacji społecznościowej np. User, itd. 

def save(object: Object) -> None:
# Tutaj korzystamy z naszej jednostki pracy
    with db_session_scope(commit=True) as session:
        session.add(object)
       
def find_by_id(object: Object) -> None:
    with db_session_scope(commit=True) as session:
        return session.query(object).filter_by(id=object.id).one()
    
def find_all(object: Object) -> List:
    with db_session_scope(commit=True) as session:
        return session.query(object).all()


Nasze repozytorium powinno być modelowane zgodnie z potrzebami logiki biznesowej. Funkcji może być dużo i różnych, jednak ważne jest, aby korzystały z obsługi sesji, którą daje “jednostka pracy”, pozwoli to zredukować liczbę przyszłych problemów. Mimo że ORM daje nam możliwość w łatwy sposób napisać kolejne zapytania do bazy bez użycia jakichkolwiek wzorców, zalecam trzymanie się zasad dostępu do danych w naszej warstwie usług. 

Plusy wzorca “repozytorium”:

  1. Pomaga zorganizować kod
  2. Oddziela model domeny (logikę biznesową) od szczegółów implementacji bazy danych 
  3. Korzysta z “jednostki pracy” i tym samym pozwala zasypiać spokojnie, nie myśląc o sesjach i transakcjach naszej bazy

Możecie zapytać - bardzo fajnie, są jakieś minusy? 

Minusem jest fakt, że posługiwanie się tymi wzorcami na początku może zająć trochę więcej czasu i wymagać więcej linijek kodu, ale wraz ze wzrostem aplikacji, zaowocuje szybszym developmentem, mniejszą ilością błędów, spójnością kodu i przewidywalnością pracy naszej bazy danych. 

Poznawanie świata dobrych praktyk potrafi być wymagające, ale mam nadzieję, że mój artykuł pomoże Wam choć trochę go zrozumieć i zachęci do samodzielnego zgłębiania wiedzy. Ten artykuł napisałem po przeczytaniu książki “Architecture Patterns in Python”, która jest bardzo dobrym wprowadzeniem do tematu wzorców i którą od serca polecam.

<p>Loading...</p>