教程:Hyperf
通过设置?Hyperf\Database\Model\Builder::eagerLoad加载需查询用的model, 查询条件子查询使用in。
eagerLoad在Builder::eagerLoadRelations()被调用,传入Builder::eagerLoadRelation()。eagerLoadRelation()中调用addEagerConstraints()构造查询。
#测试
$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)
}
}
#Hyperf\Database\Model\Model
public 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\Builder
public 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\BelongsToMany
public 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\HasOneOrMany
public function addEagerConstraints(array $models) {
$whereIn = $this->whereInMethod($this->parent, $this->localKey);
$this->query->{$whereIn}(
$this->foreignKey,
$this->getKeys($models, $this->localKey)
);
}
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;
?用户和图片一对多关系,文章和图片一对一关系。
#model
#User
public function photo() {
return $this->morphMany(Photo::class, 'ref');
}
#Article
public function author() {
return $this->belongsTo(User::class, 'user_id', 'id');
}
public function photo() {
return $this->morphOne(Photo::class, 'ref');
}
#Photo
public function ref() {
return $this->morphTo('ref');
}
#listener
class 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.php
return [
"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的对应键。
例如:
#mysql
photo
refid
reftype
user
id1
article
id2
#model
#User
public function photo() {
return $this->morphMany(Photo::class,null,'refid','reftype','id1');
}
#Article
public function photo() {
return $this->morphMany(Photo::class,null,'refid','reftype','id2');
}
#photo
public 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()的类。
参考:
和模型关系实现的原理差不多都是使用__get()查询,通过match()执行查询。
有点区别是中间件的设置,中间件通过ProviderConfig::load();加载配置。ListenerProviderFactory::register()执行监听。
根据上述例子中监听设置为Relation::morphMap(),返回静态static::$morphMap()。Relation::morphMap()传入数组为设置,无参数为获取。其中使用array_search()通过传入的类名,获取对应的键名并返回。addConstraints()方法调用返回的键名构造sql。
whereHasMorph()使用Hyperf\Database\Model\Concerns\HasRelationships::belongsTo()实现。
#Hyperf\Framework\ApplicationFactory
class 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\EventDispatcherFactory
class EventDispatcherFactory
{
public function __invoke(ContainerInterface $container)
{
$listeners = $container->get(ListenerProviderInterface::class);
$stdoutLogger = $container->get(StdoutLoggerInterface::class);
return new EventDispatcher($listeners, $stdoutLogger);
}
}
#Hyperf\Event\ConfigProvider
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => [
ListenerProviderInterface::class => ListenerProviderFactory::class,
EventDispatcherInterface::class => EventDispatcherFactory::class,
],
'annotations' => [
'scan' => [
'paths' => [
__DIR__,
],
],
],
];
}
}
#Hyperf\Event\ListenerProviderFactory
public 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);
}
}
}
#Hyperf\Database\Model\Relations\Relation
public 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\HasRelationships
public 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\MorphOneOrMany
public 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);
}
}
#Hyperf\Database\Model\Concerns\QueriesRelationships
public 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;
}