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 具体实现层面的两种不同方案,也就是 DataMapper
和ActiveRecord
。在 php 框架中,它们分别对应着Doctrine和Eloquent。
Active Record
在 Martin Fowler 的P of EAA 中,它提到了Active Record模式。
一个对象既包含数据又包含行为。这些数据大部分是持久性的,需要存储在数据库中。Active Record使用最明显的方法,将数据访问逻辑放在域对象中。这样,所有人都知道如何在数据库中读取和写入数据。
所以,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 这种,本质上都是对数据访问层的实现提供了抽象,能让我们的业务跟数据存储隔离开来。至于选择哪种,没有所谓的对错,根据自己的使用场景进行取舍才是最重要的。
参考: