scribble

ottocho's blog

Home About GitHub

20 Oct 2013
Date Source Architectural Patterns

数据源架构模式小结


终于有了个自由的一天。

这篇文章主要是 EAA 的阅读笔记(主要是第十章关于数据库架构模式的内容),加上我的理解与翻译,同时以 Python 和 Ruby 代码来尝试示例解释。

This is the note of book Patterns of Enterprise Application Architecture, mainly about the chapter 10: Date Source Architectural Patterns.

但是,Flower此书以 C#/Java 这种强类型语言作为解释(这和我目前暂时的 Ruby/Python 经验有很大的理念和设计上的区别、利用了 C#/Java 的一些特定库或者框架(如DataSethibernate 等我没有实践过也因此没有真正理解的)、使用了 UML 作为说明图(这也是我现在不太熟悉的东西)。我只是略懂C#/Java,没有大型项目经验。因此,在很多细节上,我可能会有些理解的偏颇。

本文使用 Python 来说明 EAA 中提及的范形。代码参考原书中的 C#/Java 代码。代码未必能直接执行,但应该是能说明范形的。

本书的目标应该更偏向于超大型软件项目的架构设计,但是,在中小型项目,顾及了敏捷性和项目管理的其他原则的同时,借鉴这些设计范式是有很多好处的。

前言

通常而言,我们在讨论架构的时候,我们在讨论什么。这也是 EAA 全书的目的。

This book is thus about how you decompose an enterprise application into layers and how these layers work together.

简而言之:分层。

My general advice is to choose the most appropriate form of separation for your problem but make sure you do some kind of separation, at least at the subroutine level.

最普遍的情况下,我们会去划分为三层:Presentation Layer, Domain Logic Layer and Data Source Layer

Presentation Layer (表现层)

Provision of services, display of information (e.g., in Windows or HTML, handling of user request (mouse clicks, keyboard hits), HTTP requests, command-line invocations, batch API)

Domain Logic Layer (逻辑层、业务层)

(Business logic) that is the real point of the system

Data Source Layer (数据层)

Communication with databases, messaging systems, transaction managers, other packages

本篇读书笔记在于总结数据层的内容

关于 Data Source

Data source logic is about communicating with other systems that carry out tasks on behalf of the application. These can be transaction monitors, other applications, messaging systems, and so forth. For most enterprise applications the biggest piece of data source logic is a database that is primarily responsible for storing persistent data.

在所谓的 pattern 设计上,必须认真想清楚的是:你的层间界限是什么?如果你定义了某层为逻辑层(假设在 Python 代码上,它是一个 object ),那么什么是所谓的 逻辑 ? (即 domain 或 business);与数据层对比合作的时候,你如何区分数据操作逻辑操作?数据操作到什么界限才不被视为逻辑操作?在我的实践中,我发现这是最困难的。

或许这也是 COC 的优势所在:不同人对分层与架构会有不同的理解从而做出不同的配置和实现,而一个确定的 convention 直接减少了沟通和管理的成本,代价是学习成本,和一个可能的烂设计(convention 不合理)带来的后续的非常多的问题。

Data Source Architectural Patterns

数据源架构模式,通常而言,我们的数据源也就是数据库。

有四种模式:
Table Data Gateway
Row Data Gateway
Active Record
Data Mapper

Table Data Gateway

什么是 Table Data Gateway

要理解 Table Data Gateway,先要理解 Gateway。一句话概括 Gateway :一个封装了对外部资源或者系统访问的对象。关键词是:外部。

Gateway: An object that encapsulates access to an external system or resource. 或者: One instance handles all the rows in the table. via: http://martinfowler.com/eaaCatalog/gateway.html

顾名思义。首先它是一个 Gateway,用以访问外部系统数据源;其次,它是一个 Table Data 的 Gateway 。也就是说,这个 Gateway 的外部源是 Table Data。

这样说似乎都是废话,但是就是这么一回事:

