## 新增单条数据:save( )方法 #### 1. 功能:向指定数据表中添加一条记录 >[info] 该方法每次仅能向表中添加一条新记录,添加多条可重复执行,不过后面要学习的saveAll()可一次性添加多条 **** #### 2. 源码:/thinkphp/library/think/Model.php >[info] save方法是Model类中较复杂的方法之一,因为save方法身兼多职,不仅用于新增,还可以用于更新数据,这里我将用于新增部分作了简单注释 ~~~ /** * 保存当前数据对象,并写入表中 * @access public * @param array $data 数据 * @param array $where 更新条件 * @param string $sequence 自增序列名 * @return integer|false */ public function save($data = [], $where = [], $sequence = null) { if (!empty($data)) { // 数据自动验证 if (!$this->validateData($data)) { return false; } // 数据对象赋值,其实就是给$data[]初始化 foreach ($data as $key => $value) { $this->setAttr($key, $value, $data); } if (!empty($where)) { //第二个参数$where条件数组不为空 $this->isUpdate = true; //才允许更新,防止全部更新产生误操作 } //即没有设置条件就拒绝更新 } // 检测字段 if (!empty($this->field)) { //如果字段数组不为空 foreach ($this->data as $key => $val) {//遍历数据对象 if (!in_array($key, $this->field)) { //数据对象中与表字段不对应的元素 unset($this->data[$key]); //应该销毁 } } } // 数据自动完成 $this->autoCompleteData($this->auto); // 自动写入更新时间 if ($this->autoWriteTimestamp && $this->updateTime) { $this->setAttr($this->updateTime, null); } // 事件回调 if (false === $this->trigger('before_write', $this)) { return false; } $pk = $this->getPk(); if ($this->isUpdate) { // 自动更新 $this->autoCompleteData($this->update); // 事件回调 if (false === $this->trigger('before_update', $this)) { return false; } // 去除没有更新的字段 $data = []; foreach ($this->data as $key => $val) { if (in_array($key, $this->change) || $this->isPk($key)) { $data[$key] = $val; } } if (!empty($this->readonly)) { // 只读字段不允许更新 foreach ($this->readonly as $key => $field) { if (isset($data[$field])) { unset($data[$field]); } } } if (empty($where) && !empty($this->updateWhere)) { $where = $this->updateWhere; } if (is_string($pk) && isset($data[$pk])) { if (!isset($where[$pk])) { unset($where); $where[$pk] = $data[$pk]; } unset($data[$pk]); } $result = $this->db()->where($where)->update($data); // 清空change $this->change = []; // 更新回调 $this->trigger('after_update', $this); } else { // 自动写入 $this->autoCompleteData($this->insert); // 自动写入创建时间 if ($this->autoWriteTimestamp && $this->createTime) { $this->setAttr($this->createTime, null); } if (false === $this->trigger('before_insert', $this)) { return false; } //数据库更新底层仍是调用Db::insert()完成 //db()获取数据库Query对象,这样才能调用数据库Query方法:insert(数据对象$data) $result = $this->db()->insert($this->data); // 获取自动增长主键 if ($result && is_string($pk) && !isset($this->data[$pk])) { $insertId = $this->db()->getLastInsID($sequence); if ($insertId) { $this->data[$pk] = $insertId; } } // 标记为更新 $this->isUpdate = true; // 清空change $this->change = []; // 新增回调 $this->trigger('after_insert', $this); } // 写入回调 $this->trigger('after_write', $this); return $result; } ~~~ >[info] 源码简要解析:新增忽略条件[ isUpsata(false) ],只需要用参数$data初始化模型的$data属性,创建数据对象,就可以自动将数据添加到数据表中。 源码中有二条关键语句,负责写表操作: 1. `$result = $this->db()->insert($this->data);`:将数据插入到表中; 2. `$result = $this->db()->where($where)->update($data);`:根据条件更新表中数据; >[warning] 新增与更新操作都是:save方法,那么如何区分新增与更新呢? #### 答案就是:看有没有设置where条件,如果无就是新增,有就是更新。 **由此可见:**最终仍是由数据库查询方法完成相关操作。 * * * * * #### 3. 参数与返回值 * 参数:这里有三个参数,但与新增有用的只有一个 | 序号 | 参数 | 说明 | | --- | --- | --- | | 1 | $data / Array数组 | 用来初始化模型对象的$data属性 | * 返回值:返回受影响记录数量,成功返回1。 * * * * * #### 4. 基本语法: * 格式1:将数据直接写在save方法参数中 ~~~ 模型对象 -> save(数组); ~~~ * 格式2:先生成数据对象,然后用save方法直接写入表中 ~~~ 模型对象 -> data(数组) -> save(); ~~~ >[info] 这二种语法都可以完成新增操作。但是第二种语法结构更加清晰,可读性好,修改更加方便。第一种更加的简洁紧凑,但可读性不如第二个,想用哪个由你决定吧~~ * * * * * #### 5. 实例演示 * 先创建一个模型类与表绑定:Staff.php ~~~ <?php namespace app\index\model; //导入模型类 use think\model; class Staff extends model { //自定义模型类代码 } ~~~ #### **任务:** 新增一条记录到tp5_staff表中 >[info] 根据数据对象创建方式和save参数有无,我们用三种方法来完成这个任务 一、用data方法创建数据对象,来完成新增 * 控制器:Index.php ~~~ <?php namespace app\index\controller; //导入自定义模型类 use app\index\model\Staff; class Index { public function index(){ //1.创建数据:与表中字段对应 $data = []; $data['name'] = '周星星'; $data['sex'] = 1; $data['age'] = 39; $data['salary'] = 3500; $data['dept'] = 3; $data['hiredate'] = date('Y-m-d',time()); //2.创建数据对象 $model = (new Staff) -> data($data); //3.获取新增操作执行前:数据对象原始数据 $data_before = $model -> getData(); //4.查看新增操作执行前的数据对象:$model echo '查看新增操作执行前的数据对象:<br />'; dump($data_before); //5.将数据对象原始数据写入数据表中,返回影响记录数 $affected = $model -> allowField(true) -> save(); //6.获取新增操作执行后:数据对象原始数据 $data_after = $model -> getData(); //7.查看新增操作执行后的数据对象:$model echo '查看新增操作执行后的数据对象:<br />'; dump($data_after); //8. 获取新增记录的主键id,等价于: $model -> id $insert_ID = $data_after['id']; //6.验证是否新增成功 echo $affected ? '新增成功!新记录主键id是:'.$insert_ID : '新增失败!'; } } ~~~ >[info] 代码分析: 1. 其实上面源码,部分语句是可以合并的,分开写是为了可读性更好; 2. `$model = (new Staff) -> data($data);` 中,` (new Staff)` 生成模型对象,`data($data);`给模型对象$data属性初始化,完成数据对象`$model`的创建。 3. `$data_before = $model -> getData();`,此时$model已经是经过初始化了的数据对象,它的$data属性是有值的,该语句可直接获取到这个值($data数组); 4. `$data_after = $model -> getData();`,这时获取的原始数据与操作执行前是不一样的,系统自动添加了自增主键id字段,注意区别。 4. `$affected = $model -> allowField(true) -> save();`:将数据写入表中,并返回影响条数,成功返回 1, 5. `allowField(true)`:过滤非数据表字段数据,仅允许与表字段对应的数组值写入。 6. `echo $affected ? '新增成功!新记录主键id是:'.$insert_ID : '新增失败!'; `: `$affected` 作为判断条件,如果新增成功,`$affected`为 1,失败为 0 ,根据该值返回相应的提示信息,反馈给客户。 * 让我们看一下执行结果: ~~~ 查看新增操作执行前的数据对象: array(6) { ["name"] => string(9) "周星星" ["sex"] => int(1) ["age"] => int(39) ["salary"] => int(3500) ["dept"] => int(3) ["hiredate"] => string(10) "2016-11-25" } 查看新增操作执行后的数据对象: array(7) { ["name"] => string(9) "周星星" ["sex"] => int(1) ["age"] => int(39) ["salary"] => int(3500) ["dept"] => int(3) ["hiredate"] => string(10) "2016-11-25" ["id"] => string(4) "1033" } 新增成功!新记录主键id是:1033 ~~~ * 对应的SQL语句: ~~~ INSERT INTO `tp5_staff` (`name` , `sex` , `age` , `salary` , `dept` , `hiredate`) VALUES ('周星星' , 1 , 39 , 3500 , 3 , '2016-11-25') ~~~ * 再返回到SQLPRO for MySQL,查看一下现在表中数据: ![](https://box.kancloud.cn/17e7a6382112653cfafbd9d91787a576_822x775.png) >[warning] 是不是觉得上面的代码太冗长啦,做为程序员的我们,怎么能够容忍?说实话,能学习到ThinkPHP框架开发,都不算是PHP小白了。下面跟我一道,把上面的代码进行精简~~ * * * * * 二、给sava方法传参方式,完成新增操作 * 控制器:Index.php >[info] 有了上例的基础,看懂下面的代码,是没有难度的 ~~~ <?php namespace app\index\controller; //导入自定义模型类 use app\index\model\Staff; class Index { public function index(){ //1.创建数据:与表中字段对应 $data = []; $data['name'] = '紫霞仙子'; $data['sex'] = 0; $data['age'] = 19; $data['salary'] = 7800; $data['dept'] = 2; $data['hiredate'] = date('Y-m-d',time()); //2.创建模型对象 $model = new Staff(); //3.将数据参数传入save方法,将原始数据写入表中,并返回影响记录数 $affected = $model -> allowField(true) -> save($data); //3.验证是否新增成功 echo $affected ? '新增成功!新记录主键id是:'.$model->getData('id') : '新增失败!'; } } ~~~ * 源码分析:这里我们是把数据直接传给了save方法,其实把数据参数传给Staff类构造器也可以完成操作。为什么呢? 1. Staff类构造方法,就是给对象的$data属性初始化,来创建数据对象的; 2. save方法,其实底层实现就是把数据对象中的$data原始数据写入到数据表中。 >[warning] 所以说,把数组赋给类,还是save方法,本质上是完全相同 。 #### 不好意思,这里面有点点绕,把前面课程多看几遍吧! * 现在我们再简单修改一下控制器:Index.php >[info] 用Staff类的对象构造器创建数据对象,只需实例化传入数据参数即可 ~~~ <?php namespace app\index\controller; //导入自定义模型类 use app\index\model\Staff; class Index { public function index(){ //1.创建数据:与表中字段对应 $data = []; $data['name'] = '紫霞仙子'; $data['sex'] = 0; $data['age'] = 19; $data['salary'] = 7800; $data['dept'] = 2; $data['hiredate'] = date('Y-m-d',time()); //2.创建模型对象 $model = new Staff($data); //3.将数据对象原始数据写入数据表中,返回影响记录数 $affected = $model -> allowField(true) -> save(); //3.验证是否新增成功 echo $affected ? '新增成功!新记录主键id是:'.$model->getData('id') : '新增失败!'; } } ~~~ >[info] 其实这段代码与上面的代码就一点点区别: 1. 第一段代码中,是这样的:$data是传入save方法的 ~~~ /2.创建模型对象 $model = new Staff(); //3.将数据参数传入save方法,将原始数据写入表中,并返回影响记录数 $affected = $model -> allowField(true) -> save($data); ~~~ 2. 第二段代码中,仅是将参数从传给save,改为传给Staff类 ~~~ //2.创建模型对象 $model = new Staff($data); //3.将数据对象原始数据写入数据表中,返回影响记录数 $affected = $model -> allowField(true) -> save(); ~~~ * 当然,运行结果是完全一致的,这里就不再贴出运行结果啦! * * * * * 三、用魔术方法创建数据对象,完成新增数据操作 >[info] 先复习一下Model类的魔术方法:__set( ) ~~~ /** * 修改器 设置数据对象的值 * @access public * @param string $name 名称 * @param mixed $value 值 * @return void */ public function __set($name, $value) //魔术方法来动态设置数据对象$data[] { $this->setAttr($name, $value); //键值对方式写入 } ~~~ **分析**:__set( )可完成模型对象的原始数据$data属性赋值 * 控制器:Index.php ~~~ <?php namespace app\index\controller; //导入自定义模型类 use app\index\model\Staff; class Index { public function index(){ //1.创建模型对象 $model = new Staff(); //2.创建数据:与表中字段对应 $model -> name = '朱老师'; $model -> sex = 1; $model -> age = 31; $model -> salary = 4600; $model -> dept = 1; $model -> hiredate = date('Y-m-d',time()); //3.将数据对象原始数据写入数据表中,返回影响记录数 $affected = $model -> allowField(true) -> save(); //3.验证是否新增成功 echo $affected ? '新增成功!新记录主键id是:'.($model -> id) : '新增失败!'; } } ~~~ >[warning] $model -> id:是调用Model类中的魔术方法__get( )方法实现的 * 返回结果: ~~~ 新增成功!新记录主键id是:1038 ~~~ * 生成的SQL语句: ~~~ INSERT INTO `tp5_staff` (`name` , `sex` , `age` , `salary` , `dept` , `hiredate`) VALUES ('朱老师' , 1 , 31 , 4600 , 1 , '2016-11-25') ~~~ * SQLPRO for MySQL 控制台查看: ![](https://box.kancloud.cn/97838a4af6474dc68281302f31bb0d04_585x406.png) * * * * * 四、save方法多次调用的问题: >[danger] 特别提示: 1. 用save方法连续新增数据时,第二次调用时,数据对象中存在有第一次调用时创建的自增主键的(如:id),该id值会被认为是更新条件,而将新增误判为更新操作。 2. 所以,同一脚本中,第2次调用save方法新增时,一定要在save方法前添加isUpdata(false)方法,来规避这个问题。 * 我们还是用实例来演示: * 控制器:Index.php ~~~ <?php namespace app\index\controller; //导入自定义模型类 use app\index\model\Staff; class Index { public function index(){ //1.创建模型对象 $model = new Staff(); //2.创建数据:与表中字段对应 $model -> name = '王老师'; $model -> sex = 1; $model -> age = 20; $model -> salary = 5000; $model -> dept = 3; $model -> hiredate = date('Y-m-d',time()); //3.将数据对象原始数据写入数据表中,返回影响记录数 $affected = $model -> allowField(true) -> save(); //4.再创建一个要添加到表中的数据 $model -> name = '张老师'; $model -> sex = 1; $model -> age = 50; $model -> salary = 4600; $model -> dept = 2; $model -> hiredate = date('Y-m-d',time()); //5.将该数据添加到表中 $affected = $model -> allowField(true) -> save(); //3.验证是否新增成功 echo $affected ? '新增成功!新记录主键id是:'.($model -> id) : '新增失败!'; } } ~~~ >[info] 上面的代码看上去很简单:添加二条数据到表中,可实际上只会添加name='张老师'的记录,而第一条数据并未没有添加到表中,这是为什么呢? 原因是:第一次添加完name=’王老师‘的记录后,数据对象中自增主键id有了值,而这个值将会被视为更新操作的条件使用,所以第二次执行save方法时,就变成了更新操作,直接根据上一次执行返回的新增id,将name=’王老师‘的记录,更新成了name='王老师'的信息。 * 我们再次修改一下控制器代码:Index.php 将代码中第2次调用save方法部分修改一下: ~~~ ······ //4.再创建一个要添加到表中的数据 $model -> name = '张老师'; $model -> sex = 1; $model -> age = 50; $model -> salary = 4600; $model -> dept = 2; $model -> hiredate = date('Y-m-d',time()); //5.将该数据添加到表中 $affected = $model -> allowField(true) //过滤非表中字段 -> setAttr('id',null) //将数据对像主键置空 -> isUpdate(false) //忽略主键 -> save(); //执行新增操作 ······ ~~~ >[info] -> setAttr('id',null) //将数据对像主键置空 -> isUpdate(false) //忽略主键 这是一步双保险操作,有时:isUpdate(false)无效, 这是是ThinkPHP5的小bug * * * * * #### 6. 总结 >[danger] 我们通过save方法的学习,又全面复习了之前的知识,下节课要学习的saveAll方法就方便多了。