## 二、模型 模型代表了应用程序中的信息(数据)和处理数据的规则。模型主要用于管理与相应数据库表进行交互的规则。 大多数情况中,在应用程序中,数据库中每个表将对应一个模型。 应用程序中的大部分业务逻辑都将集中在模型里。 Phalcon\\Mvc\\Model 是 Phalcon 应用程序中所有模型的基类。它保证了数据库的独立性,基本的 CURD 操作, 高级的查询功能,多表关联等功能。 Phalcon\\Mvc\\Model 不需要直接使用 SQL 语句,因为它的转换方法,会动态的调用相应的数据库引擎进行处理。 ### 2.1 映射其他表 默认情况下,模型 “Store\\Toys\\RobotParts” 对应的是数据库表 “robot\_parts”, 如果想映射到其他数据库表,可以使用 `setSource()` 方法: ``` namespace Store\Toys; use Phalcon\Mvc\Model; class RobotParts extends Model { public function initialize() { $this->setSource("toys_robot_parts"); } } ``` ### 2.2 Setters/Getters #### 2.2.1 不使用 模型可以通过公共属性的方式实现,意味着模型的所有属性在实例化该模型的地方可以无限制的读取和更新。 ``` namespace Store\Toys; use Phalcon\Mvc\Model; class Robots extends Model{ public $id; public $name; public $price; } ``` #### 2.2.2 使用 通过使用 getters/setters 方法,可以控制哪些属性可以公开访问,并且对属性值执行不同的形式的转换,同时可以保存在模型中的数据添加相应的验证规则。 ``` namespace Store\Toys; use InvalidArgumentException; use Phalcon\Mvc\Model; class Robots extends Model{ protected $id; protected $name; protected $price; public function getId(){ return $this->id; } public function setName($name){ // The name is too short? if (strlen($name) < 10) { throw new InvalidArgumentException( "The name is too short" ); } $this->name = $name; } public function getName(){ return $this->name; } public function setPrice($price){ // Negative prices aren't allowed if ($price < 0) { throw new InvalidArgumentException( "Price can't be negative" ); } $this->price = $price; } public function getPrice(){ // Convert the value to double before be used return (double) $this->price; } } ``` #### 2.2.3 比较 - 公共属性的方式可以在开发中降低复杂度。 - getters/setters 的实现方式可以显著的增强应用的可测试性、扩展性和可维护性。 - ORM同时兼容这两种方法。 #### 2.2.4 注意 1. 如果你在字段名中使用了下划线,那么你必须使用驼峰命名法去代替下划线来访问属性todo - `$model->getPropertyName` 代替 `$model->getProperty_name` - `$model->findByPropertyName` 代替 `$model->findByProperty_name` 2. 在模型名中也是同样的:例如,数据库名node\_body对应的模型文件名和类名应该是NodeBody而不是Node\_body 3. 如果你不想遵循这些规则,那么您可以通过列映射来正确让模型访问数据库 ### 2.3 理解记录对象 #### 2.3.1 model中查询记录 通过主键找到某一条记录并且打印它的名称 表结构如下: ``` mysql> select * from robots; +----+------------+------------+------+ | id | name | type | year | +----+------------+------------+------+ | 1 | Robotina | mechanical | 1972 | | 2 | Astro Boy | mechanical | 1952 | | 3 | Terminator | cyborg | 2029 | +----+------------+------------+------+ 3 rows in set (0.00 sec) ``` ##### 1. 查询值 ``` use Store\Toys\Robots; // Find record with id = 3 $robot = Robots::findFirst(3); // Prints "Terminator" echo $robot->name; ``` ##### 2. 修改值 `findFirst()`方法查询的值会加载到内存中,可以直接修改其中部分或全部值,进行`save()`操作更新 ``` use Store\Toys\Robots; $robot = Robots::findFirst(3); $robot->name = "RoboCop"; $robot->save(); ``` #### 2.3.2 查找记录 ##### 1. find()基本查询 `find()`方法可以从一个模型中查找一条或多条记录 ``` // How many robots are there? $robots = Robots::find(); echo "There are ", count($robots), "\n"; // How many mechanical robots are there? $robots = Robots::find("type = 'mechanical'"); echo "There are ", count($robots), "\n"; // Get and print virtual robots ordered by name $robots = Robots::find( [ "type = 'virtual'", "order" => "name", ] ); foreach ($robots as $robot) { echo $robot->name, "\n"; } // Get first 100 virtual robots ordered by name $robots = Robots::find( [ "type = 'virtual'", "order" => "name", "limit" => 100, ] ); foreach ($robots as $robot) { echo $robot->name, "\n"; } ``` ##### 2. 查询条件 可用的查询选项如下: 参数 描述 举例 conditions 查询操作的搜索条件。用于提取只有那些满足指定条件的记录。默认情况下 [Phalcon\\Mvc\\Model](http://www.iphalcon.cn/api/Phalcon_Mvc_Model.html) 假定第一个参数就是查询条件。 `"conditions" => "name LIKE'steve%'"` columns 只返回指定的字段,而不是模型所有的字段。 当用这个选项时,返回的是一个不完整的对象。 `"columns" => "id, name"` bind 绑定与选项一起使用,通过替换占位符以及转义字段值从而增加安全性。 `"bind" => ["status" => "A","type" => "some-time"]` bindTypes 当绑定参数时,可以使用这个参数为绑定参数定义额外的类型限制从而更加增强安全性。 `"bindTypes" =>[Column::BIND_PARAM_STR,Column::BIND_PARAM_INT]` order 用于结果排序。使用一个或者多个字段,逗号分隔。 `"order" => "name DESC,status"` limit 限制查询结果的数量在一定范围内。 `"limit" => 10` offset Offset the results of the query by a certain amount `"offset" => 5` group 从多条记录中获取数据并且根据一个或多个字段对结果进行分组。 `"group" => "name, status"` for\_update 通过这个选项, [Phalcon\\Mvc\\Model](http://www.iphalcon.cn/api/Phalcon_Mvc_Model.html) 读取最新的可用数据,并且为读到的每条记录设置独占锁。 `"for_update" => true` shared\_lock 通过这个选项, [Phalcon\\Mvc\\Model](http://www.iphalcon.cn/api/Phalcon_Mvc_Model.html) 读取最新的可用数据,并且为读到的每条记录设置共享锁。 `"shared_lock" => true` cache 缓存结果集,减少了连续访问数据库。 `"cache" => ["lifetime" =>3600, "key" => "my-find-key"]` hydration Sets the hydration strategy to represent each returned record in the result `"hydration" =>Resultset::HYDRATE_OBJECTS`1. 可以通过字符串查询 ``` $robot = Robots::findFirst("type = 'mechanical'"); ``` 2. 通过关联数组 ``` $robot = Robots::findFirst( [ "type = 'virtual'", "order" => "name", ] ); ``` 3. 通过面向对象 ``` $robots = Robots::query() ->where("type = :type:") ->andWhere("year < 2000") ->bind(["type" => "mechanical"]) ->order("name") ->execute(); ``` 静态方法 query() 返回一个对IDE自动完成友好的 Phalcon\\Mvc\\Model\\Criteria 对象。 4. 通过属性名称 Phalcon提供了一个`findFirstBy<property-name>()`方法 这个方法扩展了前面提及的 findFirst() 方法。它允许您利用方法名中的属性名称,通过将要搜索的该字段的内容作为参数传给它,来快速从一个表执行检索操作 ``` $name = "Terminator"; $robot = Robots::findFirstByName($name); ``` 5. 总结 - 所有查询在内部都以 **PHQL** 查询的方式处理。 - PHQL是一个高层的、面向对象的类SQL语言。 - 通过PHQL语言你可以使用更多的比如join其他模型、定义分组、添加聚集等特性。 ##### 3. 绑定查询特性 - [Phalcon\\Mvc\\Model](http://www.iphalcon.cn/api/Phalcon_Mvc_Model.html) 中支持绑定参数 - 使用绑定参数对性能有一点很小的影响 - 可以减少消除代码受SQL注入攻击的可能性 - 绑定参数支持字符串和整数占位符 ##### 4. 绑定查询方式 1. 字符串占位符 ``` // Query robots binding parameters with string placeholders // Parameters whose keys are the same as placeholders $robots = Robots::find( [ "name = :name: AND type = :type:", "bind" => [ "name" => "Robotina", "type" => "maid", ], ] ); ``` 2. 整数占位符 ``` // Query robots binding parameters with integer placeholders $robots = Robots::find( [ "name = ?1 AND type = ?2", "bind" => [ 1 => "Robotina", 2 => "maid", ], ] ); ``` 3. 混合字符串 ``` // Query robots binding parameters with both string and integer placeholders // Parameters whose keys are the same as placeholders $robots = Robots::find( [ "name = :name: AND type = ?1", "bind" => [ "name" => "Robotina", 1 => "maid", ], ] ); ``` 4. 注意 - 如果是数字占位符,则必须把它们定义成整型(如1或者2)。若是定义为字符串型(如”1”或者”2”),则这个占位符不会被替换。 - 使用PDO\_的方式会自动转义字符串。它依赖于字符集编码,因此建议在连接参数或者数据库配置中设置正确的字符集编码。 若是设置错误的字符集编码,在存储数据或检索数据时,可能会出现乱码。 5. 设置参数的“bindTypes” 1. 使用bindTypes允许你根据数据类型来定义参数应该如何绑定 ``` use Phalcon\Db\Column; use Store\Toys\Robots; // Bind parameters $parameters = [ "name" => "Robotina", "year" => 2008, ]; // Casting Types $types = [ "name" => Column::BIND_PARAM_STR, "year" => Column::BIND_PARAM_INT, ]; // Query robots binding parameters with string placeholders $robots = Robots::find( [ "name = :name: AND year = :year:", "bind" => $parameters, "bindTypes" => $types, ] ); ``` > 默认的参数绑定类型是 Phalcon\\Db\\Column::BIND\_PARAM\_STR , 若所有字段都是string类型,则不用特意去设置参数的“bindTypes”. 2. 如果你的绑定参数是array数组,那么数组索引必须从数字0开始 ``` use Store\Toys\Robots; $array = ["a","b","c"]; // $array: [[0] => "a", [1] => "b", [2] => "c"] unset($array[1]); // $array: [[0] => "a", [2] => "c"] // Now we have to renumber the keys $array = array_values($array); // $array: [[0] => "a", [1] => "c"] $robots = Robots::find( [ 'letter IN ({letter:array})', 'bind' => [ 'letter' => $array ] ] ); ``` > 参数绑定的方式适用于所有与查询相关的方法,如 find() , findFirst() 等等, 同时也适用于与计算相关的方法,如 count(), sum(), average() 等等. 6. 隐式的参数绑定 若使用如下方式,phalcon也会自动为你进行参数绑定: ``` use Store\Toys\Robots; // Explicit query using bound parameters $robots = Robots::find( [ "name = ?0", "bind" => [ "Ultron", ], ] ); // Implicit query using bound parameters(隐式的参数绑定) $robots = Robots::findByName("Ultron"); ``` #### 2.3.4 模型结果集 - `findFirst()` 方法直接返回一个被调用对象的实例(如果有结果返回的话) - `find()` 方法返回一个 [Phalcon\\Mvc\\Model\\Resultset\\Simple](http://www.iphalcon.cn/api/Phalcon_Mvc_Model_Resultset_Simple.html) 对象。这个对象也封装进了所有结果集的功能,比如遍历、查找特定的记录、统计等等。 这些对象比一般数组功能更强大。最大的特点是 [Phalcon\\Mvc\\Model\\Resultset](http://www.iphalcon.cn/api/Phalcon_Mvc_Model_Resultset.html) **每时每刻只有一个结果在内存中**。这对操作大数据量时的内存管理相当有帮助。 ##### 1. 假设结果集 ``` use Store\Toys\Robots; // Get all robots $robots = Robots::find(); ``` ##### 2. 循环结果集 ``` // Traversing with a foreach foreach ($robots as $robot) { echo $robot->name, "\n"; } // Traversing with a while $robots->rewind(); while ($robots->valid()) { $robot = $robots->current(); echo $robot->name, "\n"; $robots->next(); } ``` ##### 3. 结果集总数 ``` // Count the resultset echo count($robots); // Alternative way to count the resultset echo $robots->count(); ``` ##### 4. 结果集指针操作 ``` // Move the internal cursor to the third robot $robots->seek(2); $robot = $robots->current(); // Access a robot by its position in the resultset $robot = $robots[5]; // Check if there is a record in certain position if (isset($robots[3])) { $robot = $robots[3]; } // Get the first record in the resultset $robot = $robots->getFirst(); // Get the last record $robot = $robots->getLast(); ``` ##### 5. 结果集总结 - Phalcon 的结果集模拟了可滚动的游标,你可以 **通过位置,或者内部指针去访问任何一条特定的记录** - 注意有一些数据库系统不支持滚动游标(MySQL支持),这就使得查询会被重复执行, 以便回放光标到最开始的位置,然后获得相应的记录。类似地,如果多次遍历结果集,那么必须执行相同的查询次数。 - 将大数据量的查询结果存储在内存会消耗很多资源,正因为如此,分成每32行一块从数据库中获得结果集,以减少重复执行查询请求的次数,在一些情况下也节省内存。 - 注意结果集可以序列化后保存在一个后端缓存里面。 [Phalcon\\Cache](http://www.iphalcon.cn/reference/cache.html) 可以用来实现这个。但是,序列化数据会导致 [Phalcon\\Mvc\\Model](http://www.iphalcon.cn/api/Phalcon_Mvc_Model.html) 将从数据库检索到的所有数据以一个数组的方式保存,因此在这样执行的地方 **会消耗更多的内存**。 ``` // Query all records from model parts $parts = Parts::find(); // Store the resultset into a file file_put_contents( "cache.txt", serialize($parts) ); // Get parts from file $parts = unserialize( file_get_contents("cache.txt") ); // Traverse the parts foreach ($parts as $part) { echo $part->id; } ``` #### 2.3.5 过滤结果集 过滤数据最有效的方法是设置一些查询条件,数据库会利用表的索引快速返回数据。Phalcon 额外的允许你通过任何数据库不支持的方式过滤数据。 ``` $customers = Customers::find(); $customers = $customers->filter( function ($customer) { // Return only customers with a valid e-mail if (filter_var($customer->email, FILTER_VALIDATE_EMAIL)) { return $customer; } } ); ``` #### 2.3.6 获取记录的初始化以及准备 有时从数据库中获取了一条记录之后, **在被应用程序使用之前,需要对数据进行初始化**。 你可以在模型中实现”`afterFetch`”方法,在模型实例化之后会执行这个方法,并将数据分配给它: ``` namespace Store\Toys; use Phalcon\Mvc\Model; class Robots extends Model { public $id; public $name; public $status; public function beforeSave() { // Convert the array into a string $this->status = join(",", $this->status); } public function afterFetch() { // Convert the string to an array $this->status = explode(",", $this->status); } public function afterSave() { // Convert the string to an array $this->status = explode(",", $this->status); } } ``` 如果使用`getters/setters`方法代替公共属性的取/赋值,你能在它被调用时,对成员属性进行初始化: ``` namespace Store\Toys; use Phalcon\Mvc\Model; class Robots extends Model { public $id; public $name; public $status; public function getStatus() { return explode(",", $this->status); } } ``` #### 2.3.7 生成运算 Phalcon提供了一些计算的函数,如`COUNT, SUM, MAX, MIN or AVG` ##### 1. count ``` // How many employees are? $rowcount = Employees::count(); // How many different areas are assigned to employees? $rowcount = Employees::count( [ "distinct" => "area", ] ); // How many employees are in the Testing area? $rowcount = Employees::count( "area = 'Testing'" ); // Count employees grouping results by their area $group = Employees::count( [ "group" => "area", ] ); foreach ($group as $row) { echo "There are ", $row->rowcount, " in ", $row->area; } // Count employees grouping by their area and ordering the result by count $group = Employees::count( [ "group" => "area", "order" => "rowcount", ] ); // Avoid SQL injections using bound parameters $group = Employees::count( [ "type > ?0", "bind" => [ $type ], ] ); ``` ##### 2. Sum ``` // How much are the salaries of all employees? $total = Employees::sum( [ "column" => "salary", ] ); // How much are the salaries of all employees in the Sales area? $total = Employees::sum( [ "column" => "salary", "conditions" => "area = 'Sales'", ] ); // Generate a grouping of the salaries of each area $group = Employees::sum( [ "column" => "salary", "group" => "area", ] ); foreach ($group as $row) { echo "The sum of salaries of the ", $row->area, " is ", $row->sumatory; } // Generate a grouping of the salaries of each area ordering // salaries from higher to lower $group = Employees::sum( [ "column" => "salary", "group" => "area", "order" => "sumatory DESC", ] ); // Avoid SQL injections using bound parameters $group = Employees::sum( [ "conditions" => "area > ?0", "bind" => [ $area ], ] ); ``` ##### 3. Average ``` // What is the average salary for all employees? $average = Employees::average( [ "column" => "salary", ] ); // What is the average salary for the Sales's area employees? $average = Employees::average( [ "column" => "salary", "conditions" => "area = 'Sales'", ] ); // Avoid SQL injections using bound parameters $average = Employees::average( [ "column" => "age", "conditions" => "area > ?0", "bind" => [ $area ], ] ); ``` ##### 4. Max/Min ``` // What is the oldest age of all employees? $age = Employees::maximum( [ "column" => "age", ] ); // What is the oldest of employees from the Sales area? $age = Employees::maximum( [ "column" => "age", "conditions" => "area = 'Sales'", ] ); ``` #### 2.3.8 创建与更新记录 ``` public boolean save ([array $data], [array $whiteList]) ``` 保存和更新均使用:`Phalcon\Mvc\Model::save()` ##### 1. 单独属性赋值保存 ``` use Store\Toys\Robots; $robot = new Robots(); $robot->type = "mechanical"; $robot->name = "Astro Boy"; $robot->year = 1952; if ($robot->save() === false) { echo "Umh, We can't store robots right now: \n"; $messages = $robot->getMessages(); foreach ($messages as $message) { echo $message, "\n"; } } else { echo "Great, a new robot was saved successfully!"; } ``` ##### 2. 数组赋值保存 ``` $robot->save( [ "type" => "mechanical", "name" => "Astro Boy", "year" => 1952, ] ); ``` ##### 3. form传值保存 ``` $robot->save($_POST); // 如上代码不安全,没有经过过滤,什么值都可以写入到数据库 $robot->save( $_POST, [ "name", "type", ] ); ``` #### 2.3.9 创建与更新结果判断 - `save()`方法可以拆分成`create()`或`update()` > create() will try to INSERT your data, while save() will check if it already exists (by primary key), and will INSERT it if not and UPDATE it if it does. The third relevant method is update(), respectively [参考](https://forum.phalconphp.com/discussion/14859/what-the-difference-between-save-and-create-while-saving-data-in) #### 2.3.10 删除记录 ##### 1. 删除一条记录 `Phalcon\Mvc\Model::delete()` :删除一条记录 ``` use Store\Toys\Robots; $robot = Robots::findFirst(11); if ($robot !== false) { if ($robot->delete() === false) { echo "Sorry, we can't delete the robot right now: \n"; $messages = $robot->getMessages(); foreach ($messages as $message) { echo $message, "\n"; } } else { echo "The robot was deleted successfully!"; } } ``` ##### 2. 删除多条记录 使用循环 ``` use Store\Toys\Robots; $robots = Robots::find( "type = 'mechanical'" ); foreach ($robots as $robot) { if ($robot->delete() === false) { echo "Sorry, we can't delete the robot right now: \n"; $messages = $robot->getMessages(); foreach ($messages as $message) { echo $message, "\n"; } } else { echo "The robot was deleted successfully!"; } } ``` ##### 3. 删除时可执行方法 Operation Name Can stop operation? Explanation Deleting beforeDelete YES Runs before the delete operation is made Deleting afterDelete NO Runs after the delete operation was made ``` public function beforeDelete() { if ($this->status === "A") { echo "The robot is active, it can't be deleted"; return false; } return true; } ```