An object that acts as a Gateway to a database table. One instance handles all the rows in the table. ( “handles” here typically refers to CRUD).

一个 Table Data Gateway 有一个简单的接口(interface),这个 interface 由很多 method 组成,这些 method 是简单把参数弄成一个SQL语句,执行然后返回结果。我的理解: Table Data Gateway 是一个仅仅将需要的参数转化为 SQL 的进行数据操作的接口对象,它是基本没有业务逻辑(Domain Logic)的。

实际上就是这个 Gateway 是个数据访问的接口而已。所谓的 handles all the rows in the table 就是废话,SQL 语句就是操作 table 的行。

而在实现的时候,显然这个 data table gateway 算是非常纯粹的 Data Source 层级了,数据层,没有做业务逻辑操作。而通常而言,对应 Data Source 层级(也就是所谓的数据层)使用 data table gateway 的情况下, Domain Logic 层级(也就是所谓的业务层),通常都会使用的模式为 Table Module

Table Module : A single instance that handles the business logic for all rows in a database table or view.

基本来说,Table Data Gateway 是最简单的数据接口范式了。在使用 Table Data Gateway 作为数据层的模型时,逻辑层很多时候就是 Table Module 范式。

以下是一个非常简单直接的范例:

class UserGateway(object):
    '''
        Data Gateway for users
    '''

    conn = MySQLdb.connect(*args)

    @classmethod
    def get_user(class_):
        #
        sql = 'select id, name, mail from users'
        self.conn.fetch(sql)
        pass

    @classmethod
    def update_user(class_, id, **args):
        sql = 'update users set xxxx '
        pass

    @classmethod
    def delete_user(class_, *args):
        sql = 'delete from users where xxx '
        pass

使用如下:

# 获取第一个用户(id为1)
first_user = UserGateway.get_user(id=1)

# 更新 id 为 1 的人的名字和邮箱
UserGateway.update_user(1, name='ottocho', email='ottozhuo@tencent.com')

# 删除名字为 ottocho 的用户
UserGateway.delete_user(name='ottocho')

在使用 Table Data Gateway 范式进行数据操作时,还会需要考虑两个问题:

一,这个 Gateway 对象不一定只操作一个数据表。它可以同时对多个表进行读写。例如 UserGateWay 它除了读写 Users 表,还可以读写 Groups,从而可以获取到用户的分组信息情况。

二,这个 Gateway 的数据返回结构、接口访问接口结构是不受规定的,即:Table Data Gateway范式定义的只是对象对数据表的所有行的、不包含任何业务逻辑的统一访问接口。所以在具体程序设计上,接口和数据结构是需要根据业务和使用语言而具体确定的。

Row Data Gateway

在对 Table Data Gateway 的理解上,来对比理解 Row Data Gateway

Table Data GatewayTable 为单位进行 Gateway 封装,而 Row Data GatewayRow 为单位进行 Gateway 封装。Row Data Gateway 对象对每行数据保持一个对象。因此,此对象控制一个行,对此行的读写由此对象负责。

而显然,构建一个操作某个 RowRow Data Gateway 对象前,你需要获得这个 Row。因此,基本在使用此范式之时,还会去实现一个 Finder,去获取此 Row Data Gateway 。即:

  +-----------------------+              +-----------------------+
  |     User Finder       |              |     User Gateway      |
  |-----------------------|              |-----------------------|
  |  get_user(id)         | -----------> |  name                 |
  +-----------------------+              |  mail                 |
                                         +-----------------------+
                                         |  insert               |
                                         |  update               |
                                         |  delete               |
                                         +-----------------------+

以下是 Python 版的一个 Row Data Gateway 范例。

class UserHelper(object):
    '''
        Helper to get `Row Data Gateway`
    '''
    conn = db.conn(*args)

    @classmethod
    def get_user(class_, id):
        sql = 'select id, name, mail from users'
        user_data = class_.conn.get(sql)
        name = user_data.name
        mail = user_data.mail
        user_gateway = UserGateWay(id=id, name=name, mail=name)
        return user_gateway

