silverengine/reflectorm

1.0.0 2019-09-24 18:13 UTC

This package is not auto-updated.

Last update: 2024-09-19 16:56:17 UTC


README

关于查询

查询是一个类,帮助我们构建 SQL 查询并从数据库获取结果。

使用 Query 类我们有以下优势

  1. 所有值都经过适当的转义。
  2. 相同的查询可以在不同的数据库上执行。
  3. 事务和子事务(即保存点)的使用简单

构建查询

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');

插入数据

  1. 我们可以插入整行:$data1 = [1, 2, ‘value’];
  2. 我们可以使用默认数据插入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章节。

与模式一起工作

对于操作数据库模式,我们有以下查询

::drop()
Drop
::create()
Create
::alter()
Alter

删除表

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';
    }
}