Laravel Eloquent ORM 学习

Eloquent ORM 是 Laravel 自带的 ORM , 提供了一个优雅的、简单的 数据库 ActiveRecord 实现。每一个数据库的表有一个对应的 “Model” 用来与这张表交互。葡萄的Kerisy框架中的ORM直接采用了Eloquent.

注:以下文档基于 Laravel 5.1 版本。

参考文档:
(1) GoLaravel中文社区Eloquent ORM文档
(2) JohnLui: 深入理解 Laravel Eloquent(一)——基本概念及用法
(3) JohnLui: 深入理解 Laravel Eloquent(二)——中间操作流(Builder)
(4) JohnLui: 深入理解 Laravel Eloquent(三)——模型间关系(关联)

一、数据库配置

配置文件:

app/config/database.php

配置文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'default' => env('DB_CONNECTION', 'mysql'),
'connections' => [
/**
* 默认数据库连接,业务部分
* 如需链接多个数据库,该部分可以配置多份,即可在创建 Model 的时候选择链接的是哪个数据库
*/
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4', // 根据需要配置数据库和数据库链接的编码格式,默认是utf8
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => false,
],
]

其中数据库的各项链接信息配置可以直接在环境配置文件 .env 中进行配置同时环境隔离。

1
2
3
4
5
6
#.env
DB_CONNECTION=mysql
DB_HOST=localhost
DB_USERNAME=username
DB_PASSWORD=password
DB_DATABASE=designup_db

读取配置文件的位置:

vendor/laravel/framework/src/Illuminate/Database/DatabaseManager::connection($name)

设计模式:单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
public function connection($name = null)
{
list($name, $type) = $this->parseConnectionName($name);
// If we haven't created this connection, we'll create it based on the config
// provided in the application. Once we've created the connections we will
// set the "fetch mode" for PDO which determines the query return types.
if (! isset($this->connections[$name])) {
$connection = $this->makeConnection($name);
$this->setPdoForType($connection, $type);
$this->connections[$name] = $this->prepare($connection);
}
return $this->connections[$name];
}

具体选择所用connection的位置:

vendor/laravel/framework/src/Illuminate/Queue/Connectors/DatabaseConnector::connect($config)

1
2
3
4
5
6
7
8
9
public function connect(array $config)
{
return new DatabaseQueue(
$this->connections->connection(Arr::get($config, 'connection')),
$config['table'],
$config['queue'],
Arr::get($config, 'expire', 60)
);
}

创建 Model 时如下:

1
2
3
4
5
6
7
8
9
10
namespace App\Library\Model;

use Illuminate\Database\Eloquent\Model;

class Base extends Model
{
protected $connection = 'mysql'; // 使用的数据库链接
protected $dateFormat = 'U'; // U:时间字段格式为int,默认不设置该字段则为 datetime 格式的时间戳即mysql中的 timestamp 格式
public $timestamps = true; // 自动时间戳
}

二、Eloquent ORM的基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class User extends Eloquent {
protected $table = 'my_users'; //可以不设置,此时默认标名为类名的负数,如users
protected $primaryKey = 'id';
//可以考虑中间加一层类,
//同一个数据库的所有model继承使用共同connection的基类,
//此基类自定义__construct方法创建链接
protected $connection = 'default';

//多表查询 多对一关系
public function hasOneGroup() {
return $this->hasOne('App\Core\Model\Group','id','group_id');
}
//多表查询 1对多关系
public function hasManyArticles() {
return $this->hasMany('App\Core\Model\Article','uid', 'id');
}
//多表查询 多对多
public function hasManyFriends() {
return $this->belongsToMany(App\Core\Model\User::class, '本表外键KEY', '对应表主键');
}
}

注:以下调用方法,可以用静态调用,也可以将 User 类实例化之后用 $model 对象调用。

1.增(add)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 方法一
# 注:使用此方法,需要将 add 时所涉及到的key,全都配置到 Model 的 $fillable = [] 属性中。
$data = [
'username' => 'name',
'sex' => 'boy'
];
$userId = User::add($data);

# 方法二
$obj = new User;
$obj->username = 'name';
$obj->sex = 'boy';
$obj->save();
$userId = $obj->id
2.删(delete)
1
2
3
4
5
6
7
8
# 删除方法一:find/where & delete
User::find(1)->delete();
User::where('id', 1)->delete();

# 删除方法二:destroy【主键】
User::destroy(1);
User::destroy(1,2,3);
User::destroy([1,2,3]);

