hyperf 二十一 数据库 模型关系

发布时间:2024年01月16日

教程: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。

??????? 可能是使用习惯问题,平时还是不太喜欢这种比较隐晦的方式设置对应关系,可能用会手动多查几次。

????????根据这种方式,在不了解框架的情况下,会增加二开的难度。

????????而且设置中间表的条件也不是很自由。

文章来源:https://blog.csdn.net/lsswear/article/details/135460052
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。