1、什么是ORM?
ORM,即 Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间作一个映射,这样,我们在操作具体的 业务对象时,就不需要再去和复杂的SQL语句打交道,只需简单的操作对象的属性和方法即可。
ORM 两种最常见的实现方式是 ActiveRecord 和 DataMapper,ActiveRecord 尤其流行,在很多框架中都能看到它的身影。两者的区别主要在于 ActiveRecord 中模型与数据表一一对应,而 DataMapper 中模型与数据表是完全分离的。
Laravel 中的 Eloquent ORM 使用的也是 ActiveRecord 实现方式,每一个 Eloquent 模型类对应着数据库中的一张表,我们通过调用模型类的相应方法实现对数据库的增删改查。
2、定义模型
2.1 创建模型
我们使用Artisan命令make:model生成模型类,模型类默认位于app目录下,我们也可以在创建时指定生成目录:
php artisan make:model Models/Post
这样就会在app目录下生成一个Models目录,并且在Models目录下生成一个Post模型类。Laravel 中所有模型类继承自Illuminate\Database\Eloquent\Model类。
2.2 指定表名
如果不手动指定,默认Post对应的数据表为posts,以此类推。也可以通过设置$table属性自定义表名:
public $table = 'posts';
2.3 指定主键
Eloquent默认数据表主键为id,当然也可以通过设置$primaryKey属性来自定义主键:
public $primaryKey = 'id';
2.4 时间戳设置
默认情况下,Eloquent模型类会自动管理时间戳列create_at和update_at(如果定义迁移时设置了这两列的话),如果要取消自动管理,可以设置$timestamps属性为false:
public $timestamps = false;
还有,如果你想要设置时间戳的格式,可以使用$dateFormat属性,该属性决定了日期时间以何种格式存入数据库,以及以何种格式显示:
//设置日期时间格式为Unix时间戳
protected $dateFormat = 'U';
3、查询数据
3.1 获取多个模型
我们可以使用Eloquent模型上的all方法获取所有模型实例,比如我们通过如下方法获取所有文章:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Http\Requests; use App\Http\Controllers\Controller; use App\Models\Post; class TestController extends Controller { /** * Display a listing of the resource. * * @return Response */ public function index() { //获取多个Eloquent模型 $posts = Post::all(); dd($posts); } }
注:这里需要引入 App\Models\Post
Collection {#229 ▼
#items: array:3 [▼
0 => Post {#230 ▶}
1 => Post {#231 ▶}
2 => Post {#232 ▶}
]
}
可见输出结果是模型数组集合,每一个$items元素对应一个Post模型实例。
此外,需要了解的是每一个Eloquent模型本身都是一个查询构建器,所有我们可以调用所有查询构建器上的方法,只不过第一个方法调用都要使用静态方法调用:
$posts = Post::where('id','<',3)->orderBy('id','desc')->take(1)->get();
dd($posts);
Collection {#227 ▼
#items: array:1 [▼
0 => Post {#228 ▼
#connection: null
#table: null
#primaryKey: "id"
#perPage: 15
+incrementing: true
+timestamps: true
#attributes: array:8 [▶]
#original: array:8 [▶]
#relations: []
#hidden: []
#visible: []
#appends: []
#fillable: []
#guarded: array:1 [▶]
#dates: []
#dateFormat: null
#casts: []
#touches: []
#observables: []
#with: []
#morphClass: null
+exists: true
+wasRecentlyCreated: false
}
]
}
也许你已经注意到了,模型查询返回结果都是Illuminate\Database\Eloquent\Collection的一个实例,该类实现了ArrayAccess接口,所以我们可以像访问数组一样访问该实例,此外,该Collection类还提供了很多其它有用的方法对查询结果进行处理,详见源码。
既然Eloquent模型是查询构建器,自然也支持分组块获取数据:
Post::chunk(2,function($posts){
foreach ($posts as $post) {
echo $post->title.'<br>';
}
});
输出结果如下:
test 1
test 2
test 3
3.2 获取单个模型
可以使用查询构建器方法获取单个模型实例:
$post = Post::where('id',1)->first();
dd($post);
当然也可以通过Eloquent模型类提供的快捷方法find:
$post = Post::find(1);
两者输出结果一样:
如果没有找到对应的表记录,会输出null,如果我们想要捕获查询结果为空的异常并进行处理,比如跳转到404页面,可以使用findOrFail或者firstOrFail方法,如果表记录存在,两者返回获取到的第一条记录,否则抛出Illuminate\Database\Eloquent\ModelNotFoundException异常。
3.3 聚合函数查询
如果要对查询结果进行计数、统计、最大值/最小值、平均数等聚合运算,可以使用查询构建器上的对应方法,我们我们查询文章总数:
$count = Post::where('id','>',0)->count();
echo $count;
输出结果为3,又或者我们想要获取文章最大阅读数:
$views = Post::where('id','>',0)->max('views');
echo $views;
输出结果为800。
4、创建模型
4.1 使用save方法创建模型
调用Eloquent模型类的save方法即可创建模型并插入数据到数据库:
$post = new Post;
$post->title = 'test 4';
$post->content = 'test content';
$post->user_id = 1;
$post->cat_id = 1;
if($post->save()){
echo '添加文章成功!';
}else{
echo '添加文章失败!';
}
4.2 使用create方法插入数据
除此之外还可以使用create方法插入数据,由于该方法中用到了批量赋值(Mass Assignment),所以我们需要在模型类中设置 fillable属性或者 guarded属性,以表明哪些属性可以通过该方法设置,哪些不可以。
开始之前,我们先解释下什么是批量赋值,以及为什么要使用批量赋值。
批量赋值的英文名称是Mass Assignment,所谓的批量赋值是指当我们将一个数组发送到模型类用于创建新的模型实例的时候(通常是表单请求数据),我们可以简单通过如下方式实现:
$post = Post::create(Input::all());
而不是像使用save方法那样一个一个的设置属性值,如果模型属性很多的话,使用save简直是噩梦有木有。
但事物总是相对的,使用批量赋值是很方便,但同时也带来了安全隐患,很多时候模型类的某些属性值不是我们所期望通过批量赋值修改的,比如用户模型有个user_type属性,如果用户通过请求数据将其类型修改为管理员类型,这显然是不允许的,正是基于这一考虑,Eloquent模型类为我们提供了 fillable属性和 guarded属性,我们可以将其分别看作“白名单”和“黑名单”,定义在 fillable中的属性可以通过批量赋值进行赋值,而定义在 guarded中的属性在批量赋值时会被过滤掉。
那么如果我们确实想要修改定义在$guarded中的属性怎么办?答案是使用save方法。
此外需要注意的是 fillable和 guarded方法同时只能定义一个,原因嘛很简单,非黑即白,定义了一个另外一个也就确定了。
可见批量赋值不仅为我们创建模型提供了便利,还避免了安全隐患,提高了系统的安全性。
下面我们来演示一下批量赋值的使用。首先在Post模型中定义$guarded属性如下:
protected $guarded = ['views','user_id','updated_at','created_at'];
然后在控制器中实现创建模型实例的逻辑:
Post {#222 ▼
#guarded: array:4 [▼
0 => "views"
1 => "user_id"
2 => "updated_at"
3 => "created_at"
]
#connection: null
#table: null
#primaryKey: "id"
#perPage: 15
+incrementing: true
+timestamps: true
#attributes: array:6 [▼
"title" => "test5"
"content" => "test content"
"cat_id" => 1
"updated_at" => "2016-03-14 05:26:20"
"created_at" => "2016-03-14 05:26:20"
"id" => 5
]
#original: array:6 [▶]
#relations: []
#hidden: []
#visible: []
#appends: []
#fillable: []
#dates: []
#dateFormat: null
#casts: []
#touches: []
#observables: []
#with: []
#morphClass: null
+exists: true
+wasRecentlyCreated: true
}
可见user_id和views字段都没有插入进去,这正是$guarded发挥了作用,如果要设置这两个值也很简单:
$input = [
'title'=>'test 5',
'content'=>'test content',
'cat_id'=>1,
'views'=>100,
'user_id'=>2
];
$post = Post::create($input);
$post->user_id = 2;
$post->views = 100;
$post->save();
dd($post);
输出结果如下:
Post {#222 ▼
#guarded: array:4 [▶]
#connection: null
#table: null
#primaryKey: "id"
#perPage: 15
+incrementing: true
+timestamps: true
#attributes: array:8 [▼
"title" => "test5"
"content" => "test content"
"cat_id" => 1
"updated_at" => "2016-03-14 05:38:21"
"created_at" => "2016-03-14 05:38:21"
"id" => 6
"user_id" => 2
"views" => 100
]
#original: array:8 [▶]
#relations: []
#hidden: []
#visible: []
#appends: []
#fillable: []
#dates: []
#dateFormat: null
#casts: []
#touches: []
#observables: []
#with: []
#morphClass: null
+exists: true
+wasRecentlyCreated: true
}
4.3 其他插入数据的方法
Eloquent模型类还支持其它插入数据的方法——firstOrCreate和firstOrNew,两者都是先通过通过传入属性值在数据库中查找匹配记录,如果没有找到则创建一个新的模型实例,不同之处在于后者不会将数据持久化到数据库,需要调用save方法才行。
5、更新模型
5.1 使用save方法更新模型
save方法还可以用于更新模型,要更新模型数据,先要获取该模型实例,然后修改模型属性,再调用save方法保存即可:
$post = Post::find(1);
$post->title = 'test update';
if ($post->save()) {
echo 'update success';
} else {
echo 'update fail';
}
5.2 使用update方法更新数据
和create相对应的,Eloquent模型类还支持使用update方法更新数据,同样要用到批量赋值:
$input = [
'title'=>'test 6 title',
'content'=>'test content 6',
'cat_id'=>1,
'views'=>200,
'user_id'=>1
];
$post = Post::find(3);
if($post->update($input)){
echo 'update success';
dd($post);
}else{
echo 'update fail';
}
Post {#228 ▼
#guarded: array:4 [▶]
#connection: null
#table: null
#primaryKey: "id"
#perPage: 15
+incrementing: true
+timestamps: true
#attributes: array:8 [▼
"id" => 3
"title" => "test 6 title"
"content" => "test content 6"
"user_id" => 2
"created_at" => "0000-00-00 00:00:00"
"updated_at" => "2016-03-14 05:45:54"
"cat_id" => 1
"views" => 800
]
#original: array:8 [▶]
#relations: []
#hidden: []
#visible: []
#appends: []
#fillable: []
#dates: []
#dateFormat: null
#casts: []
#touches: []
#observables: []
#with: []
#morphClass: null
+exists: true
+wasRecentlyCreated: false
}
可见user_id和views并没有更新。
6、删除模型
6.1 使用delete删除模型
删除模型很简单,先获取要删除的模型实例,然后调用delete方法即可:
$post = Post::find(5);
if($post->delete()){
echo '删除文章成功!';
}else{
echo '删除文章失败!';
}
该方法返回true或false。
6.2 使用destroy删除模型
当然如果已知要删除的模型id的话,可以用更简单的方法destroy直接删除:
$deleted = Post::destroy(5);
你也可以一次传入多个模型id删除多个模型:
$deleted = Post::destroy([1,2,3,4,5]);
调用destroy方法返回被删除的记录数。
6.3 使用查询构建器删除模型
既然前面提到Eloquent模型本身就是查询构建器,也可以使用查询构建器风格删除模型,比如我们要删除所有浏览数为0的文章,可以使用如下方式:
$deleted = Post::where('views', 0)->delete();
返回结果为被删除的文章数。
7、软删除及其相关实现
7.1 软删除实现
上述删除方法都会将数据表记录从数据库删除,此外Eloquent模型还支持软删除。
所谓软删除指的是数据表记录并未真的从数据库删除,而是将表记录的标识状态标记为软删除,这样在查询的时候就可以加以过滤,让对应表记录看上去是被”删除“了。Laravel中使用了一个日期字段作为标识状态,这个日期字段可以自定义,这里我们使用deleted_at,如果对应模型被软删除,则deleted_at字段的值为删除时间,否则该值为空。
要让Eloquent模型支持软删除,还要做一些设置。首先在模型类中要使用SoftDeletestrait,该trait为软删除提供一系列相关方法,具体可参考源码Illuminate\Database\Eloquent\SoftDeletes,此外还要设置$date属性数组,将deleted_at置于其中:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Post extends Model { use SoftDeletes; //设置表名 public $table = 'posts'; //设置主键 public $primaryKey = 'id'; //设置日期时间格式 public $dateFormat = 'U'; protected $guarded = ['id','views','user_id','updated_at','created_at']; protected $dates = ['delete_at']; }
然后对应的数据库posts中添加deleted_at列,我们使用迁移来实现,先执行Artisan命令:
php artisan make:migration alter_posts_deleted_at –table=posts
然后编辑生成的PHP文件如下:
php artisan migrate
这样posts中就有了deleted_at列。接下来,我们在控制器中编写测试代码:
$post = Post::find(4);
$post->delete();
if($post->trashed()){
echo '软删除成功!';
dd($post);
}else{
echo '软删除失败!';
}
结果如下
Post {#234 ▼
#guarded: array:5 [▶]
#dates: array:1 [▶]
#connection: null
#table: null
#primaryKey: "id"
#perPage: 15
+incrementing: true
+timestamps: true
#attributes: array:9 [▼
"id" => 4
"title" => "test4"
"content" => "test4"
"user_id" => 1
"created_at" => "2016-03-14 05:13:57"
"updated_at" => "2016-03-14 05:13:57"
"cat_id" => 1
"views" => 0
"deleted_at" => Carbon {#230 ▼
+"date": "2016-03-14 06:19:58"
+"timezone_type": 3
+"timezone": "UTC"
}
]
#original: array:9 [▶]
#relations: []
#hidden: []
#visible: []
#appends: []
#fillable: []
#dateFormat: null
#casts: []
#touches: []
#observables: []
#with: []
#morphClass: null
+exists: false
+wasRecentlyCreated: false
#forceDeleting: false
}
当我们再次通过下面这段代码获取所有文章:
posts=Post::all();dd( posts);
已经看不到id为4的文章的身影了。
那如果想要在查询结果中包含软删除的记录呢?可以使用SoftDeletes trait上的withTrashed方法:
$posts = Post::withTrashed()->get();
dd($posts);
id为4的文章又出现在了查询结果中。有时候我们只想要查看被软删除的模型,这也有招,通过SoftDeletes上的onlyTrashed方法即可:
$posts = Post::onlyTrashed()->get();
dd($posts);
7.2 软删除恢复
有时候我们需要恢复被软删除的模型,可以使用SoftDeletes提供的restore方法:
恢复单个模型
$post = Post::find(6);
$post->restore();
恢复多个模型
Post::withTrashed()->where('id','>',1)->restore();
恢复所有模型
Post::withTrashed()->restore();
恢复关联查询模型
$post = Post::find(6);
$post->history()->restore();
7.3 强制删除
如果模型配置了软删除但我们确实要删除改模型对应数据库表记录,则可以使用SoftDeletes提供的forceDelete方法:
$post = Post::find(4);
$post->forceDelete();
8、查询作用域
Eloquent还支持将一些常用的查询封装到模型方法中,方便调用,我们将其称之为“查询作用域”,实现查询作用域很简单,只需要在模型方法前加上scope前缀即可,比如我们经常需要获取浏览数最高的文章,就可以使用该机制实现——在Post中定义一个scopePopular方法:
public function scopePopular($query) {
return $query->where('views','>=',100);
}
对应的,我们在控制器中定义测试代码如下:
$posts = Post::popular()->orderBy('views','desc')->get();
foreach ($posts as $post) {
echo '<'.$post->title.'> '.$post->views.'views<br>';
}
此外,查询作用域还支持动态传入参数,为了测试该方法我们为posts新增一个status字段, 同时在模型类中新增一个scopeStatus方法:
public function scopeStatus($query,$status=1) {
return $query->where('status',$status);
}
接下来测试下该方法:
$posts = Post::popular()->status(1)->orderBy('views','desc')->get();
foreach ($posts as $post) {
echo '<'.$post->title.'> '.$post->views.'views<br>';
}
9、模型事件
Eloquent也支持模型事件——当模型被创建、更新或删除的时候触发相应事件,Eloquent目前支持八种事件类型:creating、created、updating、updated、saving、saved、deleting、deleted。
deleting和deleted很好理解,在删除模型时触发,deleting在删除操作前执行,deleted在删除完成后执行。
当创建模型时,依次执行saving、creating、created和saved,同理在更新模型时依次执行saving、updating、updated和saved。无论是使用批量赋值(create/update)还是直接调用save方法,都会触发对应事件(前提是注册了相应的模型事件)。
你可以在任何你喜欢的地方注册模型事件,这里我们选择在服务提供者AppServiceProvider的boot方法中注册:
Post::saving(function($post){
echo 'saving event is fired<br>';
});
Post::creating(function($post){
echo 'creating event is fired<br>';
});
Post::created(function($post){
echo 'created event is fired<br>';
});
Post::saved(function($post){
echo 'saved event is fired<br>';
});
然后在控制器中编写测试代码如下:
$data = array(
'title'=>'test model event',
'content'=>'test content',
'cat_id'=>1,
);
$post = Post::create($data);
if(!$post->exists){
echo '添加文章失败!';exit();
}
echo '<'.$post->title.'>保存成功!';
接下来在浏览中访问http://selfstudy.com/testSql,页面输入如下:
saving event is fired
creating event is fired
created event is fired
saved event is fired
<test model event>保存成功!
需要注意的是如果saving/creating/updating/deleting事件返回false,则相应的创建/更新/删除操作会退出,不再往下执行,比如我们修改上述creating事件代码如下:
Post::creating(function($post){
echo 'creating event is fired<br>';
if($post->cat_id==1)
return false;
});
也就是当文章分类id等于1的时候,不再往下执行,在浏览器中再次访问http://selfstudy.com/testSql,页面输出如下:
saving event is fired
creating event is fired
添加文章失败!
有了模型事件之后,我们就很方便地在模型创建、更新或删除的不同生命周期阶段添加相应的业务逻辑。