关于删除,上面两种方法都是直接从数据库中删除掉该记录,但是一般生产环境,我们大多数删除操作都是以某个字段标记数据状态为已删除、未删除等。EloquentOrm提供了一系列处理这种删除的方法,我们称之为“软删除”。它所使用的,标记“软删除”状态的字段,是 deleted_at ,即删除时间。此字段默认为空(NULL),若 deleted_at not null 则证明该记录已被软删除。

Model声明:

1
2
3
4
5
6
7
8
9
10
class User extends Model 
{
use SoftDeletes;
/**
* 需要被转换成日期的属性。
*
* @var array
*/
protected $dates = ['deleted_at'];
}

此处关于 deleted_at 字段:laravel默认将之处理为 datetime 类型, 若在model中设置属性:protected $dateFormat = 'U'; , 则会将至处理为时间戳。

关于软删除的一系列操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$user = find(1);
# 删除
$user->softDeletes();

# 判断是否软删除
if ( $user->trashed() ) {
//
}

# 查询
## 默认where查询,会过滤掉已软删除的数据,若想查出,则需如下
User::withTrashed()// 强制查找已被软删除的模型,包括软删除和未软删除的所有数据
->where('id', '>', 0)
->get();
## 仅查询被软删除的
User::onlyTrashed()// 强制查找范围为已软删除的数据 ->where('id', '>', 0)
->get();

# 回复软删除的数据
$user->restore();

# 彻底删除已被软删除的数据
$user->forceDelete();
3.查(select)

Model定义时可以设置主键 $primaryKey ,默认为 id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 查询方法一:根据主键查询一条
$userInfo = User::find($id);

# 查询类型二:条件查询 - 列表
$list = User::where('username', 'name')
->where('created_at','<',time() - 3600) //不等条件
->orderBy('id', 'desc') //排序
->take(3) //limit
->skip(2) //offset
->get(); //取出结果并返回

# 查询类型三:条件查询 - 一条结果
$user = User::where('username', 'name')
->where('created_at','<',time() - 3600) //不等条件
->orderBy('id', 'desc') //排序
->first();

# 查询类型三:返回全部结果
$list = User::all();

# 查询类型四:一些数字的查询
$count = User::where('key','value')->count();
$total = User::count();
$sumAge = User::where('key','value')->sum('age');
$maxAge = User::max('age');
$minAge = User::min('age');

这里注意,查出来的结果除了第四项都是int型之外,其他都是 Object 类型的,如需使用数组,则结果继续调用 ->toArray() 方法即可。但是调用之前必须确保结果非空,否则会报错。

4.改(update)
1
2
3
4
5
6
7
# 修改方式一: Object
$user = User::find(1);
$user->username = 'newname';
$user->save();

# 修改方式二:Object
User::where('id', 1)->update(['nickname' => 'newname']);

三、ORM 提升

1.查询自动过滤字段
1
2
3
4
class User extends Eloquent {
...
...
}
2.查询结果404
1
2
$model = App\Flight::findOrFail(1);
$model = App\Flight::where('legs', '>', 100)->firstOrFail();

如此,在找不到符合条件的数据时,会抛出 HTTP 404 异常给用户

3.关联模型

EloquentORM 还依赖其强大的 查询语句构造器 提供了同样强大的外键查询功能。传送门:关联查询

设计如下四张表

rooms students subjects chooses
字段 意义 - 字段 意义 - 字段 意义 - 字段 意义
room_id 宿舍号 student_id 学号 subject_id 科目ID id 关联ID
room_id 所属宿舍 student_id 选课学生
subject_id 所选课程

rooms 表,记录宿舍信息, 以 room_id 宿舍号为索引
students 表, 记录学生信息, 以 student_id 学号为索引, room_id 记录所属宿舍
subjects 表, 记录课程信息, 以 subject_id 课程ID为索引
choosees 表, 主键ID无意义, 以 student_idsubject_id 关联选课关系

故而:

Rooms         (1) ---------------- (n) Students 
Students     (n) ---------------- (n) Subjects

先做一批测试数据,初始化数据的代码在 routes.php 中,/try/init/

  • 一对一关系:HasOne & BelongsTo
  • 一对多关系:HasMany & BelongsTo

一对一和一对多比较类似,以一对多为例介绍。

Rooms 与 Students 是一对多关系,面上看来,一个 roomHasMany Students,反之,每个 studentbelongsTo 一个 Room

1
2
3
4
5
6
7
8
9
10
# Model Room
public function students()
{
return $this->hasMany('App\Models\Student');
}

# Model Student
public function room(){
return $this->belongsTo('App\Models\Room');
}

