应用程序通常需要连接到其他服务(数据库,缓存,API等)。为了理智和DRY,我们希望将所有这些连接保存在一个模块中,以便我们的代码库的其余部分可以共享连接。
为了减少样板,下游使用应该很简单:
# app/do_stuff.py
from .connections import AwesomeDB
db = AwesomeDB()
def get_stuff():
return db.get('stuff')
设置连接也应该很简单:
# 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']
像Django和Flask这样的Web框架做了类似的事情,但感觉有点笨拙:
Connect to a Database in Flask, Which Approach is better? 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
改进空间:将初始__connection设置为通用ConnectionProxy而不是None,这会捕获所有属性访问并引发异常。
我已经在SO和各种OSS项目上做了相当多的讨论,并且没有看到类似的东西。虽然它确实意味着一堆模块将在导入时将实例化连接对象作为副作用,但它感觉非常可靠。这会在我脸上爆炸吗?这种方法还有其他负面影响吗?
答案 0 :(得分:0)
首先,在设计方面,我可能会遗漏一些东西,但我不明白为什么你需要重型mixin +单例机器而不是像这样定义一个帮助器:
_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
此外,有一个错误可能看起来不像支持的用例,但无论如何:如果连续进行以下2次调用,即使您传递了不同的参数,也会错误地获取相同的连接对象:
db = AwesomeDB()
db.init(username='stuff admin')
db = AwesomeDB()
db.init(username='not-admin') # You'll get admin connection here.
一个简单的解决方法是使用键入输入参数的连接词典。
现在,就问题的本质而言。
我认为答案取决于你的“连接”类的实际实现方式。
我看到你的方法的潜在缺点是:
在多线程环境中,您可能会遇到来自多个线程的对全局连接对象的非同步并发访问问题,除非它已经是线程安全的。如果您关心这一点,您可以更改代码和接口,并使用线程局部变量。
如果创建连接后进程分叉怎么办? Web应用程序服务器倾向于这样做,它可能不安全,同样取决于底层连接。
连接对象是否有状态?如果连接对象变得无效(由于连接错误/超时)会发生什么?您可能需要用新的连接替换断开的连接,以便在下次请求连接时返回。
通过客户端库中的connection pool,连接管理通常已经高效安全地实现。
例如,redis-py Redis客户端使用以下实现:
Redis客户端然后使用连接池,如下所示:
因此,由于Redis客户端处理所有这些问题,您可以安全地直接执行所需操作。连接将被延迟创建,直到连接池达到满容量。
# app/connections.py
def redis_client(**kwargs):
# Maybe read configuration/set default arguments
# kwargs.setdefault()
return redis.Redis(**kwargs)
同样,SQLAlchemy可以使用connection pooling as well。
总结一下,我的理解是:
如果您的客户端库支持连接池,则无需执行任何特殊操作即可在模块甚至线程之间共享连接。您可以定义一个类似于redis_client()
的帮助器来读取配置,或者指定默认参数。
如果您的客户端库仅提供低级别连接对象,则需要确保对它们的访问是线程安全的和fork安全的。此外,您需要确保每次返回有效连接(或者如果您无法建立或重复使用现有连接,则引发异常)。