technicalguru / database
一个用于访问数据库的 PHP 库
Requires
- php: >=7.0.0
- technicalguru/utils: ~1
Requires (Dev)
- phpunit/phpunit: ^9
README
一个用于轻松访问数据库的 PHP 库。此库提供了一个类似 MySQL/MariaDB 风格的数据库对象,它抽象了许多日常 SQL 编写任务,例如引号、转义、构建 SQL 语句、WHERE 子句、错误处理等。
还提供了一个 数据访问对象 (DAO) 基类,用于处理对象关系映射任务。该接口将使通过 ID 查找对象、创建和更新它们,以及使用您自己的特殊数据对象变得更加容易。
最后,提供了一个 查询 API,以支持灵活地编写限制条件,独立于任何 SQL 方言。
版本 1.3 是最后一个主要的 1.x 版本,标志着迁移版本。它同时支持传统 SQL 编写和查询 API。然而,许多传统的 SQL 方法已被标记为已弃用,它们将在 2.0 版本中删除。因此,您应尽早迁移到新的查询 API。
许可证
此项目根据 GNU LGPL 3.0 许可。
安装
通过 Composer
composer require technicalguru/database
通过包下载
您可以从 GitHub 发布页面 下载源代码包
如何使用简单的数据库层
创建数据库对象
创建一个配置数组并将其传递给构造函数
$config = array(
'host' => 'database-hostname',
'port' => 3306,
'dbname' => 'my-db-name',
'tablePrefix' => 'test_',
'user' => 'user',
'pass' => 'password',
);
$db = \TgDatabase\Database($config);
请注意,我们提供了一个表前缀。通常的做法是将所有表名都加上前缀,例如 test_
。这样,您可以在数据库中保持多个“命名空间”。前缀将在您使用 #__
在表名中的时候添加到您的查询语句中(请参见下面的示例)。
您可以使用 TgUtils\Auth\CredentialsProvider
替代在配置中保留凭据,它将保留这些数据。然后,Database
将忽略配置数组中的凭据。
$db = \TgDatabase\Database($config, $credentialsProvider);
查询对象
// Query a list of objects
$arr = $db->queryList('SELECT * FROM #__devtest');
// Querying a single object
$obj = $db->querySingle('SELECT * FROM #__devtest WHERE uid='.$uid);
默认情况下,该接口提供 stdClass
对象。但是,您可以命名自己的数据类,这样数据就会被填充到这样的类中。
$arr = $db->queryList('SELECT * FROM #__devtest', 'MyNamespace\\MyDataClass');
$obj = $db->querySingle('SELECT * FROM #__devtest WHERE uid='.$uid, 'MyNamespace\\MyDataClass');
插入、更新和删除对象
您可以插入自己的数据类或简单地使用 stdClass
对象或数组。
// Use a standard class object
$obj = new stdClass;
$obj->name = 'test-name';
$obj->email = 'test-email';
$uid = $db->insert('#__devtest', $obj);
// Use your own data class
$obj = new MyNamespace\MyDataClass($initialData);
$uid = $db->insert('#__devtest', $obj);
// Use an array
$arr = array(
'name' => 'test-name',
'email' => 'test-email'
);
$uid = $db->insert('#__devtest', $arr);
Database
将自动转义并引用出现在您新对象中的字符串值。
更新您的行相应地很简单。您需要表名、新值(作为对象或数组)和 WHERE 条件。
// Save all object values
$obj->name = 'Some other name';
$db->update('#__devtest', $obj, 'uid='.$obj->uid);
// Save values from array only
$arr = array('name' => 'Another name');
$db->update('#__devtest', $arr, 'uid='.$uid);
如果您只想更改单个对象,也可以使用 updateSingle()
,它可以返回更改后的对象(作为 stdClass
)。
// Update a single row
$updated = $db->updateSingle('#__devtest', array('name' => 'test-value2'), 'uid='.$uid);
最后,您还可以删除对象。您需要表名和 WHERE 条件。
// Delete a single row
$db->delete('#__devtest', 'uid='.$uid);
备注: update()
、updateSingle()
和 delete()
现在支持新的 查询 API 用于 WHERE 条件。
如何使用数据库访问对象 (DAO)
低级的 Database
抽象使对象关系映射变得简单。然而,编写样板代码仍然很多,例如表名、WHERE 子句等。一种更好的方法是使用 DAO
对象。它极大地简化了数据库的使用。
创建 DAO
通过传递数据库实例和表名创建一个DAO
$dao = \TgDatabase\DAO($db, '#__users');
如上所示的默认构造函数对您的表有假设
- 它总是返回
stdClass
对象。 - 它假设您的表有一个名为
uid
的int auto-increment
主键。
然而,您可以告诉DAO
您的具体信息
// Uses a specific class for the data
$dao = \TgDatabase\DAO($db, '#__users', 'MyNamespace\\User`);
// Uses a specific class and another primary key attribute
$dao = \TgDatabase\DAO($db, '#__users', 'MyNamespace\\User`, 'id');
DAO
实际上可以处理非数字主键。不过,由于您需要自己创建主键,因此不建议这样做。
查找对象
现在查找对象将更加容易
// Get user with specific ID
$user = $dao->get(3);
// Find a singe user with a specific email address
$user = $dao->findSingle(array('email' => $email));
// Find all active admin users, ordered by name and email in ascending order
$users = $dao->find(array('group' => 'admin', 'active' => 1), array('name', 'email));
注意:从v1.3版本开始,这种描述限制的方式已被弃用。find()
和findSingle()
现在支持新的查询API。请阅读查询API章节。
创建、保存和删除对象
// Create a new user
$newUser = new stdClass;
$newUser->name = 'John Doe';
$newUser->email = 'john.doe@example.com';
$newUser->password = '123456';
$newUser->group = 'webusers';
$newUser->active = 1;
$newId = $dao->create($newUser);
// Update an existing user
$user = $dao->get($newId);
$user->name = 'Jane Doe';
$dao->save($user);
// Delete a user
$dao->delete($user);
// or
$dao->delete($user->uid);
DAO接口中的WHERE子句
WHERE子句的最简单形式就是条件本身
$users = $dao->find('group=\'admin\'');
但是,您需要自己进行引号和转义。这就是为什么您可以有一个所有条件组成的数组,这些条件通过AND
连接
$users = $dao->find(array('group' => 'admin', 'active' => 1));
或者,当等号(=
)操作不是您需要的时候
$users $dao->find(array(
array('group', 'admin', '!='),
array('active' , 1)
));
默认操作符是等号(=
),但您也可以使用!=
、<=
、>=
、<
、>
、IN
和NOT IN
。后两者需要在数组的第二位置提供值数组
$users = $dao->find(array(
array('group', array('admin'), 'NOT IN'),
array('active' , 1)
));
注意:从v1.3版本开始,这种描述限制的方式已被弃用。find()
和findSingle()
现在支持新的查询API。请阅读查询API章节。
DAO接口中的ORDER子句
在任何可以提供ORDER子句的地方,有两种类型
// As string
$users = $dao->find('', 'name');
$users = $dao->find('', 'name DESC');
$users = $dao->find('', 'name DESC, email ASC');
// As array
$users = $dao->find('', array('name'));
$users = $dao->find('', array('name DESC'));
$users = $dao->find('', array('name DESC', 'email ASC'));
如果没有指定,默认的排序顺序是升序(ASC
)。
注意:从v1.3版本开始,这种描述排序的方式已被弃用。find()
和findSingle()
现在支持新的查询API。请阅读查询API章节。
扩展DAO
不直接使用DAO
类,而是在项目中从它派生,这是一个好的做法。这样,您可以进一步抽象数据访问,例如。
class Users extends DAO {
public function __construct($database) {
parent::__construct($database, '#__users', 'MyNamespace\\User');
}
public function findByEmail($email) {
return $this->findSingle(array('email' => $email));
}
public function findByDepartment($department, $order = NULL) {
return $this->find(array('department' => $department), $order);
}
}
注意:从v1.3版本开始,这种描述限制的方式已被弃用。find()
和findSingle()
现在支持新的查询API。请阅读查询API章节。
使用DAO与数据对象
如上所述,您可以使用自己的数据类。实际上没有任何限制,除了这个类需要一个无参数的构造函数。主要优点是这个类可以有额外的具有逻辑的方法。您甚至可以定义额外的属性,这些属性不会被DAO保存到数据库中。这些属性以下划线开头。
这里有一个例子
class User {
// will not be saved
private $_derivedAttribute;
public function __construct() {
// You can initialize here
}
public function getDerivedAttribute() {
// Have your logic for the attribute here
// or do something completely different
// Return something
return $this->_derivedAttribute;
}
}
使用DataModel
最后,我们将所有这些整合在一起。我们最后需要的是一个所有DAO
的中心位置。这就是DataModel
。
// Setup the model
$model = new \TgDatabase\DataModel($database);
$model->register('users', $userDAO);
$model->register('products', $productDAO);
// And use it:
$products = $model->get('products')->find();
当然,一个更好的想法是将它封装在自己的DataModel
子类中
class MyDataModel extends \TgDatabase\DataModel {
public function __construct($database) {
parent::__construct($database);
}
protected function init($database) {
// Optional step: call the parent method (it's empty, but could change)
parent::init($database);
// No create your DAOs
$this->register('users', new UserDAO($database));
$this->register('products', new ProductDAO($database));
}
}
您只需要实现init()
方法。现在,您的最终应用程序代码看起来更干净,更容易阅读
// Setup...
$database = new Database($config);
$myModel = new MyDataModel($database);
// ...and use
$users = $myModel->get('users')->find();
想象一下,您需要自己编写多少容易出错的代码!
使用DaoFactory
DataModel
可以利用DaoFactory
。这样的工厂会在应用程序请求时创建DAO。只需创建这样一个工厂的实例
class MyFactory implements DaoFactory {
// code omitted for ease of understanding...
public function createDao($name) {
switch ($name) {
case 'users': return new UserDAO($this->database);
case 'products': return new ProductDAO($this->database);
}
return NULL;
}
}
...并且使用它...
$model = new \TgDatabase\DataModel($database, $myDaoFactory);
现在,您不需要重写DataModel
的init()
方法。
当您有很多DAO要管理,并且应用程序通常只使用其中的一小部分时,推荐使用DaoFactory
。它还将您的数据模型与DAO解耦。
查询API
版本1.3引入了Query
,它在使用搜索、更新或删除对象时提供了更多表达SQL条件的自由度。它是使用Hibernate ORM Criteria模板设计的。因此,大部分代码可能对您来说都很熟悉。
查询API是在数据模型和DAO API的基础上创建的,并增强了它。因此,您仍然可以使用v1.0方式搜索对象,同时开始使用查询API。但是,计划移除旧的API方式描述限制和排序。注意日志中的弃用警告信息。
注意: 如果您已经在使用 v1.2 的 Criteria
类,请放心,它在 1.x 版本中仍然保留以保持兼容性(Criteria
现在继承自 Query
)。从 v2.0 版本开始,此接口将被移除。
创建查询
有两种方法:从 Database
对象创建 Query
对象,或者从 DAO
对象创建。
// From Database object
$query = $database->createQuery('#__users');
// From DAO object
$query = $userDAO->createQuery();
在创建 Query
时,您可以定义模型类(SELECT
查询返回的对象)和别名。
// From Database object
$query = $database->createQuery('#__users', 'MyNamespace\\User', 'a');
// From DAO object
$query = $userDAO->createQuery('a');
由于 DAO 已经知道模型类,因此从 DAO
创建时无需提及。
别名分配给底层表名,并在需要时自动添加到限制条件中。
使用限制条件
限制条件是可用于 WHERE
子句(以及 JOIN
- 见下文)的表达式。辅助类 Restrictions
用于创建它们。
$expr1 = Restrictions::eq('name', 'myUsername');
$expr2 = Restrictions::isNotNull('email');
$query->where($expr1, $expr);
提供了最常用的限制条件:eq、ne、lt、gt、ge、le、like、isNull、isNotNull、between。您也可以在两个属性之间使用限制条件。
// Check for equalitity of name and email of a user.
$expr = Restrictions::eqProperty('name', 'email');
并且可以结合使用 and()
和 or()
来组合限制条件。
$expr = Restrictions::or($expr1, $expr2, $expr3);
排序结果
Order
类包含三个静态方法,可生成相应的子句。
// Ascending order
$order1 = \TgDatabase\Order::asc('columnName1');
// Descending order
$order2 = \TgDatabase\Order::desc('columnName2');
// Use plain SQL as given in argument
$order3 = \TgDatabase\Order::sql('ANY_SQL_FUNCTION() ASC');
asc()
和 desc()
将自动尊重别名并引用列名,而 sql()
则简单地使用给定的字符串。
但是,如果需要,您可以使用另一个别名。
$order2 = \TgDatabase\Order::desc(array('b', 'columnFromJoinedTable'));
最后将这些对象添加到您的查询中。
$query->orderBy($order1, $order2);
$query->orderBy($order3);
修改列列表:列和投影
默认情况下,查询将返回查询表的全部列。但是,您可以修改列列表。
// Select myColumn only
$query->select(Projections::property('myColumn'));
// Add another column
$query->select(Projections::property('anotherColumn'));
请注意,第一次调用 select
将从查询中删除 *
检索。任何后续调用都将增强列表。您可以使用以下方法达到相同的效果:
$query->setSelect(Projections::property('myColumn'), Projections::property('anotherColumn'));
还有一些快捷方式。
// Variant 1: flexible argument list
$query1->select(Projections::property('myColumn'), Projections::property('anotherColumn'));
// Variant 2: use #properties() method in Projections
$query1->select(Projections::properties('myColumn', 'anotherColumn'));
注意: 对 setSelect()
或 setProjection()
(已弃用的替代方案)的调用不会像以前那样从查询对象中删除结果类定义。这打破了与先前版本的兼容性。因此,您需要调用 setResultClass(NULL)
以返回 stdClass
。
获取结果
这是最简单的一部分。
$query->list();
您可以为结果设置限制条件。
$query->setFirstResult(10);
$query->setMaxResults(20);
或者您只期望得到一行数据。
$query->first();
使用投影
基本投影 - 不同行的列的聚合 - 是可用的。
$proj = Projections::rowCount();
$query->select($proj);
您将找到以下投影:count、distinct、sum、avg、min、max。请注意,返回的模型类不会被重置。在使用投影时,您需要调用 setResultClass(NULL)
以返回 stdClass
。
子查询和 JOIN
这是使用查询 API 的最大进步之一。传统的 API 方法无法在搜索依赖于其他表的对象时使用子查询。
让我们假设您想找到所有作者名字以 A 开头的数据库中的书籍。主查询来自书籍,因为我们希望返回的模型类。
$query = $bookDAO->createQuery('a');
接下来,我们连接作者表,并使用相应的连接限制将其添加到主查询中。
$authors = $authorDAO->createQuery('b');
$restriction = Restrictions::eq(array('a','author'), array('b','uid'));
$query->join($authors, $restriction);
最后,我们应用作者搜索条件。
$authors->where(Restrictions::like('name', 'A%'));
另一种添加子查询的方法是直接通过主 Query
对象。
$authors = $booksDAO->createQuery('a');
$authors->createJoin('#__authors', 'b', Restrictions::eq(array('a','author'), array('b','uid')));
$authors->where(Restrictions::like('name', 'A%'));
更新和删除多个对象
Query
对象还可以更新和删除对象。
// Update
$restrictions = Restrictions::eq('name', 'John Doe');
$updates = array('comment' => 'This is an unknown author');
$dao->createQuery()->where($restrictions)->update($updates);
// Delete
$restrictions = Restrictions::eq('name', 'Jane Doe');
$dao->createQuery()->where($restrictions)->delete();
GROUP BY 和 HAVING 子句
查询 API 允许定义分组结果集,并使用 HAVING 子句限制返回的结果。
// List the number of books that authors published whose names begin with 'John'
$bookQuery
->select(Projections::property('author'), Projections::rowCount('cnt'))
->groupBy(Projections::property('author'))
->having(Restrictions::like('author', 'John%'))
->setResultClass(NULL)
->list();
请注意,对这样的查询使用 ->count()
可能会产生意外的结果。这是一个尚未解决的问题。
有用的方法
您可能想利用一些可以简化代码编写的方法。
// Query the authors with specific name, starting with 10th row and a max of 20 authors:
$query = $dao->createQuery('a', Restrictions::eq('name', 'John Doe'), Order::asc('uid'), 10, 20);
// Count all records (before limiting the result):
$count = $query->count(); // returns e.g. 65
$result = $query->list(); // returns 20 objects
// Count using the DAO
$count = $dao->count(Restrictions::eq('name', 'John Doe'));
优点和局限性
查询API进一步简化了在数据库中搜索对象并返回模型类的操作,通过更复杂的表达式和限制来实现。您可以根据前端用户的需求和您的应用程序动态地应用限制。一旦创建了Query
对象,就不再需要DAO。它是自包含的。
然而,也存在一些限制
- 查询API目前支持基本用例(带有基本限制的对象搜索、更新、删除)。
- 仅生成MySQL / MariaDB SQL方言(但只要遵循API,可以轻松扩展到其他方言)。
- 一些限制可以通过使用
SqlExpression
和SqlProjection
类来克服
// Use a specific restriction not supported
$myExpr = Restrictions::sql('my-sql-fragment');
// Use a specific projection not supported
$myProj = Projections::sql('RANDOM()');
如果您需要一些尚未支持的扩展,请随时提出问题(见下文)。
贡献
在GitHub问题跟踪器中报告错误、请求增强或发起pull request。