在Python中管理连接创建?

kni*_*ite 6 python architecture design-patterns database-connection

应用程序通常需要连接到其他服务(数据库,缓存,API等).为了理智和DRY,我们希望将所有这些连接保存在一个模块中,以便我们的代码库的其余部分可以共享连接.

为了减少样板,下游使用应该很简单:

# app/do_stuff.py
from .connections import AwesomeDB

db = AwesomeDB()

def get_stuff():
    return db.get('stuff')
Run Code Online (Sandbox Code Playgroud)

设置连接也应该很简单:

# 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']
Run Code Online (Sandbox Code Playgroud)

像Django和Flask这样的Web框架做了类似的事情,但感觉有点笨重:

连接到Flask中的数据库,哪种方法更好? http://flask.pocoo.org/docs/0.10/tutorial/dbcon/

这个问题的一个重要问题是我们需要引用实际的连接对象而不是代理,因为我们希望在iPython和其他开发环境中保留tab-completion.

那么正确的方法(tm)是做什么的?经过几次迭代,这是我的想法:

#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
Run Code Online (Sandbox Code Playgroud)

需要改进的地方:将初始__connection设置为通用ConnectionProxy而不是None,这会捕获所有属性访问并引发异常.

我已经在SO和各种OSS项目上做了相当多的讨论,并没有看到这样的事情.虽然它确实意味着一堆模块将在导入时将实例化连接对象作为副作用,但它感觉非常可靠.这会在我脸上爆炸吗?这种方法还有其他负面影响吗?

emi*_*hev 1

首先,在设计方面,我可能会遗漏一些东西,但我不明白为什么你需要重型 mixin+singleton 机制,而不是像这样定义一个助手:

_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
Run Code Online (Sandbox Code Playgroud)

此外,还有一个错误可能看起来不像受支持的用例,但无论如何:如果您连续进行以下 2 个调用,即使您传递了不同的参数,您也会错误地获得相同的连接对象两次:

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

db = AwesomeDB()
db.init(username='not-admin')    # You'll get admin connection here.
Run Code Online (Sandbox Code Playgroud)

一个简单的解决方法是使用输入参数上的连接字典。

现在,关于问题的本质。

我认为答案取决于您的“连接”类的实际实现方式。

我认为您的方法的潜在缺点是:

  • 在多线程环境中,您可能会遇到从多个线程对全局连接对象进行不同步并发访问的问题,除非它已经是线程安全的。如果您关心这一点,您可以稍微更改一下代码和接口并使用线程局部变量。

  • 如果创建连接后进程分叉怎么办?Web 应用程序服务器往往会这样做,但它可能不安全,这又取决于底层连接。

  • 连接对象有状态吗?如果连接对象变得无效(由于即连接错误/超时),会发生什么?您可能需要用新连接替换断开的连接,以便在下次请求连接时返回。

连接管理通常已经通过客户端库中的连接池高效且安全地实现。

例如,redis-py Redis客户端使用以下实现:

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

然后,Redis 客户端使用连接池,如下所示:

因此,由于 Redis 客户端在后台处理所有这些事情,因此您可以安全地直接执行您想要的操作。连接将被延迟创建,直到连接池达到满容量。

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

同样,SQLAlchemy也可以使用连接池。

总结一下,我的理解是:

  • 如果您的客户端库支持连接池,则无需执行任何特殊操作即可在模块甚至线程之间共享连接。您可以只定义一个类似于redis_client()读取配置的帮助程序,或指定默认参数。

  • 如果您的客户端库仅提供低级连接对象,您将需要确保对它们的访问是线程安全和分叉安全的。此外,您需要确保每次返回有效连接(或者如果无法建立或重用现有连接,则引发异常)。