将SQLAlchemy ORM分离的实体公开为域对象

时间:2018-03-04 13:00:24

标签: python orm architecture sqlalchemy

这是一篇相当冗长的文章,所以我把它分成了一个主要问题,一个带代码的说明性例子,最后描述了我的想法和问题。

问题

我在整个应用程序中使用了分离的实体。这是将ORM实体暴露给应用程序其余部分的正确方法吗?如果不是,那有什么问题呢?

代码(设置)

数据库设置:

db_conn_string = "sqlite://"
# db_conn_string = "mysql+pymysql://root:root@33.33.33.1:33060/alchemist"
engine = create_engine(db_conn_string)
Base.metadata.bind = engine
DBSession = sessionmaker(bind=engine, expire_on_commit=False)

对于实体,我只是在我急切加载它们时才定义关系。这是为了确保当它们被用作分离实体时,您无法直接检索未加载的相关实体。

实体(截断):

class _Base(object):
    id = Column(Integer, primary_key=True)


Base = declarative_base(cls=_Base)


class Tree(Base):
    type = Column(String(200))

    branches = relationship("Branch", back_populates="tree", lazy='subquery')

    def __repr__(self):
        return "<Tree(id='{}', type='{}', branches='{}')>".format(self.id, self.type, self.branches)


class Branch(Base):
    name = Column(String(200))

    tree_id = Column(Integer, ForeignKey('tree.id'), nullable=False)
    tree = relationship("Tree", back_populates="branches", lazy='subquery')

    # Not defining leaves since this is not an eager loaded relationship and must be retrieved explicitly.
    # leaves = relationship("Leaf", back_populates="branch")

    def __repr__(self):
        return "<Branch(id='{}', name='{}', tree_id='{}')>".format(self.id, self.name, self.tree_id)


class Leaf(Base):
    size = Column(Integer)

    branch_id = Column(Integer, ForeignKey('branch.id'), nullable=False)
    # branch = relationship("Branch")  # Also not eager loaded

    def __repr__(self):
        return "<Leaf('{}')>".format(self.size)

我提供了一个存储库对象来执行基本的CRUD操作。然后,在存储库周围的上下文对象中管理会话事务范围:

class RepositoryContext(object):
    def __init__(self, entity_class):
        self.entity_class = entity_class

    def __enter__(self):
        self.session = get_session()
        return CrudRepository(self.entity_class, self.session)

    def __exit__(self, exc_type, exc_val, exc_tb):
        try:
            self.session.commit()
        except Exception:
            self.session.rollback()
            raise
        finally:
            self.session.close()


class CrudRepository(object):
    """
    CrudRepository implements (entity agnostic) CRUD operations.
    """

    def __init__(self, entity_class, session):
        self.entity_class = entity_class
        self._session = session

    def retrieve_all(self):
        return self._session.query(self.entity_class).all()

    def retrieve_by(self, **kwargs):
        return self._session.query(self.entity_class).filter_by(**kwargs).one_or_none()

    # Other CRUD methods here.

    def query(self):
        '''To perform custom queries'''
        return self._session.query(self.entity_class)

代码(用法)

所以基本用法如下:

with RepositoryContext(Tree) as repo:
    tree = repo.retrieve_by(id=1)
# Session is closed outside of context. tree is a detached instance.
len(tree.branches)  # This is ok, eager loaded

如果您想要为某个分支获取分支,我们无法使用tree.branches[0].leaves访问它,因为未定义该关系。因为我们不想急于加载它,所以我们必须单独检索它,如下所示:

with RepositoryContext(Leaf) as repo:
    branch = tree.branches[0]
    leaves = repo.query().filter_by(branch_id=branch.id).all()
# Now have leaves related to first branch. Also detached

同样,对于更新或刷新,您可以打开上下文并调用相关功能:

with RepositoryContext(Tree) as repo:
    repo.refresh(tree)  # This refreshes the tree state (discarding changes)

上下文

对于我们使用SQLAlchemy ORM的新项目,但我不确定如何正确使用ORM实体。

我一直被告知,域模型和数据库详细信息应尽可能地相互分离。然而,使用ORM,这意味着我经常需要将ORM实体映射到域模型对象,然后再返回,这首先会破坏使用ORM的全部要点。

沿着应用程序中使用ORM实体的路径,我想确保我不会产生意想不到的副作用。因此,我创建了一个紧密的事务范围来强制执行由存储库类显式处理的任何持久性。这意味着我在整个应用程序中使用分离的实体,并仅在事务中附加它们。

使用我当前的实现,我失去了SQLAlchemy提供的一些功能。这是访问尚未加载的相关数据(例如,从代码示例:branch.leaves未定义,因此您必须通过指定分支ID来添加它们。

如果帖子很长,我很抱歉,我尽可能地截断,同时让它保持可运行状态。

1 个答案:

答案 0 :(得分:0)

对于那些正在寻找更多信息/指南的人:Mike Bayer在https://groups.google.com/forum/#!topic/sqlalchemy/u9Igta1CYdo对我的问题做了非常详细的回复。