sqlican 2019-06-27
Phalcon\Db
是Phalcon\Mvc\Model
底层组件,由它驱动框架中的模型层。它完全由C语言编写,是一个独立的数据库高级抽象层。
与传统模型相比,该组件允许更底层的数据库操作。
该组件使用适配器来封装特定的数据库操作。Phalcon使用PDO连接数据库,支持下列数据库引擎:
类 | 说明 |
---|---|
Phalcon\Db\Adapter\Pdo\Mysql | 世界上最流行的关系型数据库系统(RDBMS),作为服务器运行,支持多用户、多数据库访问 |
Phalcon\Db\Adapter\Pdo\Postgresql | Postgresql是一个强大的开源关系数据库系统,超过15年的发展和通过验证的架构,为其赢得了正确、可靠、数据完整的良好声誉 |
Phalcon\Db\Adapter\Pdo\Sqlite | SQLite是一个实现自包含、无服务、零配置的事务型数据库 |
使用适配器选项加载PDO:
<?php use Phalcon\Db\Adapter\Pdo\Factory; $options = [ 'host' => 'localhost', 'dbname' => 'blog', 'port' => 3306, 'username' => 'sigma', 'password' => 'secret', 'adapter' => 'mysql', ]; $db = Factory::load($options);
创建自定义数据库适配器或扩展现有适配器,必须实现Phalcon\Db\AdapterInterface
接口。
phalcon语言封装了每个数据库的具体操作,为适配器提供通用方法和SQL生成器。
类 | 说明 |
---|---|
Phalcon\Db\Dialect\Mysql | MySQL特定语言 |
Phalcon\Db\Dialect\Postgresql | Postgresql特定语言 |
Phalcon\Db\Dialect\Sqlite | SQLite特定语言 |
创建自定义数据库语言或扩展现有语言,必须实现Phalcon\Db\DialectInterface
接口。
建立数据库连接,必须实例化适配器类,它只接收一个包含连接参数的数组。下面例子展示了如何传递必选参数和可选参数来建立数据库连接:
<?php // 必选参数 $config = [ 'host' => '127.0.0.1', 'username' => 'mike', 'password' => 'sigma', 'dbname' => 'test_db', ]; // 可选参数 $config['persistent'] = false; // 建立连接 $connection = new \Phalcon\Db\Adapter\Pdo\Mysql($config);
<?php // 必选参数 $config = [ 'host' => 'localhost', 'username' => 'postgres', 'password' => 'secret1', 'dbname' => 'template', ]; // 可选参数 $config['schema'] = 'public'; // 建立连接 $connection = new \Phalcon\Db\Adapter\Pdo\Postgresql($config);
在建立连接时,传递options
参数设置PDO:
<?php // 使用PDO选项建立连接 $connection = new \Phalcon\Db\Adapter\Pdo\Mysql( [ 'host' => 'localhost', 'username' => 'root', 'password' => 'sigma', 'dbname' => 'test_db', 'options' => [ PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES UTF8', PDO::ATTR_CASE => PDO::CASE_LOWER, ], ] );
使用一个简单的ini文件来配置 / 连接数据库。
[database] host = TEST_DB_MYSQL_HOST username = TEST_DB_MYSQL_USER password = TEST_DB_MYSQL_PASSWD dbname = TEST_DB_MYSQL_NAME port = TEST_DB_MYSQL_PORT charset = TEST_DB_MYSQL_CHARSET adapter = mysql
<?php use Phalcon\Config\Adapter\Ini; use Phalcon\Db\Adapter\Pdo\Factory; use Phalcon\Di; $di = new Di(); $config = new Ini('config.ini'); $di->set('config', $config); $di->set( 'db', function () { return Factory::load($this->config->database); } );
上述代码返回数据库连接实例,这样做的好处是可以在不修改应用代码的情况下改变数据库连接甚至是数据库适配器。
Phalcon\Db
提供了多种查询方法。这种情况下,SQL必须遵循数据库引擎的特定语法:
<?php $sql = "SELECT id, name FROM robots ORDER BY name"; // 发送SQL语句到数据库 $result = $connection->query($sql); // 打印robot的name字段 while ($robot = $result->fetch()) { echo $robot['name']; } // 获取结果集数组 $robots = $connection->fetchAll($sql); foreach ($robots as $robot) { echo $robot['name']; } // 获取结果集中的第一条记录 $robot = $connection->fetchOne($sql);
默认情况下,调用这些方法会返回一个数组(关联+索引)。可以调用Phalcon\Db\Result::setFetchMode()
方法改变这种行为,该方法接收一个常量值,定义返回结果集的类型:
常量 | 说明 |
---|---|
Phalcon\Db::FETCH_NUM | 返回索引数组 |
Phalcon\Db::FETCH_ASSOC | 返回关联数组 |
Phalcon\Db::FETCH_BOTH | 返回数组(索引+关联) |
Phalcon\Db::FETCH_OBJ | 返回对象 |
<?php $sql = "SELECT id, name FROM robots ORDER BY name"; $result = $connection->query($sql); $result->setFetchMode(Phalcon\Db::FETCH_NUM); while ($robot = $result->fetch()) { echo $robot[0]; }
Phalcon\Db::query()
方法返回一个Phalcon\Db\Result\Pdo
实例。该对象封装了与返回结果集相关的所有功能,如遍历、查找特定行、统计总行数等。
<?php $sql = "SELECT id, name FROM robots"; $result = $connection->query($sql); // 遍历结果集 while ($robot = $result->fetch()) { echo $robot['name']; } // 查找第三行 $result->seek(2); $robot = $result->fetch(); // 计算总行数 echo $result->numRows();
Phalcon\Db
支持参数绑定。使用参数绑定会影响性能,但可以防止SQL注入。
支持字符串和数字占位符,参数绑定可以简单的实现如下:
<?php // 数字占位符 $sql = "SELECT * FROM robots WHERE name = ? ORDER BY name"; $result = $connection->query( $sql, [ 'Wall-E', ] ); // 字符串占位符 $sql = "INSERT INTO `robots`(name, year) VALUES(:name, :year)"; $success = $connection->query( $sql, [ 'name' => 'Astro Boy', 'year' => 1952, ] );
使用数字占位符时,需要将它们定义为数字值(如1或2),'1'或'2'会被视为字符串而非数字,导致占位符不能被成功替换。使用任何数据库适配器,数据都会被Pdo::Quote()
自动转义。该方法会考虑到连接字符集,因此建议在连接选项或服务器配置中定义正确的字符集,错误的字符集会在存储或检索数据时产生不良影响。
此外,可以将参数直接传递给execute() / query()方法,这种情况下的绑定参数会直接传递给PDO:
<?php // PDO占位符 $sql = "SELECT * FROM robots WHERE name = ? ORDER BY name"; $result = $connection->query( $sql, [ 1 => 'Wall-E', ] );
占位符允许执行参数绑定以避免SQL注入:
<?php $phql = "SELECT * FROM Store\Robots WHERE id > :id:"; $robots = $this->modelsManager->executeQuery( $phql, [ 'id' => 100, ] );
某些数据库系统在使用占位符时需要执行额外操作,如指定绑定参数的类型:
<?php use Phalcon\Db\Column; // ... $phql = "SELECT * FROM Store\Robots LIMIT :number:"; $robots = $this->modelsManager->executeQuery( $phql, [ 'number' => 10, ], Column::BIND_PARAM_INT );
可以在参数中使用类型化的占位符,而不用在executeQuery()
方法中指定:
<?php $phql = "SELECT * FROM Store\Robots LIMIT {number:int}"; $robots = $this->modelsManager->executeQuery( $phql, [ 'number' => 10, ] ); $phql = "SELECT * FROM Store\Robots WHERE name <> {name:str}"; $robots = $this->modelsManager->executeQuery( $phql, [ 'name' => $name, ] );
如果不需要指定绑定参数类型,可以省略:
<?php $phql = "SELECT * FROM Store\Robots WHERE name <> {name}"; $robots = $this->modelsManager->executeQuery( $phql, [ 'name' => $name, ] );
类型化的占位符很强大,我们可以绑定静态数组,而无需将每个参数作为占位符单独传递:
<?php $phql = "SELECT * FROM Store\Robots WHERE id IN ({ids:array})"; $robots = $this->modelsManager->executeQuery( $phql, [ 'ids' => [1, 2, 3, 4], ] );
支持下列类型:
绑定类型 | 绑定类型常量 | 示例 |
---|---|---|
str | Column::BIND_PARAM_STR | {name:str} |
int | Column::BIND_PARAM_INT | {number:int} |
double | Column::BIND_PARAM_DECIMAL | {price:double} |
bool | Column::BIND_PARAM_BOOL | {enabled:bool} |
blob | Column::BIND_PARAM_BLOB | {image:blob} |
null | Column::BIND_PARAM_NULL | {exists:null} |
array | Column::BIND_PARAM_STR数组 | {codes:array} |
array-str | Column::BIND_PARAM_STR数组 | {names:array} |
array-int | Column::BIND_PARAM_INT数组 | {flags:array} |
默认情况下,绑定参数不会在PHP中转换为指定类型,
如,在LIMIT / OFFSET
中给占位符传递一个字符串值就会导致错误:
<?php $number = '100'; $robots = $modelsManager->executeQuery( "SELECT * FROM Some\Robots LIMIT {number:int}", [ 'number' => $number, ] );
这会导致异常:
Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''100'' at line 1' in /Users/scott/demo.php:78
错误原因是'100'是一个字符串。可以先将值转换为整型:
<?php $number = '100'; $robots = $modelsManager->executeQuery( "SELECT * FROM Some\Robots LIMIT {number:int}", [ 'number' => (int) $number, ] );
要解决这个问题,需要开发者格外注意绑定参数类型及其如何传递。为了简化操作并避免异常,可以指定Phalcon
自动转换:
<?php \Phalcon\Db::setup(['forceCasting' => true]);
以下操作根据指定的绑定类型执行:
绑定类型 | 操作 |
---|---|
Column::BIND_PARAM_STR | 将值转换为PHP字符串 |
Column::BIND_PARAM_INT | 将值转换为PHP整型 |
Column::BIND_PARAM_BOOL | 将值转换为PHP布尔值 |
Column::BIND_PARAM_DECIMAL | 将值转换为PHP浮点数 |
从数据库返回的值在PDO中始终表示为字符串,无论该列值是数字还是布尔值。这种情况是因为某些列类型由于其大小限制而无法用PHP原来类型表示。例如,MySQL中的BIGINT
可以存储无法用PHP 32位整型表示的大整数。所以,PDO和ORM默认将所有值作为字符串。可以设置ORM自动将这些值转换为PHP实际类型:
<?php \Phalcon\Mvc\Model::setup(['castOnHydrate' => true]);
通过这种方式,可以使用严格运算符或对变量类型进行假设:
<?php $robot = Robots::findFirst(); if (11 === $robot->id) { echo $robot->name; }
可以使用原生SQL或类方法来插入、更新、删除记录:
<?php // 用原生SQL插入数据 $sql = "INSERT INTO `robots`(`name`, `year`) VALUES('Astro Boy', 1952)"; $success = $connection->execute($sql); // 使用占位符 $sql = "INSERT INTO `robots`(`name`, `year`) VALUES(?, ?)"; $success = $connection->execute( $sql, [ 'Astro Boy', 1952, ] ); // 动态生成SQL语句 $success = $connection->insert( 'robots', [ 'Astro Boy', 1952, ], [ 'name', 'year', ] ); // 动态生成SQL语句(另一种语法) $success = $connection->insertAsDict( 'robots', [ 'name' => 'Astro Boy', 'year' => 1952, ] ); // 使用原生SQL更新数据 $sql = "UPDATE `robots` SET `name` = 'Astro Boy' WHERE `id` = 101"; $success = $connection->execute($sql); // 使用占位符 $sql = "UPDATE `robots` SET `name` = ? WHERE `id` = ?"; $success = $connection->execute( $sql, [ 'Astro Boy', 101, ] ); // 动态生成SQL语句 $success = $connection->update( 'robots', [ 'name', ], [ 'New Astro Boy', ], 'id = 101' // 注意,这种情况下值不会被自动转义 ); // 条件中数据的转义 $success = $connection->update( 'robots', [ 'name', ], [ 'New Astro Boy', ], [ 'conditions' => 'id = ?', 'bind' => [101], 'bindTypes' => [PDO::PARAM_INT], // 可选参数 ] ); $success = $connection->updateAsDict( 'robots', [ 'name' => 'New Astro Boy', ], [ 'conditions' => 'id = ?', 'bind' => [101], 'bindTypes' => [PDO::PARAM_INT], // 可选参数 ] ); // 使用原生SQL删除记录 $sql = "DELETE `robots` WHERE `id` = 101"; $success = $connection->execute($sql); // 使用占位符 $sql = "DELETE `robots` WHERE `id` = ?"; $success = $connection->execute($sql, [101]); // 动态生成SQL语句 $success = $connection->delete( 'robots', 'id = ?', [ 101, ] );
PDO支持事务处理,在事务内部执行数据库操作通常可以提高数据库的性能:
<?php try { // 开始事务 $connection->begin(); // 执行SQL语句 $connection->execute("DELETE `robots` WHERE `id` = 101"); $connection->execute("DELETE `robots` WHERE `id` = 102"); $connection->execute("DELETE `robots` WHERE `id` = 103"); // 如果一切顺利,提交事务 $connection->commit(); } catch (Exception $e) { // 发生异常,回滚事务 $connection->rollback(); }
除了标准事务,Phalcon\Db
内置了嵌套事务(如果数据库支持)。当再次调用begin()
方法时,会创建一个嵌套事务:
<?php try { // 开始事务 $connection->begin(); // 执行SQL语句 $connection->execute("DELETE `robots` WHERE `id` = 101"); try { // 开始嵌套事务 $connection->begin(); // 嵌套事务中执行SQL语句 $connection->execute("DELETE `robots` WHERE `id` = 102"); $connection->execute("DELETE `robots` WHERE `id` = 103"); // 创建保存点 $connection->commit(); } catch (Exception $e) { // 发生异常,回滚嵌套事务 $connection->rollback(); } // 继续执行更多SQL语句 $connection->execute("DELETE `robots` WHERE `id` = 104"); // 如果一切顺利,提交事务 $connection->commit(); } catch (Exception $e) { // 发生异常,回滚事务 $connection->rollback(); }
Phalcon\Db
能够将事件发送给EventManager(如果存在),某些事件返回false时,可能会终止操作。支持以下事件:
事件名称 | 触发时机 | 是否会终止操作 |
---|---|---|
afterConnect | 成功连接到数据库后 | 否 |
beforeQuery | 执行SQL语句前 | 是 |
afterQuery | 执行SQL语句后 | 否 |
beforeDisconnect | 关闭临时数据库连接前 | 否 |
beginTransaction | 事务开启前 | 否 |
rollbackTransaction | 事务回滚前 | 否 |
commitTransaction | 事务提交前 | 否 |
将EventsManager绑定到数据库连接很简单,Phalcon\Db
将触发db
类型事件:
<?php use Phalcon\Db\Adapter\Pdo\Mysql as Connection; use Phalcon\Events\Manager as EventsManager; $eventsManager = new EventsManager(); // 监听所有数据库事件 $eventsManager->attch('db', $dbListener); $connection = new Connection( [ 'host' => 'localhost', 'username' => 'root', 'password' => 'secret', 'dbname' => 'invo', ] ); // 将eventsManager分配给数据库适配器实例 $connection->setEventsManager($eventsManager);
数据库事件中,终止SQL操作非常有用。例如,想在SQL执行前实现注入检查:
<?php use Phalcon\Events\Event; $eventsManager->attch( 'db:beforeQuery', function (Event $event, $connection) { $sql = $connection->getSQLStatement(); // 检查SQL中是否有恶意关键字 if (preg_match('/DROP|ALTER/i', $sql)) { // 不允许DROP / ALTERT操作 return false; } return true; } )
Phalcon\Db
内置了性能分析组件Phalcon\Db\Profiler
,用于分析数据库性能,以便诊断问题,发现瓶颈。使用Phalcon\Db\Profiler
进行数据库分析相当容易:
<?php use Phalcon\Db\Profiler as DbProfiler; use Phalcon\Events\Event; use Phalcon\Events\Manager as EventsManager; $eventsManager = new EventsManager(); $profiler = new DbProfiler(); // 监听所有数据库事件 $eventsManager->attch( 'db', function (Event $event, $connection) use ($profiler) { if ($event->getType() === 'beforeQuery') { $sql = $connection->getSQLStatement(); // 开始分析 $profiler->startProfile($sql); } if ($event->getType() === 'afterQuery') { // 停止分析 $profiler->stopProfile(); } } ); // 将事件管理器分配给数据库连接 $connection->setEventsManager($eventsManager); $sql = "SELECT buyer_name, quantity, product_name FROM buyers LEFT JOIN products ON buyers.pid = products.id"; // 执行SQL语句 $connection->query($sql); // 获取最后一个分析结果 $profile = $profiler->getLastProfile(); echo 'SQL Statement: ', $profile->getSQLStatement(), "\n"; echo 'Start Time: ', $profile->getInitialTime(), "\n"; echo 'Final Time: ', $profile->getFinalTime(), "\n"; echo 'Total Elapsed Time: ', $profile->getTotalElapsedSeconds(), "\n";
还可以基于Phalcon\Db\Profiler
创建自己的分析器,以实时统计发送到数据库的SQL语句:
<?php use Phalcon\Db\Profiler as Profiler; use Phalcon\Db\Profiler\Item as Item; use Phalcon\Events\Manager as EventsManager; class DbProfiler extends Profiler { // SQL语句发送给数据库服务器之前执行 public function beforeStartProfile(Item $profile) { echo $profile->getSQLStatement(); } // SQL语句发送到数据库服务器之后执行 public function afterEndProfile(Item $profile) { echo $profile->getTotalElapsedSeconds(); } } // 创建事件管理器 $eventsManager = new EventsManager(); // 创建事件监听器 $dbProfiler = new DbProfiler(); // 设置监听器监听所有数据库事件 $eventsManager->attch('db', $dbProfiler);
使用诸如Phalcon\Db
这样的高级抽象组件来访问数据库时,很难获知哪些语句被发送到了数据库。Phalcon\Logger
配合Phalcon\Db
使用,可以在数据库抽象层上提供日志记录功能。
<?php use Phalcon\Events\Event; use Phalcon\Events\Manager as EventsManager; use Phalcon\Logger; use Phalcon\Logger\Adapter\File as FileLogger; $eventsManager = new EventsManager(); $logger = new FileLogger('app/logs/db.log'); $eventsManager->attch( 'db:beforeQuery', function (Event $event, $connection) use ($logger) { $sql = $connection->getSQLStatement(); $logger->log($sql, Logger::INFO); } ); // 将eventsManager分配给数据库适配器实例 $connection->setEventsManager($eventsManager); // 执行SQL语句 $connection->insert( 'products', [ 'Hot pepper', 3.50, ], [ 'name', 'price', ] );
如上所述,文件app/logs/db.log
将包含下列内容:
[Sun, 29 Apr 12 22:35:26 -0500][DEBUG][Resource Id #77] INSERT INTO products (name, price) VALUES ('Hot pepper', 3.50)
可以自定义记录器以记录数据库操作,通过创建一个实现了log()
方法的类,该方法接受一个字符串作为第一个参数。可以将记录器对象传递给Phalcon\Db::setLogger()
,这样在执行任何SQL语句时将调用log()
方法进行记录。
Phalcon\Db
提供了获取表、视图详情的方法:
<?php // 获取test_db库中的数据表 $tables = $connection->listTables('test_db'); // 表'robots'是否存在于当前库中 $exists = $connection->tableExists('robots'); // 获取'robots'表字段名称、类型、特性 $fields = $connection->describeColumns('robots'); foreach ($fields as $field) { echo 'Column Type: ', $field['Type']; } // 获取'robots'表索引 $indexes = $connection->describeIndexes('robots'); foreach ($indexes as $index) { print_r( $index->getColumns() ); } // 获取'robots'表外键 $references = $connection->describeReferences('robots'); foreach ($references as $reference) { // 打印引用列 print_r( $reference->getReferenceColumns() ); }
表详情和MySQL的describe命令返回的信息相似,包含如下信息:
Field | Type | Key | Null |
---|---|---|---|
字段名称 | 字段类型 | 是否主键或索引列 | 是否允许为空 |
对于被支持的数据库系统,同样实现了获取视图详情的方法:
<?php // 获取test_db库中的视图 $tables = $connection->listViews('test_db'); // 视图'robots'是否存在于当前库中 $exists = $connection->viewExists('robots');
不同的数据库系统(MySQL,Postgresql等)通过CREATE、ALTER、DROP命令提供了用于创建、修改、删除数据表的功能。SQL语法因数据库而异。Phalcon\Db
为编辑表提供了统一接口,无需区分不同数据库系统的SQL语法。
下面例子展示如何创建表:
<?php use Phalcon\Db\Column as Column; $connection->createTable( 'robots', null, [ 'columns' => [ new Column( 'id', [ 'type' => Column::TYPE_INTEGER, 'size' => 10, 'notNull' => true, 'autoIncrement' => true, 'primary' => true, ] ), new Column( 'name', [ 'type' => Column::TYPE_VARCHAR, 'size' => 70, 'notNull' => true, ] ), new Column( 'year', [ 'type' => Column::TYPE_INTEGER, 'size' => 11, 'notNull' => true, ] ), ], ] );
Phalcon\Db::createTable()
接收一个描述数据表的关联数组,用Phalcon\Db\Column
类创建字段,下表列出了定义字段的选项:
选项 | 说明 | 是否可选 |
---|---|---|
type | 字段类型,必须是PhalconDbColumn 常量 | 否 |
primary | 是否主键 | 是 |
size | VARCHAR 或INTEGER 类型的字段定义字段长度 | 是 |
scale | DEMICAL 或NUMBER 类型字段定义数据精度 | 是 |
unsigned | INTEGER 类型字段定义是否有符号,该选项不适用于其他类型字段 | 是 |
notNull | 字段是否非空 | 是 |
default | 默认值 | 是 |
autoIncrement | 是否自增 | 是 |
bind | BIND_TYPE_* 常量定义字段在保存前如何绑定数据 | 是 |
first | 把字段设置为表的第一个字段 | 是 |
after | 设置字段放在指定字段之后 | 是 |
Phalcon\Db
支持下列字段类型:
Phalcon\Db\Column::TYPE_INTEGER
Phalcon\Db\Column::TYPE_DATE
Phalcon\Db\Column::TYPE_VARCHAR
Phalcon\Db\Column::TYPE_DECIMAL
Phalcon\Db\Column::TYPE_DATETIME
Phalcon\Db\Column::TYPE_CHAR
Phalcon\Db\Column::TYPE_TEXT
传入Phalcon\Db::createTable()
方法的关联数组可能包含下列索引:
索引 | 说明 | 是否可选 |
---|---|---|
columns | 由Phalcon\Db\Column 定义的字段组成的数组 | 否 |
indexes | 由Phalcon\Db\Index 定义的表索引组成的数组 | 是 |
references | 由Phalcon\Db\Reference 定义的表引用(外键)组成的数组 | 是 |
options | 包含表创建选项的数组,这些选项通常与数据库迁移相关 | 是 |
随着应用程序越来越庞杂,可能需要调整数据库,作为重构或添加新功能的一部分。并非所有数据库系统都允许修改列或者新增列,Phalcon\Db
也受到这些限制:
<?php use Phalcon\Db\Column as Column; // 新增列 $connection->addColumn( 'robots', null, new Column( 'robot_type', [ 'type' => Column::TYPE_VARCHAR, 'size' => 32, 'notNull' => true, 'after' => 'name', ] ) ); // 编辑列 $connection->modifyColumn( 'robots', null, new Column( 'name', [ 'type' => Column::TYPE_VARCHAR, 'size' => 40, 'notNull' => true, ] ) ); // 删除'name'列 $connection->dropColumn( 'robots', null, 'name' );
删除表示例:
<?php // 从当前库中删除'robots'表 $connection->dropTable('robots'); // 从'machines'库中删除'robots'表 $connection->dropTable('robots', 'machines');