教程:Hyperf
一 预加载
1.1 原理
通过设置Hyperf\Database\Model\Builder::eagerLoad加载需查询用的model, 查询条件子查询使用in。
eagerLoad在Builder::eagerLoadRelations()被调用,传入Builder::eagerLoadRelation()。eagerLoadRelation()中调用addEagerConstraints()构造查询。
1.2 测试
#测试$log = User::query()->getConnection()->enableQueryLog();$info = User::query()->with('role')->find(1);$log = User::query()->getConnection()->getQueryLog();var_dump($info, $log);#测试结果object(App1\Model\User){……}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(106.81)}[1]=>array(3) {["query"]=>string(241) "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`.`role_id` = ? and `role_user`.`user_id` in (1)"["bindings"]=>array(1) {[0]=>int(1)}["time"]=>float(20.69)}}
1.3 源码
#Hyperf\Database\Model\Modelpublic static function with($relations) {return static::query()->with(is_string($relations) ? func_get_args() : $relations);}public function newCollection(array $models = []) {return new Collection($models);}#Hyperf\Database\Model\Builderpublic function with($relations) {$eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations);$this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad);return $this;}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);}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;}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 find($id, $columns = ['*']) {if (is_array($id) || $id instanceof Arrayable) {return $this->findMany($id, $columns);}return $this->whereKey($id)->first($columns);}public function findMany($ids, $columns = ['*']) {if (empty($ids)) {return $this->model->newCollection();}return $this->whereKey($ids)->get($columns);}#Hyperf\Database\Model\Relations\BelongsToManypublic function addEagerConstraints(array $models){$whereIn = $this->whereInMethod($this->parent, $this->parentKey);$this->query->{$whereIn}($this->getQualifiedForeignPivotKeyName(),$this->getKeys($models, $this->parentKey));}#Hyperf\Database\Model\Relations\HasOneOrManypublic function addEagerConstraints(array $models) {$whereIn = $this->whereInMethod($this->parent, $this->localKey);$this->query->{$whereIn}($this->foreignKey,$this->getKeys($models, $this->localKey));}
二 多态关联
2.1 数据库
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;CREATE TABLE `articles` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT,`user_id` int(11) DEFAULT NULL,`title` varchar(255) DEFAULT NULL,`created_at` datetime DEFAULT NULL,`updated_at` datetime DEFAULT NULL,`deleted_at` datetime DEFAULT NULL,PRIMARY KEY (`id`)) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;CREATE TABLE `photo` (`id` int(11) NOT NULL AUTO_INCREMENT,`img_url` varchar(255) DEFAULT NULL,`ref_id` int(11) DEFAULT NULL COMMENT '关联id',`ref_type` tinyint(1) DEFAULT NULL COMMENT '关联类型 1用户 2文章',PRIMARY KEY (`id`)) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
用户和图片一对多关系,文章和图片一对一关系。
2.2 测试
#model#Userpublic function photo() {return $this->morphMany(Photo::class, 'ref');}#Articlepublic function author() {return $this->belongsTo(User::class, 'user_id', 'id');}public function photo() {return $this->morphOne(Photo::class, 'ref');}#Photopublic function ref() {return $this->morphTo('ref');}#listenerclass MorphMapRelationListener implements ListenerInterface {public function listen(): array {return [BootApplication::class,];}public function process(object $event) {Relation::morphMap(['1' => User::class,'2' => Article::class,]);}}#config\autoload\listeners.phpreturn ["App\Listener\MorphMapRelationListener",];
一对多
#测试$obj2 = User::query()->find(1);$list = $obj2->photo->all();foreach ($list as $key => $value) { var_dump($value->toArray());}#测试结果array(4) {["id"]=>int(1)["img_url"]=>string(143) "https://gitee.com/lsswear/plane_design/raw/master/%E4%B8%AD%E5%9B%BD%E5%A4%8D%E5%8F%A4%E5%9B%BE%E6%A1%88/001O1JA0ly1hl17zd4l2qj60u01hcazo02.jpg"["ref_id"]=>int(1)["ref_type"]=>int(1)}array(4) {["id"]=>int(3)["img_url"]=>string(141) "https://gitee.com/lsswear/plane_design/raw/master/%E4%B8%AD%E5%9B%BD%E5%A4%8D%E5%8F%A4%E5%9B%BE%E6%A1%88/e9fbfa79gy1hku6j75abvj20j60ic76s.jpg"["ref_id"]=>int(1)["ref_type"]=>int(1)}
一对一
#测试$log = Article::query()->getConnection()->enableQueryLog();$obj1 = Article::query()->find(1);$info = $obj1->photo->toArray();var_dump($info);$log = Article::query()->getConnection()->getQueryLog();var_dump($log);#测试结果array(4) {["id"]=>int(2)["img_url"]=>string(143) "https://gitee.com/lsswear/plane_design/raw/master/%E4%B8%AD%E5%9B%BD%E5%A4%8D%E5%8F%A4%E5%9B%BE%E6%A1%88/001O1JA0ly1hlnbt7gu7nj60u00u0wwg02.jpg"["ref_id"]=>int(1)["ref_type"]=>int(2)}array(2) {[0]=>array(3) {["query"]=>string(94) "select * from `articles` where `articles`.`id` = ? and `articles`.`deleted_at` is null limit 1"["bindings"]=>array(1) {[0]=>int(1)}["time"]=>float(63.42)}[1]=>array(3) {["query"]=>string(116) "select * from `photo` where `photo`.`ref_id` = ? and `photo`.`ref_id` is not null and `photo`.`ref_type` = ? limit 1"["bindings"]=>array(2) {[0]=>int(1)[1]=>int(2)}["time"]=>float(1.76)}}
嵌套关联
#测试$log = Photo::query()->getConnection()->enableQueryLog();$photo = Photo::query()->with([ 'ref' => function (MorphTo $morphTo) { $morphTo->morphWith([Article::class => ["author"], ]);},])->get();$log = Photo::query()->getConnection()->getQueryLog();var_dump($photo->toArray(), $log);#测试结果array(3) {[0]=>array(5) {["id"]=>int(1)["img_url"]=>string(143) "https://gitee.com/lsswear/plane_design/raw/master/%E4%B8%AD%E5%9B%BD%E5%A4%8D%E5%8F%A4%E5%9B%BE%E6%A1%88/001O1JA0ly1hl17zd4l2qj60u01hcazo02.jpg"["ref_id"]=>int(1)["ref_type"]=>int(1)["ref"]=>array(4) {["id"]=>int(1)["name"]=>string(3) "123"["age"]=>int(22)["deleted_at"]=>NULL}}[1]=>array(5) {["id"]=>int(2)["img_url"]=>string(143) "https://gitee.com/lsswear/plane_design/raw/master/%E4%B8%AD%E5%9B%BD%E5%A4%8D%E5%8F%A4%E5%9B%BE%E6%A1%88/001O1JA0ly1hlnbt7gu7nj60u00u0wwg02.jpg"["ref_id"]=>int(1)["ref_type"]=>int(2)["ref"]=>array(7) {["id"]=>int(1)["user_id"]=>int(1)["title"]=>string(5) "test1"["created_at"]=>string(19) "2024-01-13 10:05:51"["updated_at"]=>string(19) "2024-01-13 10:05:53"["deleted_at"]=>NULL["author"]=>array(4) {["id"]=>int(1)["name"]=>string(3) "123"["age"]=>int(22)["deleted_at"]=>NULL}}}[2]=>array(5) {["id"]=>int(3)["img_url"]=>string(141) "https://gitee.com/lsswear/plane_design/raw/master/%E4%B8%AD%E5%9B%BD%E5%A4%8D%E5%8F%A4%E5%9B%BE%E6%A1%88/e9fbfa79gy1hku6j75abvj20j60ic76s.jpg"["ref_id"]=>int(1)["ref_type"]=>int(1)["ref"]=>array(4) {["id"]=>int(1)["name"]=>string(3) "123"["age"]=>int(22)["deleted_at"]=>NULL}}}array(4) {[0]=>array(3) {["query"]=>string(21) "select * from `photo`"["bindings"]=>array(0) {}["time"]=>float(65.45)}[1]=>array(3) {["query"]=>string(89) "select * from `userinfo` where `userinfo`.`id` in (1) and `userinfo`.`deleted_at` is null"["bindings"]=>array(0) {}["time"]=>float(1.68)}[2]=>array(3) {["query"]=>string(89) "select * from `articles` where `articles`.`id` in (1) and `articles`.`deleted_at` is null"["bindings"]=>array(0) {}["time"]=>float(2.13)}[3]=>array(3) {["query"]=>string(89) "select * from `userinfo` where `userinfo`.`id` in (1) and `userinfo`.`deleted_at` is null"["bindings"]=>array(0) {}["time"]=>float(1.33)}}
#测试$list = Photo::query()->whereHasMorph('ref',[User::class,Article::class,],function (Builder $query) {$query->where('ref_id', 1);})->get();foreach ($list as $key => $value) {$item = $value->toArray();var_dump($item);}#测试结果array(4) {["id"]=>int(1)["img_url"]=>string(143) "https://gitee.com/lsswear/plane_design/raw/master/%E4%B8%AD%E5%9B%BD%E5%A4%8D%E5%8F%A4%E5%9B%BE%E6%A1%88/001O1JA0ly1hl17zd4l2qj60u01hcazo02.jpg"["ref_id"]=>int(1)["ref_type"]=>int(1)}array(4) {["id"]=>int(3)["img_url"]=>string(141) "https://gitee.com/lsswear/plane_design/raw/master/%E4%B8%AD%E5%9B%BD%E5%A4%8D%E5%8F%A4%E5%9B%BE%E6%A1%88/e9fbfa79gy1hku6j75abvj20j60ic76s.jpg"["ref_id"]=>int(1)["ref_type"]=>int(1)}
根据测试内容和源码,model设置morphMany()、morphOne()都使用Hyperf\Database\Model\Relations\HasOneOrMany::matchOneOrMany()方法。两者参数,第一个参数为有对应关系的model,第二个参数有对应id和对应键的前缀,但是如果对应id或对应键不为“前缀_id”、“前缀_type”格式,可以将id设置为第三个参数,type设置为第四个参数吗,第五个参数为被调用model的对应键。
例如:
#mysqlphoto refidreftypeuserid1articleid2#model#Userpublic function photo() {return $this->morphMany(Photo::class,null,'refid','reftype','id1');}#Articlepublic function photo() {return $this->morphMany(Photo::class,null,'refid','reftype','id2');}#photopublic function ref() {return $this->morphTo(null,'refid','reftype');}
一对多时返回集合对象,需要用all()等方法再获取数据,之后可以用list。一对一直接返回model对象,再调用all()等,是针对这个返回model的操作。
比如,根据上面的例子,$obj1 = Article::query()->find(1)->photo->all(),返回photo表的全部数据。
作为区分多态的字段type,字段名可自定义,字段值系统默认为类名,不方便使用,可以设置监听做对应关系。Relation::morphMap()参数中键名为对应关系的值,键值为类名。listen()方法设置执行process()的类。
2.3 原理
参考:
hyperf 二十一 数据库 模型关系-CSDN博客
hyperf console 执行-CSDN博客
和模型关系实现的原理差不多都是使用__get()查询,通过match()执行查询。
有点区别是中间件的设置,中间件通过ProviderConfig::load();加载配置。ListenerProviderFactory::register()执行监听。
根据上述例子中监听设置为Relation::morphMap(),返回静态static::$morphMap()。Relation::morphMap()传入数组为设置,无参数为获取。其中使用array_search()通过传入的类名,获取对应的键名并返回。addConstraints()方法调用返回的键名构造sql。
whereHasMorph()使用Hyperf\Database\Model\Concerns\HasRelationships::belongsTo()实现。
2.4 源码
2.4.1 监听执行
#Hyperf\Framework\ApplicationFactoryclass ApplicationFactory{public function __invoke(ContainerInterface $container){if ($container->has(EventDispatcherInterface::class)) {$eventDispatcher = $container->get(EventDispatcherInterface::class);$eventDispatcher->dispatch(new BootApplication());}……$application = new Application();if (isset($eventDispatcher) && class_exists(SymfonyEventDispatcher::class)) {$application->setDispatcher(new SymfonyEventDispatcher($eventDispatcher));}foreach ($commands as $command) {$application->add($container->get($command));}return $application;}}#Hyperf\Event\EventDispatcherFactoryclass EventDispatcherFactory{public function __invoke(ContainerInterface $container){$listeners = $container->get(ListenerProviderInterface::class);$stdoutLogger = $container->get(StdoutLoggerInterface::class);return new EventDispatcher($listeners, $stdoutLogger);}}#Hyperf\Event\ConfigProviderclass ConfigProvider{public function __invoke(): array{return ['dependencies' => [ListenerProviderInterface::class => ListenerProviderFactory::class,EventDispatcherInterface::class => EventDispatcherFactory::class,],'annotations' => ['scan' => ['paths' => [__DIR__,],],],];}}#Hyperf\Event\ListenerProviderFactorypublic function __invoke(ContainerInterface $container){$listenerProvider = new ListenerProvider();// Register config listeners.$this->registerConfig($listenerProvider, $container);// Register annotation listeners.$this->registerAnnotations($listenerProvider, $container);return $listenerProvider;}private function registerAnnotations(ListenerProvider $provider, ContainerInterface $container): void{foreach (AnnotationCollector::list() as $className => $values) {/** @var Listener $annotation */if ($annotation = $values['_c'][Listener::class] ?? null) {$this->register($provider, $container, $className, (int) $annotation->priority);}}}private function register(ListenerProvider $provider, ContainerInterface $container, string $listener, int $priority = 1): void{$instance = $container->get($listener);if ($instance instanceof ListenerInterface) {foreach ($instance->listen() as $event) {$provider->on($event, [$instance, 'process'], $priority);}}}
2.4.2 自定义多态映射
#Hyperf\Database\Model\Relations\Relationpublic static function morphMap(array $map = null, $merge = true) {$map = static::buildMorphMapFromModels($map);if (is_array($map)) {static::$morphMap = $merge && static::$morphMap? $map+static::$morphMap : $map;}return static::$morphMap;}#Hyperf\Database\Model\Concerns\HasRelationshipspublic function getMorphClass() {$morphMap = Relation::morphMap();if (!empty($morphMap) && in_array(static::class, $morphMap)) {return array_search(static::class, $morphMap, true);}return static::class;}#Hyperf\Database\Model\Relations\MorphOneOrManypublic function __construct(Builder $query, Model $parent, $type, $id, $localKey){$this->morphType = $type;$this->morphClass = $parent->getMorphClass();parent::__construct($query, $parent, $id, $localKey);}public function addConstraints(){if (Constraint::isConstraint()) {parent::addConstraints();$this->query->where($this->morphType, $this->morphClass);}}
2.4.3 多态关联查询
#Hyperf\Database\Model\Concerns\QueriesRelationshipspublic function whereHasMorph($relation, $types, Closure $callback = null, $operator = '>=', $count = 1){return $this->hasMorph($relation, $types, $operator, $count, 'and', $callback);}public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null){$relation = $this->getRelationWithoutConstraints($relation);$types = (array) $types;if ($types === ['*']) {$types = $this->model->newModelQuery()->distinct()->pluck($relation->getMorphType())->filter()->all();foreach ($types as &$type) {$type = Relation::getMorphedModel($type) ?? $type;}}return $this->where(function ($query) use ($relation, $callback, $operator, $count, $types) {foreach ($types as $type) {$query->orWhere(function ($query) use ($relation, $callback, $operator, $count, $type) {$belongsTo = $this->getBelongsToRelation($relation, $type);if ($callback) {$callback = function ($query) use ($callback, $type) {return $callback($query, $type);};}$query->where($relation->getMorphType(), '=', (new $type())->getMorphClass())->whereHas($belongsTo, $callback, $operator, $count);});}}, null, null, $boolean);}protected function getBelongsToRelation(MorphTo $relation, $type){$belongsTo = Relation::noConstraints(function () use ($relation, $type) {return $this->model->belongsTo($type,$relation->getForeignKeyName(),$relation->getOwnerKeyName());});$belongsTo->getQuery()->mergeConstraintsFrom($relation->getQuery());return $belongsTo;}