silverengine / reflectorm
Requires
- php: >=7.2
This package is not auto-updated.
Last update: 2024-09-19 16:56:17 UTC
README
关于查询
查询是一个类,帮助我们构建 SQL 查询并从数据库获取结果。
使用 Query 类我们有以下优势
- 所有值都经过适当的转义。
- 相同的查询可以在不同的数据库上执行。
- 事务和子事务(即保存点)的使用简单
- …
构建查询
Query::select('column') ->from('table') ->where('column2', '=', 'value') ->orderBy('column3', 'asc') ->limit(2);
这是一个示例查询,它给出以下结果
SELECT `column` FROM `table` WHERE `column2` = ? ORDER BY `column3` ASC LIMIT 2; -- values: ['value']
当我们想要创建查询时,我们可以调用以下函数之一,它为我们提供用于我们语句的专业化 Query
- ::select()
- Select
- ::delete()
- Delete
- ::update()
- Update
- ::insert()
- Insert
- ::drop()
- Drop
- ::create()
- Create
- ::alter()
- Alter
当我们得到查询时,我们可以使用此查询的方法。由于更多的查询使用相同的方法,它们被分开存储并作为特质包含。
查询方法
每个提供的方法都接受称为查询部分的值。查询部分类型在变量名之前(仅在此文档中)书写。
我们可以传递什么给查询部分在查询部分章节中描述。
From
- from($table, $alias = null, Column $id1 = null, Column $id2 = null)
从哪个表选择数据。
$q->from('table'); $q->from('table', 't'); $q->from('table t'); $q->from('table2', 't2', 't1.id', 't2.id_table1'); $q->from('table2 t2', 't1.id', 't2.id_table1'); $q->from('table2', 't1.id', 'table2.id_table1');
我们也可以传递子查询。
$q->from(Query::select()->from('other'), 'alias_must_exists'); // or $q->from(function() { return Query::select()->from('tbl')->whereActive(true); }, 'alias');
Join
- join(Table $table, JoinCondition $condition)
- 内连接表
- leftJoin(Table $table, JoinCondition $condition)
- 左连接表
- rightJoin(Table $table, JoinCondition $condition)
- 右连接表
$q->join('table', ['table.id', 'other_table.id']);
Where 和 Having
- where(Column $column, Raw $operator, Value $value, string $how = ‘and’, boolean $not = false)
- having(Column $column, Raw $operator, Value $value, string $how = ‘and’, boolean $not = false)
$q->where('column', '=', 'value'); $q->where('column', 'between', [10, 20], 'or', true); // or not between 10-20
我们可以跳过操作符并直接写值(但在此情况下,我们无法修改 $how 和 $not 的值)
$q->where('column', 'value'); // column = value
__call() 函数简化了我们的 where 调用。我们可以通过连接更多单词到方法名称来获得我们想要的结果:(顺序很重要!)
- or
- 如果我们想通过 OR 连接此条件(可选)
- not
- 如果我们想否定此条件(可选)
- where / having
- 其中一个
- 列名
- 使用驼峰式命名的列名将被转换为 snake_case(可选)
当我们调用此特殊方法时,我们需要传递以下参数
- 列(如果尚未传递)
- 操作符(必需)
- 值(必需)
$q->whereId(1); // operator is skipped $q->whereId('=', 1); // same thing $q->whereAge('between', [10, 20]); // AND age between 10 and 20 $q->notWhereAge('between', [10, 20]); // AND NOT age between 10 and 20 $q->orNotWhereAge('between', [10, 20]); // OR NOT age between 10 and 20 $q->orWhere('specialColumn', 3); // AND specialColumn = 3
操作符
默认操作符是
- IN
- 如果值是数组
- IS
- 如果值是 null
- =
- 对于所有其他值
特殊操作符
- BETWEEN
- 它需要一个包含两个值的数组作为值
所有其他操作符都被转换为大写。操作符不会被转义或特别处理。它像原始值一样连接到 SQL 语句中。我们可以写入任何我们想要的(包括空格和注释……请不要做任何愚蠢的事情)
where 的特殊使用 - 括号
如果我们想确定操作符的优先级,我们可以使用 where() 函数将条件放在括号中。
// WHERE active = true AND age = 10 OR age = 11 $q->whereActive(true) ->whereAge(10) ->orWhereAge(11); // WHERE active = true AND (age = 10 OR age = 11) $q->whereActive(true) ->where(function ($q) { $q->whereAge(10) ->orWhereAge(11); }); // High order function example function find_best($sex) { return function($q) use ($sex) { /* ... */ } } $q->where(find_best('female'));
在这个示例中,所有的where条件(没有)都被放入了括号中。在内函数中,我们仍然可以对查询(order by、having、limit等)做任何我们想做的事情……
GroupBy
如果我们使用上一章中的having,我们需要首先分组一些数据。
- groupBy(Column $column)
就是这样。如果我们想要对更多列进行分组,我们可以多次调用groupBy()。
Order
- orderBy(Name $column, $dir = ‘asc’)
$q->orderBy('model') ->orderBy('year', 'desc'); // ORDER BY model asc, year desc
Limit
- limit(int $count, $offset = null)
- 标准limit
- offset(int $offset)
- 如果尚未指定limit,则默认设置为1
- page(int $page, int $per_page = null)
- 第一个参数是页码(从1开始),第二个参数是每页大小。
$q->limit(4); // LIMIT 4 $q->limit(4, 6); // LIMIT 4 OFFSET 5 $q->offset(3); // LIMIT 1 OFFSET 3 // ^ Actually limit should be 4, becouse we set it in second row /* Page size = 10 * * Page: * 1. 0-9 * 2. 10-19 * 3. 20-29 */ $q->page(3, 10); // LIMIT 10 OFFSET 20
Union
- union(Query $q)
- unionAll(Query $q)
$q->union(Query::select()->from('table')->limit(1)); // If we are more comfortable with callback, we can use it: $q->union(function() { return Query::select() ->from('table') ->limit(1); });
查询部分
查询部分存储在System/Database/Parts/。我们可以通过$part = OutPart::ensure([‘construct’,‘values’])或通过构造函数$part = new OurPart(‘construct’,‘values’);来构建它们。
如果Part在Query方法参数之前(在本文档中)定义,值将通过Part::ensure($value)函数传递。如果值已经是Part(可能是Raw),ensure将不会触碰它,否则值将构造为声明的Part。
Column::ensure('col'); /* is same as */ Column::ensure(['col']); $q->where('col', '=', 24); /* * So, because where is declared as: where(Column $c, $op, Value $v) * 'col' will be transformed with Column::ensure('col') * and 24 with Value::ensure(24); */ // If we want create Column like: new Column('table', 'col', 'alias'); $q->where(['table', 'col'], ...); // becouse we can construct Column like: new Column('table.col alias'): $q->where('table col'); // If we pass our own part into where, Column::ensure will skip it $q->where(new Raw('COUNT(*)'), 5)
我们也可以将别名传递给where列,但我们不想这么做。
Raw
参数:(string $raw_value)
在raw中,我们传递原始sql。
$q->select(new Raw('COUNT(*) count'))->from('table');
Literal
参数:(mixed $value)
Literal确保传递的值将被正确引用。以下方法已预定义
- Literal::wild()
- new Raw(‘*’)
- Literal::null()
- new Literal(null)
- Literal::true()
- new Literal(true)
- Literal::false()
- new Literal(false)
我们可以将任何值传递给literal,并且它将出现在sql语句中
$q->select(Literal::wild()) ->where('active', Literal::true()) ->notWhereLastLogin(Literal::null()) ->where('string', new Literal('this is string')) ->where('age', 'IN', new Literal([20, 21, 22])); // We must write // IN operator, // becouse // Literal is not // recognized as // array // SELECT * FROM ? WHERE active = true AND NOT WHERE last_login = null AND string = 'this is string' AND age IN [20, 21, 22] // We should specify IS operator for NULL too!
如果我们不使用Literal,Query将值转换为Value部分,该部分使用占位符。
$q->select(Literal::wild()) ->where('active', true) ->notWhereLastLogin(null) ->where('string', 'this is string') ->where('age', [20, 21, 22]); // SELECT * FROM ? WHERE active = ? AND NOT WHERE last_login IS ? AND string = ? AND age IN [?, ?, ?]
Value
参数:(mixed $value)
所有传递到Query的值都转换为Value。Value存储在Query绑定时,并将‘?’占位符插入到SQL语句中。
Fn
参数:(string $name, …$args)
函数调用
- Fn::count(Column $column = null)
- (默认是Literal::wild())
- Fn::groupConcat(Column $column, string $sep = ‘,’)
- GROUP_CONCAT函数
其他函数调用可以通过特殊的__call()方法构建,例如
- Fn::myOwnFunction(1,2,3)
- myOwnFunction(1,2,3);
- Fn::THIS_IS_FN(1,’string’,new Value(24))
- THIS_IS_FN(1,’string’,?);
Column
参数:(string $table_or_column, string $column = null, string $alias = null)
Column是部分,表示带有表(可选)和别名(可选)的列名。
我们可以以更多方式构建它
- new Column(‘table’,‘column’,‘alias’)
- new Column(‘table’,‘column’)
- new Column(null,‘column’,‘alias’)
- new Column(‘table.column alias’)
- new Column(‘table.column’,null,‘alias’)
因为我们传递给查询的是通过Column::ensure()函数传递的,我们可以以以下方式定义列
$q->select('table.column alias', ['table', 'column'], ['t', 'c', 'als'], [null, 'col', 'alias'], 'col alias');
Name
参数:(string $name)
Name类似于Column,但它没有表和别名。它用于列和表名。
JoinCondition
参数:(Name $col1, $operator = null, Name $col2)
连接条件可以是ON (c1 = c2)或USING (c)。
echo new JoinCondition('col1', '=', 'col2'); // ON `col` = `col2` echo new JoinCondition('col1', 'col2'); // same thing echo new JoinCondition('col'); // USING (`col`)
列名会自动转义,因为它们被转换为Name部分。
Table
参数:(Name or Query $table, Name $alias = null)
Table用于指定数据的来源。它在连接方法中使用。
$q->join(new Table(Query::select()->from('tbl'), 'alias')); $q->join(new Table('table', 'alias')); $q->join(new Table('tbl', 'alias')); $q->join(new Table('tbl')); // Becouse join already make Table part, we can skip new Table: $q->join([Query::select()->from('tbl'), 'alias']); $q->join(['table', 'alias']); $q->join(['tbl', 'alias']); $q->join(['tbl']); /* same as */ $q->join('tbl');
插入数据
- 我们可以插入整行:$data1 = [1, 2, ‘value’];
- 我们可以使用默认数据插入key=>value行:$data2 = [‘text’ => ‘value’];
Query::insert('table', $data1); Query::insert('table', $data2); // Dont forget to execute query... // We can insert even more data at once Query::insert('table', [$data1, $data1, $data1]); // id conflict with maybe Query::insert('table', [$data2, $data2, $data]); // But all data in query must have same format (full-row or key-value)
更新数据
$q = Query::update('table', [ 'col' => 'new value', 'col2' => 'new value', 'col3' => Fn::CONCAT('col4', 'col5') ]); // if we forget something $q->set('ups', null); // Add filter $q->whereId(3);
有关过滤器的文档,请参阅Where and Having章节。
与模式一起工作
对于操作数据库模式,我们有以下查询
删除表
Query::drop('table'); Query::drop('table')->ifExists();
创建表
$q = Query::create('table_name', function($q) { $q->integer('id')->primary(); // ... other column definitions }); $q->ifNotExists(); // other table properties
列类型
查询支持以下列类型
$q->boolean('true_false'); $q->enum('sex', 'male', 'female', 'alien'); $q->set('digits', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'); $q->smallInt('small'); $q->mediumInt('medium'); $q->integer('int'); $q->bigInt('big'); $q->decimal('decimal', $precision = 10, $scale = 5); $q->vachar('varchar', 255); $q->varchar('text'); $q->timestamp('timestamp'); $q->time('time'); $q->date('date'); $q->datetime('date_time'); $q->year('year');
其中一些类型不是所有数据库都支持。在sqlite数据库中,枚举和集合使用varchar。(检查Database/Parts/Sqlite/ColumnDef.php实现。)
列修饰符
列可以有以下修饰符
$col = $q->integer('int_col'); $col->unsigned(); // unsigned value $col->nullable(); // default is not null $col->default(24); // default is null $col->primary(); // primary key $col->autoincrement(); $col->unique(); // unique index
列引用
我们可以引用其他列
Query::create('table', function($q) { $q->integer('id')->primary()->autoincrement(); }); Query::create('table2', function($q) { $q->integer('id')->primary()->autoincrement(); $q->integer('id_table')->references('table.id'); });
表修饰符
表也可以有修饰符
$table = Query::create('table', function($q) { /* ... */ }); $table->temporary(); // create table in memory $table->ifNotExists(); // skip if already exists $table->option('key', 'value'); // custom option KEY VALUE // Some of options are predefined $table->engine('MyIsam'); // ENGINE=MyIsam $table->charset('utf-8'); // CHARSET SET 'utf-8' $table->defaultCharset('utf-8'); // DEFAULT CHARSET SET 'utf-8'
修改表
修改表功能存在bug且未测试,请报告bug。
Query::alter('table')->addColumn('new_column', 'varchar', 255); Query::alter('table')->addColumn('new_int_col', 'integer')->nullable();
Query::alter('table')->modifyColumn('existing_column', 'text')->nullable(false); Query::alter('table')->modifyColumn('existing_int_column', 'bigInt')->default(123);
Query::changeColumn('existing_column', 'new_name', 'varchar', 255)->nullable();
执行查询
如果我们想执行查询而不需要结果中的任何数据,我们只需调用
$q->execute();
否则我们使用方法 ->fetch() 来获取下一个结果。如果我们想在一次调用中获取所有结果,我们使用方法 ->fetchAll(),它的工作方式完全相同,只是它在一个数组中返回所有结果。
Fetch的第一个参数用于指定我们想要在结果中包含哪些列。如果传递了单个列,则返回单个值(标量);如果传递了数组,则返回值数组。
$q->select('type', 'count')->from('table'); // Get first column while ($type = $q->single()) { echo "Type: $type\n"; } // Get one (named) column while ($res = $q->get('count')) { echo "One column (count): $count\n"; } // Get more columns while ($res = $q->get(['type', 'count'])) { echo "{$res['type']} = {$res['count']}\n"; } // Get row as object while ($res = $q->get()) { echo "{$res->type} = {$res->count}\n"; }
$q->select('type', 'count')->from('table'); // Get first column foreach ($q->singleAll() as $type) { echo "Type: $type\n"; } // Get one (named) column foreach ($q->all('count') as $res) { echo "One column (count): $count\n"; } // Get more columns foreach ($q->all(['type', 'count']) as $res) { echo "{$res['type']} = {$res['count']}\n"; } // Get row as object foreach ($q->all() as $res) { echo "{$res->type} = {$res->count}\n"; } // Custom result transformation $results = $q->all(['type', 'count'], function ($row) { return $row['type'] . ' => ' . $row['count']; }); echo implode("\n", $results);
与对象一起工作
PHP PDO库有一个选项可以将接收到的数据打包到对象中。因此,如果我们使用此功能,我们可以像这样获取数据
$class = $q->fetch(MyClass::class); assert($class instanceof MyClass);
但是,我们想使用对象来表示数据库中的一行。为此,每个类都必须定义表名和主键。
使用对象
表名是类名的小写蛇形写法,主键是“id”。我们可以通过实现返回表名和主键的方法来更改这一点。
class MyTable { public static function tableName() { return 'my_table_v2'; } public static function primaryKey() { return 'id_my_table'; } }
即使类没有这些两个方法,我们也可以访问其表名和主键。但必须从QueryObject类扩展。
class QueryObject { public static function primaryKey() { return 'id'; } public static function tableName() { return snake_case(static::class); } }
当我们完成这些操作后,我们不再需要记住表名了
Query::select() ->from(User::class) ->whereId(3) ->fetch(User::class);
注意:id仍然是列`id`。如果我们想通过主键查找用户,我们应该写:->where(user::primaryKey(), 3)
在后台,Name和Column部分会检查是否是类,并将其替换为Class::tableName()。
$q = Query::select('Obj.id objid', 'name') ->from(Obj::class) ->join(Obj2::class, ['Obj2.id_obj', 'Obj.id']) // id is not necessary primary ->where('Obj2.something', true) ->orderBy('Obj2.sort') ->fetch(['objid', 'name']);
有一个问题。Obj::class可能包含命名空间A\B\Obj。在这种情况下,查询将找不到Obj类,因为它不在Query包中。因此,当新对象被声明时(在from或join方法中),查询将在其子命名空间之一中创建别名。当列引用类时,查询将在这个命名空间中查找,如果存在。
名称冲突
你可能认为,如果我在不同的命名空间中使用具有相同名称的两个类会怎样?
namespace General { class User { } // tablename = users } namespace Deleted { class User { } // tablename = deleted_users } Query::select() ->from(General\User::class) ->from(Deleted\User::class) ->where('User.id', 3); // which one?
如果你真的想使用相同的对象名称,你可以创建class_alias(Existing::class, ‘NewName’), 并使用别名。
使用具有相同名称的对象在不同的数据库中没有问题。查询可以正确处理。
$q1 = Query::select() ->from(mysql\User::class) ->where('User.id', 1); // referencing Query\Aliases\Mysql\User -> mysql\User $q2 = Query::select() ->from(sqlite\User::class) ->where('User.id', 1); // referencing Query\Aliases\Sqlite\User -> sqlite\User Query::withConnection('mysql', function() { $q1->fetch(); }); Query::withConnection('sqlite', function() { $q2->fetch(); });
不同的连接将具有用于使用对象别名的不同命名空间(例如:Query\Aliases\
这是一个很长的章节。
与对象连接
前一章中的连接仍然有效,但它们还不够好。我们仍然需要知道对象之间的关系、它们的表名和它们的id列。
Query::select() ->from(User::class) ->join(Group::class, ['group.id', 'user.group_id']);
如果我们想正确编写,我们应该这样做
Query::select() ->from(User::class) ->join(Group::class, [Group::class . '.' . Group::primaryKey(), User::class . '.group_id']);
uff... 我们仍然需要在代码中到处知道`group_id`列。
关系定义
基本思想是将连接定义写入模型,然后在代码的各个地方使用它。
class User extends QueryObject { protected class refGroup() { return Reference::toOne(Group::class, 'id_group'); } } class Group extends QueryObject { protected class refUsers() { return Reference::toMany(User::class, 'id_group'); } }
现在我们可以开始连接
Query::select('g.name') ->from(User::class) ->join('User.group g'); // and Query::count() ->from(Group::class, 'g') ->join('Group.users') ->where('g.name', 'Guest') ->single();
问题
- [ ] 表可以别名
- [ ] …
开发
class Model { public function tableName() { return 'ime_tabele'; } public function primary() { return 'ID_ime_tabele'; } }
class Query { private $components = []; // Unused... private $bindings = []; private $sources = [ 'table or alias' => 'source info', 'alias' => 'table_name', 'table_name2' => 'table_name2', // 'g' => new Source('groups', 'g') ]; private $select = [ 'name' => 'whatever(*)', 'count' => new Column(), 'xxx' => new Raw() ]; }
abstract class Source { private $name; public function __construct($name) { $this->name = $name; } public function name() { return $this->name; } abstract public function primary(); abstract public function table(); public function debug() { echo "Table: `" . $this->name() . "` primary: `" . $this->primary() . "`\n"; } }
class TableSource extends Source { private $table; public function __construct($table, $name = null) { parent::__construct($name ?: $table); $this->table = $table; } public function primary() { return 'id'; } public function table() { return $table; } }
class ModelSource extends TableSource { private $model; public function __construct($model, $name = null) { $this->setModel($model); parent::__construct($model::tableName(), $name); } private function setModel($model) { // if (class_exists($model) and is_subclass_of($model, QueryObject::class)) { if (class_exists($model)) { $this->model = $model; } else { throw new Exception("Wrong class name '$model'"); // Should be QueryException } } public function primary() { $m = $this->model; return $m::primary(); } public function table() { $m = $this->model; return $m::tableName(); } };
$s1 = new ModelSource(Model::class); $s2 = new ModelSource(Model::class, 'alias'); $s3 = new TableSource('tabela', 'alias'); echo "\n"; $s1->debug(); $s2->debug(); $s3->debug();
Table: `ime_tabele` primary: `ID_ime_tabele` Table: `alias` primary: `ID_ime_tabele` Table: `alias` primary: `id`
查询
class Query { // private $components = []; // Unused... private $bindings = []; private $sources = [ 'table or alias' => 'source info', 'alias' => 'table_name', 'table_name2' => 'table_name2', // 'g' => new Source('groups', 'g') ]; // private $select = [ // 'name' => 'whatever(*)', // 'count' => new Column(), // 'xxx' => new Raw() // ]; public function from($source, $alias = null) { if (class_exists($source)) { // it is model $source = new ModelSource($source, $alias); } else { $source = new TableSource($source, $alias); } $sources[$source->name()] = $source; } }
关系
- 在2017年5月1日星期一21:12记录的笔记
请修复查询,使其仅从第一个表中选择(在模型的情况下) - 在2017年5月1日星期一21:11记录的笔记
查询中的“id”不一定是主键
class Person as QueryObject { public static function relGroup() { return self::reference() ->hasOne(Group::class, 'group_id'); } public static function relPosts() { return self::reference() ->hasMany(Post::class, 'person_id'); } }
Query::select() ->from(Person::class) ->join('Person.group g') ->join('Person.posts posts') ->groupBy(Person::class) ->where('g.name', 'Regular user') ->having(Fn::count('posts.!id!'), '>', 10) ->all();
QueryObject
如果我们想在查询中使用对象,它们必须扩展QueryObject。如果我们想为对象使用不同的表名或主键,我们可以覆盖tableName()或primaryKey()函数。
如果我们想使用完全不同的规则来生成表名和主键,我们可以使用我们自定义的QueryObject类,该类扩展了原始的QueryObject。
class QueryObject { public static function tableName() { return camel_case(drop_namespace(QueryObject::class)); } public static function primaryKey() { return 'id'; } }