PHP 设计模式 - 结构型 - 数据映射模式(Data Mapper)

1. 模式定义

数据映射:是持久化数据存储层(通常是关系型数据库)和驻于内存得数据表现层直接进行双向数据传输得数据访问层
数据映射模式的目的:让持久化数据存储层、驻于内存的数据表现层、以及数据映射本身三者相互独立、互不依赖。
这个数据访问层由一个或多个映射器(或者数据访问对象)组成,用于实现数据传输。通用的数据访问层可以处理不同的实体类型,而专用的则处理一个或几个。

2. 数据映射模式 ( Data Mapper ) VS 活动记录模式(Active Record)

首先我们将 ORM 模型拆分开来就是两个功能

  1. 数据操作 - 对数据对象做变更,就是我们常说的业务逻辑。
  2. 数据持久化 - 将数据落地,比如存储到 MySQL,MongoDB 等不同的数据库。

数据映射模式 ( Data Mapper ):主张两个功能必须分开,扩展灵活,逼格高,数据模型遵循单一职责原则(Single Responsibility Principle)。使用Data Mappers的框架数量相比ActiveRecord要少很多,主要有Java Hibernate,PHP Doctrine,SQLAlchemy in Python,EntityFramework for Microsoft .NET。

<?php
$model = new User();
$model->setId(1);
$model->setAccount('it2048');
$model->setPassword('123456'); 

$result = (new UserMapper())->save($user);

$model 对象属性的修改属于业务逻辑UserMapper涵括持久化逻辑

活动记录模式 (Active Record) : 主张把两个功能合在一起,简单方便,易上手。用ActiveRecord ORM的PHP框架有Laravel, Yii, CodeIgniter, CakePHP等。其他语言用的有 Ruby on Rails,Django等。

<?php
$model = new User();
$model->user_id = 1;
$model->name = 'Sylvia';
$model->save();

$model 属性的修改属于业务逻辑,调用save()方法属于持久化逻辑。使用者完全不用关心save()方法执行后数据是存储到MySQL还是MongoDB,在开发过程中可以将精力全部放到业务逻辑,开发速度非常快。

3. UML类图

image.png

4. 示例代码

业务逻辑 User

<?php

namespace DesignPattern\Structural\DataMapper;

/**
 * 数据库记录在内存的表现层
 */
class User
{
    /**
     * @var int
     */
    protected $userId;

    /**
     * @var string
     */
    protected $name;


    /**
     * @param null $id
     * @param null $name
     */
    public function __construct($id = null, $name = null)
    {
        $this->userId = $id;
        $this->name = $name;
    }

    /**
     * @return int
     */
    public function getUserId()
    {
        return $this->userId;
    }

    /**
     * @param int $userId
     */
    public function setUserID($userId)
    {
        $this->userId = $userId;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @param string $name
     */
    public function setName($name)
    {
        $this->name = $name;
    }
}

持久化逻辑 UserMapper

<?php

namespace DesignPattern\Structural\DataMapper;

use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\DriverManager;

/**
 * 数据映射类
 * 数据库的连接与修改,这里用的是扩展包DBAL适配器,用于连接各种数据库并进行操作  composer require doctrine/dbal:3.0.0 进行下载安装
 */
class UserMapper
{
    protected $table = 'users';
    protected $adapter;

    /**
     * UserMapper constructor.
     * @throws DBALException
     */
    public function __construct()
    {
        $connectionParams = array(
            'dbname' => 'design_pattern',
            'user' => 'root',
            'password' => '123456',
            'host' => '127.0.0.1:3316',
            'driver' => 'pdo_mysql',
        );

        $adapter = DriverManager::getConnection($connectionParams);
        $this->adapter = $adapter;
    }


    /**
     * 将用户对象保存到数据库
     *
     * @param User $user
     * @return bool
     * @throws DBALException
     */
    public function save(User $user)
    {
        // $data的键名对应数据库表字段
        $data = array(
            'user_id' => $user->getUserId(),
            'name' => $user->getName(),
        );
        // 如果没有指定ID则在数据库中创建新纪录,否则更新已有记录
        if (null === ($id = $user->getUserId())) {
            unset($data['user_id']);
            $this->adapter->insert($this->table, $data);
            return true;
        } else {
            $this->adapter->update($this->table, $data, array('user_id ' => $id));
            return true;
        }
    }

    /**
     * 基于ID在数据库中查找用户并返回用户实例
     * @param $id
     * @return mixed
     * @throws DBALException
     * @throws \InvalidArgumentException
     */
    public function findById($id)
    {
        $result = $this->adapter->executeQuery("select * from ".$this->table." where user_id = ".$id)->fetch();

        if (empty($result)) {
            throw new \InvalidArgumentException("User #$id not found");
        }
        return $this->mapObject($result);
    }

    /**
     * 获取数据库所有记录并返回用户实例数组
     * @return array
     * @throws DBALException
     */
    public function findAll()
    {
        $resultSet = $this->adapter->executeQuery("select * from ".$this->table)->fetchAll();
        $entries = array();

        foreach ($resultSet as $row) {
            $entries[] = $this->mapObject($row);
        }

        return $entries;
    }

    /**
     * 映射表记录到对象
     *
     * @param array $row
     *
     * @return User
     */
    protected function mapObject(array $row)
    {
        $entry = new User();
        $entry->setUserID((int)$row['user_id']);
        $entry->setName($row['name']);

        return $entry;
    }

}

单元测试:

<?php

namespace DesignPattern\Tests;

use DesignPattern\Structural\DataMapper\User;
use DesignPattern\Structural\DataMapper\UserMapper;
use PHPUnit\Framework\InvalidArgumentException;
use PHPUnit\Framework\TestCase;

/**
 * 测试数据映射模式
 * Class DataMapperTest
 * @package Creational\Singleton\Tests
 */
class DataMapperTest extends TestCase
{

    public function getNewUser()
    {
        return array(array(new User(null, 'Sylvia')));
    }


    public function getExistingUser()
    {
        return array(array(new User(1, 'Sylvia1')));
    }

    /**
     * @param User $user
     *
     * @dataProvider getNewUser
     *
     * @throws \Doctrine\DBAL\DBALException
     */
    public function testCreate(User $user)
    {
        $result = (new UserMapper())->save($user);
        $this->assertIsBool($result);
    }

    /**
     * @param User $user
     * @dataProvider getExistingUser
     * @throws \Doctrine\DBAL\DBALException
     */
    public function testUpdate(User $user)
    {
        $updateResult = (new UserMapper())->save($user);
        $this->assertIsBool($updateResult);
    }

    /**
     * @param User $existing
     * @dataProvider getExistingUser
     * @throws \Doctrine\DBAL\DBALException
     */
    public function testFindById(User $existing)
    {
        $user = (new UserMapper())->findById(1);
        $this->assertEquals($existing, $user);
    }


    /**
     * @dataProvider getExistingUser
     * @throws \Doctrine\DBAL\DBALException
     */
    public function testFindAll()
    {

        $users = (new UserMapper())->findAll();
        $this->assertIsArray($users);
        foreach ($users as $user) {
            $this->assertIsObject($user);
        }
    }

}

参考文档:https://laravelacademy.org/post/2739.html
教程源码:https://github.com/SylviaYuan1995/DesignPatternDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容