什么是ORM?
ORM(Object Relational Mapping, ORM, O/RM, O/R mapping)对象关系映射用来实现面向对象编程语言中不同类型系统的数据之间的转换。
ORM是一种为了解决面向对象与关系数据库存在互补匹配现象的技术。
简单来说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数据库中。从本质上来讲,就是将数据从一种形式转换到另一种形式。
ORM中数据库操作分为两种:
- 基础的活动记录
ActiveRecord
用于快速开发和常规查询 - 高级的数据映射
DataMapping
用于事务和复杂业务查询
ActiveRecord
- 每个数据表对应创建一个类,类的每个对象实例对应数据表中的一行记录,通常表的每个字段在类中都有对应的字段。
- ActiveRecord同时负责将自己持久化,在ActiveRecord中封装了对数据库的访问即CURD。
- ActiveRecord是一种领域模型(Domain Model, DM)封装了部分业务逻辑
DataMapping
DataMapping数据映射是指给定两个数据模型,在模型之间建立起数据元素的对应关系,将此过程称为数据映射。
在简单应用中,领域模型(Domain Model)是一种和数据库结构一致的简单模型,对应的每个数据表都有一个领域类。在这种情况下,有必要让每个对象负责数据库的存取过程,这也就是活动记录(ActiveRecord)。领域对象直接与数据表进行交互,这带来了一个问题,随着领域逻辑变得越来越负责,它就慢慢转变成了一个更大的领域模型,此时简单活动记录就会逐渐无法满足需求。当领域类和数据表一对一匹配也开始随着将领域逻辑放入更小的类而失效,由于关系数据库无法处理继承,因此使用策略模式等面向对象模式非常困难。一种更好的办法是把数据库与数据库完全独立,让间接层完全领域对象和数据表之间的映射关系,这个映射类也称为数据映射器(DataMapper)。这个映射类处理数据库和领域模型之间所有的存取操作,并且允许双方都能独立变化。如果领域逻辑非常简单且数据表十分一致,使用简单的ActiveRecord活动记录就足够了,但如果领域逻辑比较复杂且是长线项目,DataMapper数据映射器则可能是需要的。
查询器
ORM有多种设计模式,Swoft采用的是DataMapping
,将业务于实体分开,但也实现了类似ActiveRecord
的操作方式,其实都是同一个实现的。
Swoft查询器,提供可使用面向对象的方法操作数据库。
Swoft提供了一套基础查询,类ActiveRecord方法,方便快捷的实现数据库操作,但实体必须先继承Model
类,且不能使用事务。如果需要使用事务,需使用高级查询。
Swoft基础查询提供延迟收包和非延迟收包两种方式,非延迟收包一般用于并发使用。
配置数据库
创建数据表
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '名称',
`age` tinyint(1) unsigned DEFAULT '0' COMMENT '年龄',
`sex` tinyint(1) unsigned DEFAULT '0' COMMENT '性别',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
配置数据库连接
$ vim .env
# 数据库主节点配置
# 连接池节点名称用于服务发现
DB_NAME=dbMaster
# 连接地址信息
DB_URI=192.168.99.100:3306/test?user=jc&password=junchow&charset=utf8,192.168.99.100:3306/test?user=jc&password=junchow&charset=utf8
# 最小活跃连接数
DB_MIN_ACTIVE=5
# 最大活跃连接数
DB_MAX_ACTIVE=10
# 最大等待连接
DB_MAX_WAIT=20
# 最大等待时间
DB_MAX_WAIT_TIME=3
# 连接最大空闲时间
DB_MAX_IDLE_TIME=60
# 连接超时时间
DB_TIMEOUT=2
如果对数据库主从不是很清晰,可参见《MySQL读写分离主从复制》。
创建实体模型
- 实体模型继承自
Model
类,位于swoft/db/src/Model.php
。 - 实体模型主要针对单表简单CURD操作,不支持事务,但支持并发查询。
创建实体命令帮助
$ php bin/swoft entity:create -h
Description:
Auto create entity by table structure
Usage:
entity:create -d[|--database] <database>
entity:create -d[|--database] <database> [table]
entity:create -d[|--database] <database> -i[|--include] <table>
entity:create -d[|--database] <database> -i[|--include] <table1,table2>
entity:create -d[|--database] <database> -i[|--include] <table1,table2> -e[|--exclude] <table3>
entity:create -d[|--database] <database> -i[|--include] <table1,table2> -e[|--exclude] <table3,table4>
Options:
-d 数据库
--database 数据库
-i 指定特定的数据表,多表之间用逗号分隔
--include 指定特定的数据表,多表之间用逗号分隔
-e 排除指定的数据表,多表之间用逗号分隔
--exclude 排除指定的数据表,多表之间用逗号分隔
--remove-table-prefix 去除表前缀
--entity-file-path 实体路径(必须在以@app开头并且在app目录下存在的目录,否则将会重定向到@app/Models/Entity)
Example:
php bin/swoft entity:create -d test
使用命令行创建实体
php bin/swoft entity:create -d test user
注意:如果命令中缺少-d
参数则会报错databases doesn't not empty!
。
创建成功后会生成模型的实体文件
$ vim app/Models/Entity/User.php
<?php
namespace App\Models\Entity;
use Swoft\Db\Bean\Annotation\Id;
use Swoft\Db\Bean\Annotation\Required;
use Swoft\Db\Bean\Annotation\Table;
use Swoft\Db\Bean\Annotation\Column;
use Swoft\Db\Bean\Annotation\Entity;
use Swoft\Db\Model;
use Swoft\Db\Types;
/**
* 用户实体
*
* @Entity()
* @Table(name="user")
* @uses User
* @version 2017年08月23日
* @author stelin <phpcrazy@126.com>
* @copyright Copyright 2010-2016 Swoft software
* @license PHP Version 7.x {@link http://www.php.net/license/3_0.txt}
*/
class User extends Model
{
/**
* 主键ID
*
* @Id()
* @Column(name="id", type=Types::INT)
* @var null|int
*/
private $id;
/**
* 名称
*
* @Column(name="name", type=Types::STRING, length=20)
* @Required()
* @var null|string
*/
private $name;
/**
* 年龄
*
* @Column(name="age", type=Types::INT)
* @var int
*/
private $age = 0;
/**
* 性别
*
* @Column(name="sex", type="int")
* @var int
*/
private $sex = 0;
/**
* 描述
*
* @Column(name="description", type="string")
* @var string
*/
private $desc = '';
/**
* 非数据库字段,未定义映射关系
*
* @var mixed
*/
private $otherProperty;
/**
* @return int|null
*/
public function getId()
{
return $this->id;
}
/**
* @param int|null $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* @return null|string
*/
public function getName()
{
return $this->name;
}
/**
* @param null|string $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* @return int
*/
public function getAge(): int
{
return $this->age;
}
/**
* @param int $age
*/
public function setAge(int $age)
{
$this->age = $age;
}
/**
* @return int
*/
public function getSex(): int
{
return $this->sex;
}
/**
* @param int $sex
*/
public function setSex(int $sex)
{
$this->sex = $sex;
}
/**
* @return string
*/
public function getDesc(): string
{
return $this->desc;
}
/**
* @param string $desc
*/
public function setDesc(string $desc)
{
$this->desc = $desc;
}
/**
* @return mixed
*/
public function getOtherProperty()
{
return $this->otherProperty;
}
/**
* @param mixed $otherProperty
*/
public function setOtherProperty($otherProperty)
{
$this->otherProperty = $otherProperty;
}
}
在此需要注意的是虽然数据表中使用是description
, 但使用@Column(name="description", type="string")
。
/**
* 描述
*
* @Column(name="description", type="string")
* @var string
*/
private $desc = '';
AR操作
关于ActiveRecord,可以理解为一个不同SQL数据库的Wrapper,同时为上层提供一种简洁优雅的API或DSL,能极大地减轻开发者的负担并提升工作效率。
由于每个AR类代表一个数据表或视图,数据表或视图的列在AR类中体现为类的属性,一个AR实例则表示为数据表中的一行。常见的CURD增删改查操作作为AR的方法实现。因此,可以以一种更加面向对象的方式访问数据。
新增
-
setter
使用对象方式插入数据
- 直接返回结果
//使用ORM的ActiveRecord
$model = new User();
$model->setName("junchow");
$model->setSex(1);
$model->setAge(mt_rand(1, 100));
$model->setDesc("this is a test");
$result = $model->save()->getResult();
var_dump($result);//int(3)
容器中开启日志并查看输出
docker@default ~$ docker logs myswoft -ft
2019-04-25T09:00:54.596389052Z int(3)
对比观察,发现返回的结果$result
与数据库的自增主键id
保持一致。
- 延迟操作
//使用ORM的ActiveRecord
$model = new User();
$model->setName("junchow");
$model->setSex(1);
$model->setAge(mt_rand(1, 100));
$model->setDesc("this is a test");
$result = $model->save(true)->getResult();
var_dump($result);
- 使用数组方式保存数据
//使用ORM的ActiveRecord
$model = new User();
//使用数组方式保存数据
$model["name"] = "bob";
$model["sex"] = 1;
$model["age"] = 20;
$model["desc"] = "this is a description demo";
$query = $model->save();
$result = $query->getResult();//直接输出
var_dump($result);
注意:这里使用的是实体属性desc
而非数据表中的description
。
-
fill
使用数组方式填充数据
//使用ORM的ActiveRecord
$model = new User();
//使用数组方式填充数据
$data = [];
$data["name"] = "alice";
$data["sex"] = 1;
$data["age"] = 20;
$data["desc"] = "this is a description demo";
$query = $model->fill($data)->save();
$result = $query->getResult();//直接输出
var_dump($result);
注意:这里使用的是实体属性desc
而非数据表中的description
。
-
batchInsert
使用批量插入方式添加数据
//使用ORM的ActiveRecord
$rows = [];
//使用数组方式保存数据
$row = [];
$row["name"] = "bob";
$row["sex"] = 1;
$row["age"] = 20;
$row["description"] = "this is a description demo";
$rows[] = $row;
//使用数组方式保存数据
$row = [];
$row["name"] = "carl";
$row["sex"] = 1;
$row["age"] = 21;
$row["description"] = "this is a description demo";
$rows[] = $row;
$query = User::batchInsert($rows);
$result = $query->getResult();//批量插入
var_dump($result);
注意:
- 这里使用的是数据表中的
description
,而非实体属性desc
。 - 批量插入后返回的是第一条记录的自增主键ID
删除
-
deleteById
使用主键删除单条记录
$id = 2;
$query = User::deleteById($id);
$result = $query->getResult();
var_dump($result);
返回值:删除记录的条数
-
deleteByIds
使用主键删除多条记录
$ids = [];
$ids[] = 3;
$ids[] = 4;
$ids[] = 5;
$query = User::deleteByIds($ids);
$result = $query->getResult();
var_dump($result);
返回值$result
是删除记录的条数
-
deleteOne
删除单条数据
$where = [];
$where["name"] = "bob";
$where["age"] = 20;
$query = User::deleteOne($where);
$result = $query->getResult();
var_dump($result);
删除条件:删除name
为bob
而且age
等于20的记录,注意这里条件是AND
的关系。
返回值:删除记录的条数
-
deleteAll
删除多条数据
$where = [];
$where["id"] = [6,7];
$where["age"] = 21;
$query = User::deleteAll($where);
$result = $query->getResult();
var_dump($result);
删除条件:删除用户编号为6或7,而且年龄为21的记录。
返回值:删除记录条数
修改
注意:由于在数据库中获取数据属于读操作,修改、新增属于写操作,因此需要注意配置文件中的主从和读写。
-
update
使用实体的方式更新记录
第一种方式是直接使用setter
设置器的方式进行修改
$id = 1;
//根据用户ID判断用户是否存在
$query = User::findById($id);
$user = $query->getResult();
if(!empty($user)){
var_dump($user);
//重新设置姓名
$user->setName("lisa");
//更新用户数据
$result = $user->update()->getResult();
var_dump($result);
}
第二种是使用数组的方式进行修改
$query = User::findById($id);
$user = $query->getResult();
$user["name"] = "lisa";
$result = $user->update()->getResult();
更新结果若返回字符串的0表示更新失败,否则更新成功。
-
updateOne
更新单条数据
$update = [];
$update["name"] = "superman";
$where = [];
$where["id"] = 11;
$query = User::updateOne($update, $where);
$result = $query->getResult();
var_dump($result);
返回值:成功返回受影响行数,上例$result
为1。
-
updateAll
更新多条数据
$update = [];
$update["name"] = "lucy";
$where = [];
$where["id"] = [10,20,30];
$query = User::updateAll($update, $where);
$result =$query->getResult();
var_dump($result);
返回值:成功返回受影响行数,上例$result
为3。
查询
- 使用AR实体查询,返回结果为实体对象,而非数组。
- 获取单条数据
findOne
$fields = ["name", "sex"];
$query = User::findOne($where, $fields);
$result = $query->getResult();
var_dump($result);
返回值:
object(App\Models\Entity\User)#1413 (7) {
["id":"App\Models\Entity\User":private]=>
int(10)
["name":"App\Models\Entity\User":private]=>
string(4) "lucy"
["age":"App\Models\Entity\User":private]=>
int(21)
["sex":"App\Models\Entity\User":private]=>
int(1)
["desc":"App\Models\Entity\User":private]=>
string(26) "this is a description demo"
["otherProperty":"App\Models\Entity\User":private]=>
NULL
["attrs":"Swoft\Db\Model":private]=>
array(5) {
["id"]=>
int(10)
["name"]=>
string(4) "lucy"
["age"]=>
int(21)
["sex"]=>
int(1)
["desc"]=>
string(26) "this is a description demo"
}
}
观察可以发现返回的是一个object
实体对象,如果要从实体对象中获取属性,则需使用getter
方法。
var_dump($result->getName());//string(4) "lucy"
var_dump($result->getSex());//int(1)
var_dump($result->getDesc());//string(26) "this is a description demo"
若需要将object
实体对象转化为数组,可使用toArray()
方法。
$arr = $result->toArray();
- 查询多条数据
findAll
用法
/**
* 查看多条数据
* $condition 数组 查询条件
* $options 数组 额外条件如orderby、limit、offset
*/
findAll(array $condition = [], array $options = [])
用法1
$condition = [];
$condition["id"] = [20,21,22,23,24,25];
$condition["sex"] = 1;
$options = [];
$options["orderby"] = ["age"=>"DESC"];
$optoins["limit"] = 10;
$query = User::findAll($condition, $options);
$result = $query->getResult();
var_dump($result);
返回值
object(Swoft\Db\Collection)#1421 (1) {
["items":protected]=>
array(3) {
[0]=>
object(App\Models\Entity\User)#1410 (7) {
["id":"App\Models\Entity\User":private]=>
int(21)
["name":"App\Models\Entity\User":private]=>
string(10) "1556193516"
["age":"App\Models\Entity\User":private]=>
int(57)
["sex":"App\Models\Entity\User":private]=>
int(1)
["desc":"App\Models\Entity\User":private]=>
string(14) "this is a test"
["otherProperty":"App\Models\Entity\User":private]=>
NULL
["attrs":"Swoft\Db\Model":private]=>
array(5) {
["id"]=>
int(21)
["name"]=>
string(10) "1556193516"
["age"]=>
int(57)
["sex"]=>
int(1)
["desc"]=>
string(14) "this is a test"
}
}
[1]=>
object(App\Models\Entity\User)#1419 (7) {
["id":"App\Models\Entity\User":private]=>
int(22)
["name":"App\Models\Entity\User":private]=>
string(10) "1556193538"
["age":"App\Models\Entity\User":private]=>
int(49)
["sex":"App\Models\Entity\User":private]=>
int(1)
["desc":"App\Models\Entity\User":private]=>
string(14) "this is a test"
["otherProperty":"App\Models\Entity\User":private]=>
NULL
["attrs":"Swoft\Db\Model":private]=>
array(5) {
["id"]=>
int(22)
["name"]=>
string(10) "1556193538"
["age"]=>
int(49)
["sex"]=>
int(1)
["desc"]=>
string(14) "this is a test"
}
}
[2]=>
object(App\Models\Entity\User)#1420 (7) {
["id":"App\Models\Entity\User":private]=>
int(23)
["name":"App\Models\Entity\User":private]=>
string(10) "1556193540"
["age":"App\Models\Entity\User":private]=>
int(19)
["sex":"App\Models\Entity\User":private]=>
int(1)
["desc":"App\Models\Entity\User":private]=>
string(14) "this is a test"
["otherProperty":"App\Models\Entity\User":private]=>
NULL
["attrs":"Swoft\Db\Model":private]=>
array(5) {
["id"]=>
int(23)
["name"]=>
string(10) "1556193540"
["age"]=>
int(19)
["sex"]=>
int(1)
["desc"]=>
string(14) "this is a test"
}
}
}
}
观察可以发现,返回值是一个Swoft\Db\Collection
类型的object
。
用法2:排序条件与分页设置
$condition = [];
$condition["sex"] = 1;
$condition[] = ["id", ">", 30];
分页设置一般是由offset
偏移量和分页条数limit
共同构成,例如:SQL语句limit 0,10
表示从第0条开始向后取10条。
$options = [];
$options["orderby"] = ["age"=>"DESC"];
$optoins["offset"] = 1;
$optoins["limit"] = 10;
- 排序条件
$options["orderby"] = ["age"=>"DESC"];
- 偏移量(从第n条开始)
$optoins["offset"] = 1;
- 条数限制
$optoins["limit"] = 10;
因此,如果在分页中需要根据页码和每页条数来获取数据。
//分页
$page = 1;//页码
$pagesize = 5;//每页显示条数
$offset = ($page - 1) * $pagesize;
$options["offset"] = $offset;
$options["limit"] = $pagesize;
- 根据主键查询单条数据
findById
$id = 22;
$query = User::findById($id);
$result = $query->getResult();
var_dump($result);
- 根据主键查询多条数据
findByIds
- 多个主键以数组方式
[]
使用 - 返回值是
Swoft\Db\Collection
类型的object
$ids = [21,22,23];
$options = [];
$options["fields"] = ["name", "sex"];
$options["orderby"] = ["id"=>"DESC"];
$options["limit"] = 2;
$query = User::findByIds($ids, $options);
$result = $query->getResult();
var_dump($result);
- 判断主键是否存在
exists
- 若主键存在则返回
true
否则返回false
$id = 10;
$query = User::exist($id);
$result = $query->getResult();
var_dump($result);//bool(true)
- 查询计数
count
- 返回满足条件的行数
$fields = "id";
$options = [];
$options["id"] = [10,11];
$query = User::count($fields, $options);
$result = $query->getResult();
var_dump($result);//string(1) "2"
小结
在了解ORM对象关系映射之后,常见使用ORM做数据查询的方式有两种AR和DataMapping,AR主要针对单表简单的查询,DataMapping主要针对复杂的多条查询。这里需要注意的一点的是由于数据库的操作主要分为读操作和写操作,如果提前在配置文件中使用读写分离,将读数据库和写数据库分开后,使用AR是查询操作实际上隶属于读操作,而插入、更新、删除隶属于写操作,这一点将在后续针对性的分析。接下来,需要挖掘的针对多表连表时的操作,DataMapping是如何使用的呢?