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' => [ 'mysql' => [ 'driver' => 'mysql' , 'host' => env('DB_HOST' , 'localhost' ), 'database' => env('DB_DATABASE' ), 'username' => env('DB_USERNAME' , 'forge' ), 'password' => env('DB_PASSWORD' , '' ), 'charset' => 'utf8mb4' , '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 (! 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' ; 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' ; protected $primaryKey = 'id' ; protected $connection = 'default' ; public function hasOneGroup ( ) { return $this ->hasOne('App\Core\Model\Group' ,'id' ,'group_id' ); } 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 $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 User::find(1 )->delete(); User::where('id' , 1 )->delete(); 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 ; 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() ) { } User::withTrashed() ->where('id' , '>' , 0 ) ->get(); User::onlyTrashed() ->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 ) ->skip(2 ) ->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 $user = User::find(1 );$user ->username = 'newname' ;$user ->save();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_id
和 subject_id
关联选课关系
故而:
Rooms (1) ---------------- (n) Students
Students (n) ---------------- (n) Subjects
先做一批测试数据,初始化数据的代码在 routes.php
中,/try/init/
一对一关系:HasOne
& BelongsTo
一对多关系:HasMany
& BelongsTo
一对一和一对多比较类似,以一对多为例介绍。
Rooms 与 Students 是一对多关系,面上看来,一个 room
会 HasMany
Students
,反之,每个 student
都 belongsTo
一个 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'); }
hasMany
和 belongsTo
的参数是相似的,第一个是对应的 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 public function bind ( ) { return $this ->morphTo(); } public function images ( ) { return $this ->morphMany('App\Models\Image' , 'bind' ); }
调用数据的方法类似上文的 hasMany
和 belongsTo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 $data = \App\Models\Room::has('students' )->get();$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 ;
以上可以看出,在这个例子中仅依靠 belongsTo
和 hasOne
/hasMany
这类方法实现的关联数据查询,时间复杂度O(n),而加上预加载之后就变成了 O(1)。而众所周知PHP最大的瓶颈就在Mysql查询,所以预加载有效地减少了查询次数,提高了查询的效率。
预加载还有多种模式,比如:App\Models\Student
中同时定义了room
、images
两种关联模型,那么就可以同时预加载两组数据:
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();