class UserGateWay(object):

    def __init__(self, id, name, mail):
        self.id = id
        self.name = name
        self.mail = mail
        self.conn = db.conn(*args)

    def update(self, name=None, mail=None):
        # 此处可以只更新有给出的参数
        sql = 'update users set XXXX where id=%s'
        self.conn.execute(sql, *(xx, xx, self.id))
        self.name = name
        self.mail = mail

    def delete(self):
        sql = 'delete from users where id=%s'
        self.conn.execute(sql, *(id,))

使用如下:

user = UserHelper.get_user(1)

# 更新名字为 ottozhuo
user.update(name='ottozhuo')
# 更新邮箱
user.update(mail='ottozhuo@tencent.com')
# 删除了此用户
user.delete()

这个范式我当时花了好些时间来理解。我认真思考其难以理解的原因,应该是因为受到我对 MySQL 的理解的干扰。因为 SQL 语句并不是以 Row 为单位进行的操作(即使以 where id=%d 的限制,也更适合理解为单个元素的集合)。

这样的对象通常会把数据存在内存一份。另外,多个类似的 Row,可能需要执行多次的 SQL,而实际上某些情况下可能并不需要多次都 SQL操作。EAA 中的说明是:使用 transation script 的情况下,最适合用这样的泛型。

http://martinfowler.com/eaaCatalog/transactionScript.html

很多应用的操作可以视为一系列的事务(transaction)。每此操作即为 client 和 server 的一系列操作。有的时候,一次操作可能只是取得数据和展示;而有的时候则会包含很多验证、计算(甚至是其他,如加锁等等)。

Transaction Script 将这样的逻辑视为一个过程(procedure),每一个事务有其事务脚本(Each transaction will have its own Transaction Script.)

我的理解是:Transaction ScriptRow Data Gateway 的结合逻辑在于,在事务性处理上,其核心在于对指定一行数据的操作。因此在不包含业务逻辑的数据层,只封装对一行数据操作,而 Transaction Script 则对指定的 事务数据行 进行具体的逻辑操作。

实际上,除了 transation script 以外,我还想到一个情景下适合用这样的泛型。

上文其实事实基础上是以关系型数据库为数据源进行的说明。假设此时数据来源并不是MySQL等关系型数据库,而是这样的一个文件型数据库,在这种情况下或许会更容易理解 Row Data Gateway

此文件数据库存放用户信息,用户信息以纯文件存储在用户 id 为名的目录下。因此对用户数据的读写,即为 对用户id同名目录下文件的读写。如下:

~/data/
    ├── 1000000
    │   ├── account
    │   │   ├── address
    │   │   ├── id
    │   │   └── name
    │   ├── connection
    │   │   ├── company
    │   │   ├── family
    │   │   └── friends
    │   └── image
    │       ├── kljafda1c.gif
    │       └── lkadsf8af.gif
    ├── 1000100
    │   └── .............
    ├── ...............
    └── ...............

此时,我们把 id 为名的目录理解为所谓的 Row,而把 data 这个目录(Row的集合)理解为所谓的 table。显而易见,如果对用户数据进行操作,Row Data Gateway 对每个 Row 以一个对象进行操作的范式是非常适合这种情况的。

Active Record

active record in wiki

active record in eaa catalog

如果理解了 Row Data Gateway,那么应该会比较好理解 Active Record

如果以上面的 Python 为基础,那么 User 的 Active Record 类如下:

+-----------------------+
|         User          |
|-----------------------|
|  name                 |
|  mail                 |
+-----------------------+
|  insert               |
|  update               |
|  delete               |
|                       |
|  is_manager           |
|  privileges           |
+-----------------------+

Row Data Gateway 仅仅包含数据读写(contains only database access,即纯数据层),而 Active Record 除了数据读写还包含了业务逻辑(contains both data source and domain logic,即包含了一定业务逻辑的数据层)。