hasManybelongsTo 的参数是相似的,第一个是对应的 Model,第二个是外键名称,默认为表名的单数形式(不带前缀)加_id。比如此处省略了第二个参数room_id

调用如下:

1
2
3
4
5
6
7
8
9
10
# routes.php
Route::get('/try/hasmany', function() {
$room = \App\Models\Room::find(3);
var_dump($room->students->toArray());
});

Route::get('/try/belongsto', function() {
$data = \App\Models\Student::find(1);
var_dump($data->room->toArray());
});

belongsTo 方法,还可以配合 with 进行查询,叫做“预加载”:

1
2
3
4
5
6
Route::get('/try/with', function() {
$data = \App\Models\Student::with('room')->whereIn('id',[1,2,3])->get();
foreach ($data as $v) {
var_dump($v->toArray());
}
});

这样打印出来,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

array(11) {
["id"]=>
int(1)
["name"]=>
string(5) "Silov"
["sex"]=>
int(1)
["birthday"]=>
string(10) "1999-10-10"
["grade"]=>
int(2016)
["class"]=>
int(2)
["created_at"]=>
string(10) "1465986707"
["updated_at"]=>
string(10) "1467603414"
["deleted_at"]=>
NULL
["room_id"]=>
int(1)
["room"]=>
array(3) {
["room_id"]=>
int(1)
["created_at"]=>
string(10) "1466496573"
["updated_at"]=>
string(10) "1466496573"
}
}
  • 多对多关系,相对复杂,因为还有一个中间关联表
1
2
3
4
5
# Model Subject
public function students()
{
return $this->belongsToMany('App\Models\Student','subject_choose', 'subject_id', 'student_id');
}

参数:Model、中间表名(不带前缀)、本表关联字段、对面表关联字段

调用方法:

1
2
3
4
5
6
7
# routes.php
Route::get('try/belongstomany', function() {
$data = App\Models\Subject::find(1);
foreach($data->students as $student){
var_dump($student->toArray());
}
});

单次循环打印结果输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
array(11) {
["id"]=>
int(1)
["name"]=>
string(5) "Silov"
["sex"]=>
int(1)
["birthday"]=>
string(10) "1999-10-10"
["grade"]=>
int(2016)
["class"]=>
int(2)
["created_at"]=>
string(10) "1465986707"
["updated_at"]=>
string(10) "1467603414"
["deleted_at"]=>
NULL
["room_id"]=>
int(1)
["pivot"]=>
array(2) {
["subject_id"]=>
int(1)
["student_id"]=>
int(1)
}
}

其中最后一个Key:pivot, 是关联表 subject_choose 的内容。

所以循环中如果想获取中间表的某个数据,比如关联关系建立时间,可以这样:

1
echo $student->pivot->created_at;

除此之外还有一种远层一对多关系,即比如:

一个 room —- 多个students — 跟 student一对一的床位

如果想直接根据宿舍查床位信息,则可以在 Room 使用 hasManyThrough 方法,通过中间一层表来实现的一对多关系。
这里不给出具体的方法和代码,详情自行参考官方文档~

4.多态关联

所谓多态关联,简单点理解就是某个外键所对应的表可能不止一个,由另外一个type一类的字段来判断这个外键对应哪张表。

比如:

如果给学生和课程,都加上图片,所有的图片都存在一张表里。

则图片表如下:

字段 类型 备注
id int(11) 图片ID
path varchar(100) 图片地址
bind_type varchar(30) ModelName,比如:App\Models\Student
bind_id int(11) 外键:student_id/subject_id
1
2
3
4
5
6
7
8
9
CREATE TABLE `fl_images` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '图片ID',
`path` varchar(100) NOT NULL DEFAULT '' COMMENT '图片链接',
`bind_type` varchar(30) NOT NULL DEFAULT '' COMMENT 'model name',
`bind_id` int(11) NOT NULL DEFAULT '0' COMMENT 'student_id or subject_id',
`created_at` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='图片';

Model中绑定模型的方法:

1
2
3
4
5
6
7
8
9
10
# Model Image
public function bind()
{
return $this->morphTo();
}
# Model Student & Model Subject
public function images()
{
return $this->morphMany('App\Models\Image', 'bind');
}

调用数据的方法类似上文的 hasManybelongsTo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# routes.php 
# 查看某个学生的照片
Route::get('try/morphto', function() {
$student = \App\Models\Student::find(13);
foreach($student->images as $image) {
var_dump($image->toArray());
}
});

# 查看某张照片的宿主信息,学生/科目
Route::get('try/bind', function() {
$image = \App\Models\Image::find(1);
var_dump($image->bind->toArray());
});

