教程:Hyperf
一 定义关联
根据文档
- 一对一:Model::hasOne(被关联模型,被关联模型外键,本模型被关联的字段)
- 一对多:Model::hasMany(被关联模型,被关联模型外键,本模型被关联的字段)
- 反向一对多:Model::belongsTo(被关联模型,本模型外键,被关联模型的对应字段,关联关系)
- 多对多:Model::belongsToMany(被关联模型,自定义连接表名,该模型在连接表里的外键名,被关联模型在连接表里的外键名,该模型关联键,被关联模型关联键,关联关系)
1.1 一对一、一对多
根据文档说明,需要在model中设置方法调用hasOne()方法。获取的使用在查询出来的数据中获取对应方法名的属性。
#model
class User extends Model
{
public function role()
{
return $this->hasOne(Role::class, 'user_id', 'id');
}
public function articles() {
return $this->hasMany(Article::class, 'user_id', 'id');
}
}
class Article extends Model {
public function author() {
return $this->belongsTo(User::class, 'user_id', 'id');
}
}
#查询
//一对一
$role = User::query()->find(1)->role;
//返回Role类对象
//一对多
$info = User::query()->find(1)->articles;
//返回Hyperf\Database\Model\Collection类
//一对多反向
$info = Article::find(1)->author;
//返回App1\Model\User
根据上面的例子,实际上find(1)查询user表id为1的行,获取role时调用role()方法,并且结果写入user对象的relations属性中。
model中定义的方法返回Relation对象,调用model的对应属性时执行Hyperf\Database\Model\Model::__get(),使用Hyperf\Database\Model\Concerns\HasAttributes::getRelationValue()设置Moel::relations属性,并设置执行结果。Relation对象是包含查询信息的Builder对象。
一对多和一对一都是调用相同父类,通过HasOneOrMany::matchOneOrMany()通过最后参数$type的值'one'、'many'处理区分。
1.2 多对多
和一对多、一对一流程一样,但是用的父类不用,使用Hyperf\Database\Model\Relations\BelongsToMany::addConstraints()构造查询,被调用对应属性时执行查询。
BelongsToMany::performJoin()以设置的model作为基础表关联中间表,BelongsToMany::addWhereConstraints()关联被调用的model。
#数据库
CREATE TABLE `role_user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`role_id` int(11) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
CREATE TABLE `roles` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`role_name` varchar(255) DEFAULT NULL,
`status` tinyint(1) DEFAULT NULL COMMENT '状态 1可用 0不可用',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
CREATE TABLE `userinfo` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`age` tinyint(2) DEFAULT '0',
`deleted_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=24 DEFAULT CHARSET=utf8;
#模型设置
class User extends Model {
public function role() {
return $this->belongsToMany(Role::class);
}
}
#查询
public function testmodel2() {
$obj2 = User::query()->find(1);
$list = $obj2->role;
foreach ($list as $key => $value) {
$role_id = $value->pivot->role_id;
$user_id = $value->pivot->user_id;
var_dump($role_id, $user_id);
}
}
#结果
int(1)
int(1)
int(2)
int(1)
实际Hyperf\Database\Model\Concerns\HasRelationships::belongsToMany()参数包括:
table 中间表、foreignPivotKey 中间表外键、relatedPivotKey 中间表关联键、parentKey 调用模型主键、relatedKey 设置模型主键、relation 关系。
在上面例子中,User::query()->find(1)->role中User为调用模型,Role为设置的模型,中间模型为RoleUser。belongsToMany()参数默认值为role_user、user_id、role_id、id、id、role。
例子中其他值未设置,因为关联表中对应的id都是为对应的表名加id,符合框架设置默认值的格式。若把role_user中rule_id设置为rule1_id,查询会报错找不到role_user.rule_id字段。
1.2.1 获取中间表字段
中间表获取通过pivot获取,其属性名可改。BelongsToMany::accessor默认pivot,可使用BelongsToMany::as()设置accessor属性名。
通过BelongsToMany::match()调用BelongsToMany::buildDictionary()设置$this->accessor的值。
一对一、一对多、多对多的过程中math()执行都是通过Builder::get()执行。
#model
public function role() {
return $this->belongsToMany(Role::class)->as("role");
}
#测试
$obj2 = User::query()->find(1);
$list = $obj2->role;
foreach ($list as $key => $value) {
$role_id = $value->role->role_id;
$user_id = $value->role->user_id;
var_dump($role_id, $user_id);
}
#测试结果
int(1)
int(1)
int(2)
int(1)
1.2.2 通过中间表过滤关系
并且可以对中间表设置查询条件,比如BelongsToMany::wherePivot()、BelongsToMany::wherePivotIn()、BelongsToMany::wherePivotIn()、BelongsToMany::orWherePivot()、BelongsToMany::withPivotValue()等,可以设置Builder的where的方法。
#model
public function role() {
return $this->belongsToMany(Role::class)->as("role")->wherePivot('role_id', "=", 1);
}
#测试
public function testmodel2() {
$log = User::query()->getConnection()->enableQueryLog();
$obj2 = User::query()->find(1);
$list = $obj2->role;
foreach ($list as $key => $value) {
$role_id = $value->role->role_id;
$user_id = $value->role->user_id;
var_dump($role_id, $user_id);
}
$log = User::query()->getConnection()->getQueryLog();
var_dump($log);
}
#测试结果
int(1)
int(1)
array(2) {
[0]=>
array(3) {
["query"]=>
string(94) "select * from `userinfo` where `userinfo`.`id` = ? and `userinfo`.`deleted_at` is null limit 1"
["bindings"]=>
array(1) {
[0]=>
int(1)
}
["time"]=>
float(47.56)
}
[1]=>
array(3) {
["query"]=>
string(238) "select `roles`.*, `role_user`.`user_id` as `pivot_user_id`, `role_user`.`role_id` as `pivot_role_id` from `roles` inner join `role_user` on `roles`.`id` = `role_user`.`role_id` where `role_user`.`user_id` = ? and `role_user`.`role_id` = ?"
["bindings"]=>
array(2) {
[0]=>
int(1)
[1]=>
int(1)
}
["time"]=>
float(2.79)
}
}
原本想把中间表的条件放到controller层,就是通过参数设置。那么model需要改为role($roleid),controller层调用就得是User::query->find(1)->role(1),但是结果并没有执行第二次包含join的查询。因为Hyperf\Database\Model\Model::__get()和Hyperf\Database\Model\Model::__call()逻辑不同。所以中间表的过滤条件,controller大概不能控制。
二 源码
3.1 match()
3.1.1 多对多
public function match(array $models, Collection $results, $relation)
{
$dictionary = $this->buildDictionary($results);
// Once we have an array dictionary of child objects we can easily match the
// children back to their parent using the dictionary and the keys on the
// the parent models. Then we will return the hydrated models back out.
foreach ($models as $model) {
if (isset($dictionary[$key = $model->{$this->parentKey}])) {
$model->setRelation(
$relation,
$this->related->newCollection($dictionary[$key])
);
}
}
return $models;
}
protected function buildDictionary(Collection $results)
{
// First we will build a dictionary of child models keyed by the foreign key
// of the relation so that we will easily and quickly match them to their
// parents without having a possibly slow inner loops for every models.
$dictionary = [];
foreach ($results as $result) {
$dictionary[$result->{$this->accessor}->{$this->foreignPivotKey}][] = $result;
}
return $dictionary;
}
3.1.2 一对一、一对多
#Hyperf\Database\Model\Relations\HasOneOrMany
public function matchOne(array $models, Collection $results, $relation) {
return $this->matchOneOrMany($models, $results, $relation, 'one');
}
public function matchMany(array $models, Collection $results, $relation) {
return $this->matchOneOrMany($models, $results, $relation, 'many');
}
protected function matchOneOrMany(array $models, Collection $results, $relation, $type) {
$dictionary = $this->buildDictionary($results);
// Once we have the dictionary we can simply spin through the parent models to
// link them up with their children using the keyed dictionary to make the
// matching very convenient and easy work. Then we'll just return them.
foreach ($models as $model) {
if (isset($dictionary[$key = $model->getAttribute($this->localKey)])) {
$model->setRelation(
$relation,
$this->getRelationValue($dictionary, $key, $type)
);
}
}
return $models;
}
protected function getRelationValue(array $dictionary, $key, $type) {
$value = $dictionary[$key];
return $type === 'one' ? reset($value) : $this->related->newCollection($value);
}
#Hyperf\Database\Model\Relations\HasOne
public function match(array $models, Collection $results, $relation) {
return $this->matchOne($models, $results, $relation);
}
#Hyperf\Database\Model\Relations\HasMany
public function match(array $models, Collection $results, $relation)
{
return $this->matchMany($models, $results, $relation);
}
#Hyperf\Database\Model\Concerns\HasRelationships
public function hasOne($related, $foreignKey = null, $localKey = null) {
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
return $this->newHasOne($instance->newQuery(), $this, $instance->getTable() . '.' . $foreignKey, $localKey);
}
public function hasMany($related, $foreignKey = null, $localKey = null) {
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
return $this->newHasMany(
$instance->newQuery(),
$this,
$instance->getTable() . '.' . $foreignKey,
$localKey
);
}
protected function newHasOne(Builder $query, Model $parent, $foreignKey, $localKey) {
return new HasOne($query, $parent, $foreignKey, $localKey);
}
protected function newHasMany(Builder $query, Model $parent, $foreignKey, $localKey) {
return new HasMany($query, $parent, $foreignKey, $localKey);
}
3.1.3 调用
#Hyperf\Database\Model\Builder
protected function eagerLoadRelation(array $models, $name, Closure $constraints)
{
// First we will "back up" the existing where conditions on the query so we can
// add our eager constraints. Then we will merge the wheres that were on the
// query back to it in order that any where conditions might be specified.
$relation = $this->getRelation($name);
$relation->addEagerConstraints($models);
$constraints($relation);
// Once we have the results, we just match those back up to their parent models
// using the relationship instance. Then we just return the finished arrays
// of models which have been eagerly hydrated and are readied for return.
return $relation->match(
$relation->initRelation($models, $name),
$relation->getEager(),
$name
);
}
public function eagerLoadRelations(array $models)
{
foreach ($this->eagerLoad as $name => $constraints) {
// For nested eager loads we'll skip loading them here and they will be set as an
// eager load on the query to retrieve the relation so that they will be eager
// loaded on that query, because that is where they get hydrated as models.
if (strpos($name, '.') === false) {
$models = $this->eagerLoadRelation($models, $name, $constraints);
}
}
return $models;
}
public function get($columns = ['*'])
{
$builder = $this->applyScopes();
// If we actually found models we will also eager load any relationships that
// have been specified as needing to be eager loaded, which will solve the
// n+1 query issue for the developers to avoid running a lot of queries.
if (count($models = $builder->getModels($columns)) > 0) {
$models = $builder->eagerLoadRelations($models);
}
return $builder->getModel()->newCollection($models);
}
#Hyperf\Database\Model\Model
public function __get($key)
{
return $this->getAttribute($key);
}
#Hyperf\Database\Model\Concerns\HasAttributes
public function getAttribute($key)
{
if (!$key) {
return;
}
// If the attribute exists in the attribute array or has a "get" mutator we will
// get the attribute's value. Otherwise, we will proceed as if the developers
// are asking for a relationship's value. This covers both types of values.
if (array_key_exists($key, $this->getAttributes())
|| $this->hasGetMutator($key)
|| $this->isClassCastable($key)) {
return $this->getAttributeValue($key);
}
// Here we will determine if the model base class itself contains this given key
// since we don't want to treat any of those methods as relationships because
// they are all intended as helper methods and none of these are relations.
if (method_exists(self::class, $key)) {
return;
}
return $this->getRelationValue($key);
}
public function getRelationValue($key)
{
// If the key already exists in the relationships array, it just means the
// relationship has already been loaded, so we'll just return it out of
// here because there is no need to query within the relations twice.
if ($this->relationLoaded($key)) {
return $this->relations[$key];
}
// If the "attribute" exists as a method on the model, we will just assume
// it is a relationship and will load and return results from the query
// and hydrate the relationship's value on the "relationships" array.
if (method_exists($this, $key)) {
return $this->getRelationshipFromMethod($key);
}
}
protected function getRelationshipFromMethod($method)
{
$relation = $this->{$method}();
if (!$relation instanceof Relation) {
if (is_null($relation)) {
throw new LogicException(sprintf(
'%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?',
static::class,
$method
));
}
throw new LogicException(sprintf(
'%s::%s must return a relationship instance.',
static::class,
$method
));
}
return tap($relation->getResults(), function ($results) use ($method) {
$this->setRelation($method, $results);
});
}
#Hyperf\Database\Model\Relations\HasMany
public function getResults()
{
return $this->query->get();
}
#Hyperf\Database\Model\Relations\HasOne
public function getResults() {
return $this->query->first() ?: $this->getDefaultFor($this->parent);
}
#Hyperf\Database\Model\Relations\BelongsToMany
public function getResults()
{
return $this->get();
}
#Hyperf\Database\Model\Relations\BelongsTo
public function getResults()
{
return $this->query->first() ?: $this->getDefaultFor($this->parent);
}
3.2 pivot属性名自定义
#Hyperf\Database\Model\Relations\BelongsToMany
protected $accessor = 'pivot';
public function as($accessor)
{
$this->accessor = $accessor;
return $this;
}
3.3 通过中间件顾虑
#Hyperf\Database\Model\Relations\BelongsToMany
public function wherePivot($column, $operator = null, $value = null, $boolean = 'and')
{
$this->pivotWheres[] = func_get_args();
return $this->where($this->table . '.' . $column, $operator, $value, $boolean);
}
public function wherePivotIn($column, $values, $boolean = 'and', $not = false)
{
$this->pivotWhereIns[] = func_get_args();
return $this->whereIn($this->table . '.' . $column, $values, $boolean, $not);
}
public function orWherePivot($column, $operator = null, $value = null)
{
return $this->wherePivot($column, $operator, $value, 'or');
}
public function withPivotValue($column, $value = null)
{
if (is_array($column)) {
foreach ($column as $name => $value) {
$this->withPivotValue($name, $value);
}
return $this;
}
if (is_null($value)) {
throw new InvalidArgumentException('The provided value may not be null.');
}
$this->pivotValues[] = compact('column', 'value');
return $this->wherePivot($column, '=', $value);
}
public function orWherePivotIn($column, $values)
{
return $this->wherePivotIn($column, $values, 'or');
}
3.4 __get()、__call()
#Hyperf\Database\Model\Model
public function __get($key)
{
return $this->getAttribute($key);
}
public function __call($method, $parameters)
{
if (in_array($method, ['increment', 'decrement'])) {
return $this->{$method}(...$parameters);
}
return call([$this->newQuery(), $method], $parameters);
}
三 理解
不管关系如何,都是先查一遍主表,再执行对应sql,可以通过不同设置拼接查询sql。
可能是使用习惯问题,平时还是不太喜欢这种比较隐晦的方式设置对应关系,可能用会手动多查几次。
根据这种方式,在不了解框架的情况下,会增加二开的难度。
而且设置中间表的条件也不是很自由。