对比在小节 Row Data Gateway 中的 User 模块,它的区别在于有 is_mangagerprivileges。实际上他们的区别不在于多实现两个函数,而在于这两个函数应该算是业务逻辑,不能算是纯粹的数据读写

一个 Active Record 对象的属性,应该和它的数据库表域对应。此对象通常会有数据表域同名的属性。如对象 user 拥有 name, mail属性,对应的数据表 users 同样会有 name, mail 域。

关系型数据库往往通过外键来表述实体的关系,AR 在数据源层面上也将这种关系映射为对象的关联和聚集。

一个 Active Record 范式的类,通常会定义完成以下工作的方法:

  • 以 SQL 结果集构建出一个 ar 对象
  • 构建一个 ar 对象,用以延后写入数据表
  • 静态检索方法,封装 SQL,直接返回 ar 对象
  • 以 ar 对象进行数据库的更新和插入
  • 获取、读取表的域值( Get and set the fields )
  • 实现业务逻辑

谈到 Active Record 就不可能不提及 Ruby on RailsActive Record 库了。实际上在搜索此单词的时候,ror 的官方文档条目更会在 eaa 对此名词定义之前。

以下用一个超级简单的范例来简要说明 Ruby on RailsActive Record

数据库如下,这是一个非常基本的用户及分组的数据库设计。

mysql> select * from users;
+----+-------+----------+----------------------------------+
| id | name  | fullname | password                         |
+----+-------+----------+----------------------------------+
|  0 | otto  | ottocho  | d15deb2e529967afa9d27014090ddf2c |
|  1 | winky | winkylin | bcb6b789146e57acd5d1fe9415a52d1d |
+----+-------+----------+----------------------------------+

mysql> select * from groups;
+----+------+
| id | name |
+----+------+
|  0 | ops  |
|  1 | dev  |
+----+------+

mysql> select * from group_users;
+----+---------+----------+
| id | user_id | group_id |
+----+---------+----------+
|  1 |       0 |        0 |
|  2 |       0 |        1 |
|  3 |       1 |        1 |
+----+---------+----------+

mysql> select users.name as user_name, groups.name as group_name
    -> from users
    -> join group_users on users.id = group_users.user_id
    -> join groups on groups.id = group_users.group_id;
+-----------+------------+
| user_name | group_name |
+-----------+------------+
| otto      | ops        |
| otto      | dev        |
| winky     | dev        |
+-----------+------------+

利用 ar 执行的行数据操作非常简单。而且在定义 ar 对象的时候,在里面定义了业务逻辑。代码可读性极强,基本都不需要太多解释。

默认继承 ActiveRecord::Base 的类,将驼峰的类名转化为下划线连接的小写单词的表名。而在 many to many的数据库范式情况下,默认表间关系表(如定义 user 与 group 的多对多关系),默认将名词以字典序排序,下划线连接并转为复数,即:users表与groups的多对多关系默认定义在group_users

ruby 代码如下:

# 定义了 对应 `users` 数据表的 `User`
class User < ActiveRecord::Base
    has_and_belongs_to_many :groups

    def is_manager?
        name == 'otto'
    end
end

# 定义了 对应 `groups` 数据表的 `Group`
class Group < ActiveRecord::Base
    has_and_belongs_to_many :users
end

user = User.find(0)
if user.is_manager?
    puts "this user is a manager"
    puts "his name is #{user.name}"
    puts "his full name is #{user.fullname}"
end
# output:
# this user is a manager
# his name is otto
# his full name is ottocho

# user.groups 代表了: user 对象所拥有的group(即用户的所有组)
user.groups.each do |group|
    puts group.name
end
# output:
# ops
# dev

在上例中,Active Record 用以构建 ar 对象(继承了类 ActiveRecord::BaseUser 类和 Group 类),以此 ar 对象进行 CURD ,并且对象间关系直接对应于数据库的关系链接(这意味着逻辑层和数据层的强结合)。

