架构

多数据库支持原理

在设计多数据库支持时, Flask-SQLAlchemy 的设计给了我很大启发, 但我并不太喜欢这种设计。原因有 2:

1. SQLAlchemy 自身就建立了 binds 机制,已经支持不同的 mapper 使用不同的 bind, 但 Flask-SQLAlchemy 并没有使用这套机制,这让我感到混乱。

2. Flask-SQLAlchemy 仅使用了一个 Model,通过在 Model 中设置 __bind_key__ 这个巧妙的方式来让不同的 Table 绑定到不同的数据库。 而我认为直接使用多个 Model,在定义 Table 的时候通过 Model 来区分,更容易理解。

Flask-SQLAlchemy 对 Metadata 做了一些手脚,自动加入 __bind_key__ 属性。

class BindMetaMixin(type):
    def __init__(cls, name, bases, d):
        bind_key = (
            d.pop('__bind_key__', None)
            or getattr(cls, '__bind_key__', None)
        )

        super(BindMetaMixin, cls).__init__(name, bases, d)

        if bind_key is not None and getattr(cls, '__table__', None) is not None:
            cls.__table__.info['bind_key'] = bind_key

然后,通过重写 Session.get_bind 方法,让 Query 可以通过 __bind_key__ 来找到对应的 Engine:

def get_bind(self, mapper=None, clause=None):
    """Return the engine or connection for a given model or
    table, using the ``__bind_key__`` if it is set.
    """
    # mapper is None if someone tries to just get a connection
    if mapper is not None:
        try:
            # SA >= 1.3
            persist_selectable = mapper.persist_selectable
        except AttributeError:
            # SA < 1.3
            persist_selectable = mapper.mapped_table

        info = getattr(persist_selectable, 'info', {})
        bind_key = info.get('bind_key')
        if bind_key is not None:
            state = get_state(self.app)
            return state.db.get_engine(self.app, bind=bind_key)
    return SessionBase.get_bind(self, mapper, clause)

pyape 采用了另一种方法。下面截取 pyape.db.DBManager 的一部分代码来说明:

class DBManager(object):

    def __build_binds(self) -> None:
        view = None
        if isinstance(self.URI, str):
            view = {None: self.URI}.items()
            self.default_bind_key = None
        else:
            view = self.URI.items()
            self.default_bind_key = list(self.URI.keys())[0]

        # 下面的三个引擎只需要创建一遍,在初始化的时间创建
        for name, uri in view:
            self.__add_bind(name, uri)

        self.__Session_Factory = sessionmaker(
            binds=self.__binds,
            autoflush=False,
            future=True
        )

    def __add_bind(self, bind_key: str, uri: str) -> bool:
        Model = self.set_Model(bind_key)
        if self.__binds.get(Model) is None:
            engine = self.__set_engine(bind_key, uri)
            self.__binds[Model] = engine
            return True
        return False

    def __set_engine(self, bind_key: str, uri: str) -> None:
        engine = create_engine(uri, future=True)
        # 保存 engine
        self.__engines[bind_key] = engine
        return engine

    def set_Model(self, bind_key: str=None):
        """ 设置并保存一个 Model。

        :param bind_key: 详见
            :ref:`pyape.db.DBManager.set_bind <pyape.db.DBManager>` 中的说明。
        """
        if self.__model_classes.get(bind_key):
            raise KeyError(bind_key)

        Model = declarative_base(name=bind_key or 'Model', metaclass=DefaultMeta)
        Model.bind_key = bind_key
        self.__model_classes[bind_key] = Model
        return Model

DBManager 在初始化时自动调用 __build_binds 方法,创建必须的 __Session_FactoryModel。 Model 是根据 pyape.toml 中的 [SQLALCHEMY.URI] 的值进行创建的。 若 URI 值为 dict,代表使用多个数据库, dict 的 key 就是 bind_key,使用这个 key 就可以获取到不同的 Engine。 若 URI 为 str,那么默认的 bind_key 就是 None,这也是一个合法的值。

在创建 __Session_Factory 时,使用 SQLAlchemy 提供的标准 binds 机制,将 mapper(即 Model) 和 Engine 对应起来。

要获取到不同数据库 Model,只需要使用 get_Model(bind_key) 即可。 若要获取到对应的 Engine,也可以直接使用 get_engine(bind_key)。 这简化了使用,也降低了理解成本。

具体案例请查看: 多数据库支持范例