多态多对多关联,比多态关联和多对多关系更复杂一层。具体的例子参考中文官网文档:多态多对多关系

5.关联查询

关联关系可以作为查询条件来查找符合条件的数据。

比如,上文提到宿舍和学生的一对多关系,可以查找哪些宿舍有学生、哪些宿舍学生有几个等等。

1
2
3
4
# 获取学生数量不为0的宿舍信息
$data = \App\Models\Room::has('students')->get();
# 获取学生数量超过4个的宿舍
$data = \App\Models\Room::has('students', '>', 4)->get();

或者,可以查询某个学生所在宿舍的信息

1
2
3
4
# 获取学生名字叫做 野原新之助 所在的宿舍信息
$data = \App\Models\Room::whereHas('students', function($query){
$query->where('name','野原新之助');
})->first();
6.预加载

效率&查询次数!非常有用!

举个例子,前面提到过一个 with 方法的预加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 预加载模式
Route::get('/try/preload', function() {
$data = \App\Models\Student::with('room')->whereIn('id', [1,5,10,16])->get();
var_dump($data->toArray());
foreach ($data as $v) {
var_dump($v->room->toArray());
}
});

# 非预加载模式
Route::get('/try/unpreload', function() {
$data = \App\Models\Student::whereIn('id', [1,5,10,16])->get();
var_dump($data->toArray());
foreach ($data as $v) {
var_dump($v->room->toArray());
}
});

两种方式结果对比可以发现,foreach 循环部分输出的内容是一样的,但是循环前面的 var_dump 输出的就不一样:预加载方法中,每条数据都多了个 room 字段。这就是差别了,两种方法的查询本质是这样的:

1
2
3
4
5
6
7
8
# 预加载
select * from fl_students;
select * from fl_rooms where id in(1,2,3,4);

# 非预加载模式
select * from fl_students;
//foreach:
select * from fl_room where id=1/2/3/4;

以上可以看出,在这个例子中仅依靠 belongsTohasOne/hasMany 这类方法实现的关联数据查询,时间复杂度O(n),而加上预加载之后就变成了 O(1)。而众所周知PHP最大的瓶颈就在Mysql查询,所以预加载有效地减少了查询次数,提高了查询的效率。

预加载还有多种模式,比如:App\Models\Student 中同时定义了roomimages 两种关联模型,那么就可以同时预加载两组数据:

1
2
3
4
Route::get('/try/preload/students', function() {
$data = \App\Models\Student::with('room','images')->get();
var_dump($data->toArray());
});

反过来,查询宿舍信息时,预加载一个宿舍所有人的头像信息,这叫做嵌套预加载:

1
2
3
4
Route::get('/try/preload/roomimages', function() {
$data = \App\Models\Room::with('students.images')->find(1)->toArray();
var_dump($data);
});

输出格式是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
'room_id' => 1,
....
'students' => [//该宿舍学生信息
....
1 => [
...
'images' => [
//图片信息
]
]
....
]
]

预加载也是可以有条件的加载,比如,查询宿舍的时候,预加载某个人的个人信息:

1
2
3
4
5
6
Route::get('/try/preload/search', function() {
$data = \App\Models\Room::with(['students' => function($query){
$query->where('name', '野原新之助');
}])->get()->toArray();
var_dump($data);
});
7.延迟预加载

实际使用的情况可能比较多,比如:预加载的数据不一定有用,不使用预加载的话,循环一次次查询又太耗时。有没有折中的办法?

都说到这里了,肯定是有的,那就是延迟预加载:

比如原来预加载:

1
$data = \App\Models\Student::with('room','images')->get();

延迟写法:

1
2
3
4
$data = \App\Models\Student::all();
if (#判断是否需要预加载数据) {
$data->load('room', 'images');
}

load 方法也可以设置预加载条件,如官网文档例子:

1
2
3
$books->load(['author' => function ($query) {
$query->orderBy('published_date', 'asc');
}]);
8.Scopes查找范围

假设我们有一个文章列表,其中有个 is_published 字段,标明文章是否发布。

那么取出已发布的文章是这样的:

1
Post::where('is_published', true)->get();

其中 where('is_published',true) 这个条件,可能在很多查询的时候重复出现。

根据 Don’t Repeat Yourself 的原则, Laravel 提供了一个预设查询范围的方式。

1
2
3
4
5
6
7
class Post extentds Model
{
public function scopePublished($query)
{
return $query->where('is_published',true);
}
}

然后所有需要带着个条件的查询都可以直接使用 published 方法:

1
Post::published()->get();