当然,得益于 ruby 的语言特性,使得 ruby 的 active_record 库有更强大的动态性(如自动以库结构自动生成 ar 对象的一系列 CURD 方法),这不是范式所要求和规定的(但是基本 AR 库都会实现类似的自动工作)。理解 Active Record 应该在于:带有业务逻辑的行级数据操作,一个对象对应一行数据,对象的属性即为此行数据的域内容,对象的方法即对此行数据进行操作。而正由于数据层与业务层的结合,使得在数据库设计与业务对象的设计上有了很强的对应,对象间关系直接对应数据表间关系。

Active Record 适合在业务逻辑(domain logic)不复杂的情况下。而如果对象间关系不仅仅限于此,还有更复杂的逻辑(如direct relationships, collections, inheritance),往往需要使用分离数据源的领域模型,此时不适合用 Active Record

当需要更新数据库表设计、更新业务逻辑的时候,Active Record会遇到一些困难(如上,当需要更新usergroup关系时,此设计需要很麻烦的重构。

Data Mapper

定义非常清晰,也非常容易理解。为了彻底隔离业务和数据的操作,Mapper 我们需要关注的是,需要如何去 mapper。

  +----------------+
  |     User       |
  |----------------|       +------------------+
  |  name          |       |    User Mapper   |         +------------+
  |  fullname      |       |------------------|         |            |
  |  mail          | <---+ |  insert          | +---->  |  Database  |
  |----------------|       |  update          |         |            |
  |  isManager     |       |  delete          |         +------------+
  |  getSalary     |       +------------------+
  +----------------+

这是最复杂,但最灵活的一种架构模式。使用 Data Mapper,业务逻辑的对象可以与数据库结构完全的分离,他们无需知道数据库的结构和数据更新的细节。Data Mapper 对于那些无法在关系型数据库表示的对象系统特性非常有效(最典型的,如继承关系)。Data Mapper 是内存中对象与数据库之间的媒介。它负责双方之间的数据传输。

Data MapperGateway 最大的区别在于依赖和控制的位置。在 Gateway 中,应用逻辑层需要了解数据库中的数据结构,而在 Data Mapper 中,应用逻辑无需了解数据库设计,提高了应用层(business logic)和数据层(data source)的分离。

面向对象的对象系统与面向关系的关系型数据库是两种异构的结构,因此在对象系统中的很多机制(collections, inheritance等)都无法在关系型数据库中得到直接的表示,这是 Data Mapper 产生的原因。在这种情况下,在两种模式下的数据转换变得相当复杂,因此我们有必要将它分层,否则任何一方(对象系统和数据库)的改变都将波及对方(这就是 Active Record的缺点)。

在隔离两层后,Data Mapper需要考虑的问题就是:如果进行两层之间的 Map。可能的方法有:

  • 定义固定的 Finder:以一个独立的包定义一个独立的 Finder,定义接口,在数据层实现这个接口。逻辑层利用 Finder 调用 数据层 的数据接口。
  • 基于元数据的映射,每个业务逻辑的对象都有一个 mapping 所用的对象或者文件(如json、xml配置),以此定义元数据业务映射关系。

关于 Data Mapper 的更具体的理解和体会,需要我对 SQLAlchemy 更熟悉再进行对比和实践再进行分析。故就此搁笔。没有实践,理解感觉很有偏颇。

结语

由于只有 Python/Ruby 的项目实践经验,因此可能在理解上会有些偏颇。欢迎指正。

需要注意的是,在项目中,除了高效的管理和沟通,更重要的是有个正确的指导思想,而不是具体的实现、分层问题。绝大部分的项目的过度设计、或者杂乱赶需求的乱设计,基本都是因为没有指导思想。没有指导思想,就不可能有一个规范或者风格氛围。

【完】

ottocho 2013.10.20


Til next time,
at 18:42

scribble

Home About GitHub