首页
ORM 的两种模式:Active Record 与 Data Mapper 比较

ORM

以前在用 java 的 Hibernate 时,只知道它是一个 ORM(Object/Relation Mapping对象关系映射) 框架。至于它的原理以及为什么会有这种技术出现都是不太了解的。随着玩的语言以及框架越来越多后,发现几乎大部分的 web 框架都会有 ORM 的实现。维基百科有篇文章列出了大部分的 ORM 框架:List of object-relational mapping software

我们知道,在面向对象编程中,我们会把一个实体看成一个对象,比如 java 的 PO(Persistant Object)。但是在关系型数据库中,它是表中的一条记录。它们的模型表示是完全不一样的。ORM 就是通过把对象跟数据库表映射起来,以达到通过编程语言来操作关系型数据库的目的。

数据库的表(table) --> 类(class)
记录(record,行数据)--> 对象(object)
字段(field)--> 对象的属性(attribute)

它带来了一些好处。

  • 比如在数据访问层,它可以屏蔽底层使用不同数据库的差异,使得我们切换数据库的成本变小。
  • ORM 框架内部会实现 sql 防注入,开发人员只需要调用对应的 api 即可,不用担心以前写原生 sql 的各种问题。
  • 可以获得一些高级支持,比如:事务、连接池、迁移等等。

但是它也会带来一些缺点。比如调试 sql 的时候,没有原生 sql 那么容易;复杂的 sql 很难通过 ORM 的语法写出来等等。

ORM 的用法不同的框架实现都不一样,使用细节这里就不赘述了。接下来,主要讲一下 ORM 具体实现层面的两种不同方案,也就是 DataMapperActiveRecord。在 php 框架中,它们分别对应着DoctrineEloquent

Active Record

在 Martin Fowler 的P of EAA 中,它提到了Active Record模式。

一个对象既包含数据又包含行为。这些数据大部分是持久性的,需要存储在数据库中。Active Record使用最明显的方法,将数据访问逻辑放在域对象中。这样,所有人都知道如何在数据库中读取和写入数据。

image_1f0bko5uc51615m37qk1nfvr4t9.png-12kB

所以,Active Record模式的持久化对象是既包括数据又包括行为的。以Eloquent为例,我们看一下它是如何实现的:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
     /**
     * 与模型关联的表名
     *
     * @var string
     */
    protected $table = 'my_flights';
     /**
     * 与表关联的主键
     *
     * @var string
     */
    protected $primaryKey = 'flight_id';
}

//检索
$flights = App\Models\Flight::all();
foreach ($flights as $flight) {
    echo $flight->name;
}

//更新
$flight = App\Models\Flight::find(1);
$flight->name = 'New Flight Name';
$flight->save();

这种方式比较有争议的一个地方是,它违背了单一职责原则。关于这一点,社区里也有很多讨论,比如:Does the ActiveRecord pattern follow/encourage the SOLID design principles? 但最重要的还是它不利于写单元测试。

DataMapper

另一种模式则是DataMapper模式了。它与Active Record不一样的地方在于它增加了一个映射器类,把持久化对象的数据跟行为分开了。它的关键地方在于数据模型遵循了单一职责原则。我们可以看一下在Doctrine中,它是怎么运用的。

数据模型

<?php
// src/Product.php

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="products")
 */
class Product
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    protected $id;
  
    /**
     * @ORM\Column(type="string")
     */
    protected $name;

    public function getId()
    {
        return $this->id;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setName($name)
    {
        $this->name = $name;
    }
}

持久化操作

<?php
// create_product.php <name>
require_once "bootstrap.php";

$newProductName = $argv[1];

$product = new Product();
$product->setName($newProductName);

$entityManager->persist($product);
$entityManager->flush();

echo "Created Product with ID " . $product->getId() . "\n";

获取数据

<?php
// list_products.php
require_once "bootstrap.php";

$productRepository = $entityManager->getRepository('Product');
$products = $productRepository->findAll();

foreach ($products as $product) {
    echo sprintf("-%s\n", $product->getName());
}

我们还可以在它的基础上使用Repository模式,让领域层跟数据映射层之间清晰分离。

其他

公司使用的CodeIgniter框架里也有Active Record Class,但我一直觉得它这个Active Record并不是真正意义上的。它的文档里称 CodeIgniter 使用 Active Record 数据库模式的修改版本,但它并没有做数据跟对象的映射,只是提供了一组 api 可以使用它的语法来做数据库的操作而已。

关于这个疑问,我也找到了几篇文章对于它的质疑。

另外,在学习 Elixir 的 ecto库时,感觉它也比较类似 ORM 的实现。找到了一篇文章关于它跟 ORM 的对比:ActiveRecord 和 Ecto 的比较

以上这些,不管是 ORM 的 Active Record 模式还是 DataMapper 模式,又或者 ecto 这种,本质上都是对数据访问层的实现提供了抽象,能让我们的业务跟数据存储隔离开来。至于选择哪种,没有所谓的对错,根据自己的使用场景进行取舍才是最重要的。

参考:

  1. https://www.ruanyifeng.com/blog/2019/02/orm-tutorial.html
  2. https://www.infinitypp.com/software-patterns/activerecord-vs-datamapper-pattern-php-laravel/