2016-03-16 27 views
6

Aplikacje często muszą łączyć się z innymi usługami (bazą danych, pamięcią podręczną, interfejsem API itp.). Dla zdrowia i DRY chcielibyśmy zachować wszystkie te połączenia w jednym module, aby reszta naszej bazy kodowej mogła współdzielić połączenia.Zarządzanie tworzeniem połączeń w Pythonie?

Aby zmniejszyć boilerplate, za użytkowanie powinno być proste:

# app/do_stuff.py 
from .connections import AwesomeDB 

db = AwesomeDB() 

def get_stuff(): 
    return db.get('stuff') 

i konfiguracja połączenia powinny być również prosta:

# app/cli.py or some other main entry point 
from .connections import AwesomeDB 

db = AwesomeDB() 
db.init(username='stuff admin') # Or os.environ['DB_USER'] 

sieci ram takich jak Django i kolby zrobić coś takiego, ale czuje się trochę niezgrabnie:

Connect to a Database in Flask, Which Approach is better? http://flask.pocoo.org/docs/0.10/tutorial/dbcon/

Jednym dużym problemem jest to, że chcemy odwoływać się do rzeczywistego obiektu połączenia zamiast do serwera proxy, ponieważ chcemy zachować tab-completion w iPython i innych środowiskach programistycznych.

To, co jest właściwym sposobem (tm) to zrobić? Po kilku iteracjach, oto mój pomysł:

#app/connections.py 
from awesome_database import AwesomeDB as RealAwesomeDB 
from horrible_database import HorribleDB as RealHorribleDB 


class ConnectionMixin(object): 
    __connection = None 

    def __new__(cls): 
     cls.__connection = cls.__connection or object.__new__(cls) 
     return cls.__connection 

    def __init__(self, real=False, **kwargs): 
     if real: 
      super().__init__(**kwargs) 

    def init(self, **kwargs): 
     kwargs['real'] = True 
     self.__init__(**kwargs) 


class AwesomeDB(ConnectionMixin, RealAwesomeDB): 
    pass 


class HorribleDB(ConnectionMixin, RealHorribleDB): 
    pass 

pokój dla poprawy: Ustaw początkowy __connection do rodzajowego ConnectionProxy zamiast Żaden, który przyciąga wszelki dostęp atrybutu i zgłasza wyjątek.

Zrobiłem sporo pogwizdywania tutaj na SO i różnych projektach OSS i nie widziałem czegoś takiego. Czuje się dość solidnie, choć oznacza to, że kilka modułów będzie tworzyło obiekty połączeń jako efekt uboczny w czasie importu. Czy to wysadzi w moją twarz? Czy istnieją inne negatywne konsekwencje tego podejścia?

Odpowiedz

0

pierwsze, design-mądry, to może brakować czegoś, ale nie widzę, dlaczego trzeba ciężki mixin + maszyny singleton zamiast po prostu definiowanie pomocnika tak:

_awesome_db = None 
def awesome_db(**overrides): 
    global _awesome_db 
    if _awesome_db is None: 
     # Read config/set defaults. 
     # overrides.setdefault(...) 
     _awesome_db = RealAwesomeDB(**overrides) 
    return _awesome_db 

Ponadto, istnieje jest to błąd, który może nie wyglądać obsługiwanym przypadków użycia, ale w każdym razie: jeśli się następujące 2 połączeń w rzędzie, byś błędnie uzyskać ten sam obiekt połączenia dwa razy, mimo że przeszedł różne parametry:

db = AwesomeDB() 
db.init(username='stuff admin') 

db = AwesomeDB() 
db.init(username='not-admin') # You'll get admin connection here. 

Łatwo to naprawić, używając dyktowanych połączeń Parametry wejściowe.

Teraz, na temat istoty pytania.

Myślę, że odpowiedź zależy od tego, w jaki sposób faktycznie są realizowane klasy "połączenia".

Potencjalne downsides o swoim podejściu widzę to:

  • W środowisku wielowątkowym można dostać problemy z unsychronized jednoczesnego dostępu do obiektu globalnego połączenia z wielu wątków, chyba że jest już bezpieczny wątku. Jeśli Ci na tym zależy, możesz nieco zmienić kod i interfejs i użyć zmiennej lokalnej wątku.

  • Co zrobić, jeśli proces rozwidla się po utworzeniu połączenia? Serwery aplikacji WWW zwykle to robią i mogą nie być bezpieczne, ponownie w zależności od podstawowego połączenia.

  • Czy obiekt połączenia ma stan? Co się stanie, jeśli obiekt połączenia stanie się nieważny (z powodu np. Błędu połączenia/limitu czasu)? Konieczne może być zastąpienie przerwanego połączenia nowym, aby zwrócić przy następnym żądaniu połączenia.

Zarządzanie połączeniami jest często już wydajnie i bezpiecznie wdrażane za pośrednictwem connection pool w bibliotekach klienta.

Na przykład, Redis-py Redis klient używa następującej realizacji:

https://github.com/andymccurdy/redis-py/blob/1c2071762ad9b9288e786665990083e61c1cf355/redis/connection.py#L974

Klient Redis następnie korzysta z puli połączeń tak:

Ponieważ klient Redis obsługuje to wszystko pod maską, możesz bezpiecznie robić to, co chcesz. Połączenia będą tworzone leniwie, dopóki pula połączeń nie osiągnie pełnej wydajności.

# app/connections.py 
def redis_client(**kwargs): 
    # Maybe read configuration/set default arguments 
    # kwargs.setdefault() 
    return redis.Redis(**kwargs) 

Podobnie, SQLAlchemy może używać connection pooling as well.

Podsumowując, moje zrozumienie jest, że:

  • Jeśli biblioteka klient obsługuje puli połączeń, nie musisz robić nic specjalnego do dzielenia połączenia między modułami a nawet nici. Można po prostu zdefiniować pomocnika podobnego do redis_client(), który odczytuje konfigurację lub określa parametry domyślne.

  • Jeśli biblioteka klienta udostępnia tylko obiekty połączeń niskiego poziomu, należy się upewnić, że dostęp do nich jest bezpieczny dla wątków i bezpieczny dla widżetów. Musisz również upewnić się, że za każdym razem, gdy zwrócisz poprawne połączenie (lub zgłosisz wyjątek, jeśli nie możesz ustanowić lub ponownie użyć istniejącego).

+0

Dzięki za dokładną odpowiedź! Naprawdę nie uważałem bezpieczeństwa nitek i wideł, na pewno o tym pomyślę. Re: parametry połączenia błąd, dobry połów i dobre rozwiązanie. Re: Pule klientów, również dobre, ale nawet z pulą połączeń, należy scentralizować inicjalizację i zagwarantować, że używasz tej samej puli w całej aplikacji. – knite

+0

Re: prosta funkcja pomocnika, początkowo rozważałem to podejście. Szybko staje się to denerwujące z powodu tego, że każdy moduł musi importować awesome_db * i * wywoływać go. – knite