9.3 KiB
Model association
Define association
Associations are presented as methods in the Hyperf
model class. Like the Hyperf
model itself, associations can also be used as powerful query builder
, providing powerful chaining and querying capabilities. For example, we can attach a constraint to the chained calls associated with role:
$user->role()->where('level', 1)->get();
One to one
One-to-one is the most basic relationship. For example, a User
model might be associated with a Role
model. To define this association, we need to write a role
method in the User
model. Call the hasOne
method inside the role
method and return its result:
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
public function role()
{
return $this->hasOne(Role::class, 'user_id', 'id');
}
}
The first parameter of the hasOne
method is the class name of the associated model. Once the model associations are defined, we can use the Hyperf
dynamic properties to get the related records. Dynamic properties allow you to access relationship methods just like properties defined in the model:
$role = User::query()->find(1)->role;
One-to-many
A "one-to-many" association is used to define a single model with any number of other associated models. For example, an author may have written multiple books. As with all other Hyperf
relationships, the definition of a one-to-many relationship is to write a method in the Hyperf
model:
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
public function books()
{
return $this->hasMany(Book::class, 'user_id', 'id');
}
}
Remember that Hyperf
will automatically determine the foreign key properties of the Book
model. By convention, Hyperf
will use the "snake case" form of the owning model name, plus the _id
suffix as the foreign key field. Therefore, in the above example, Hyperf
will assume that the foreign key corresponding to User
on the Book
model is user_id
.
Once the relationship is defined, the collection of comments can be obtained by accessing the books
property of the User
model. Remember, since Hyperf provides "dynamic properties", we can access associated methods just like properties of the model:
$books = User::query()->find(1)->books;
foreach ($books as $book) {
//
}
Of course, since all associations can also be used as query constructors, you can use chained calls to add additional constraints to the books method:
$book = User::query()->find(1)->books()->where('title', 'Mastering the Hyperf framework in one month')->first();
One-to-many (reverse)
Now that we can get all the works of an author, let's define an association to get its author through the book. This association is the inverse of the hasMany
association and needs to be defined in the child model using the belongsTo
method:
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class Book extends Model
{
public function author()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
}
After this relationship is defined, we can get the associated User
model by accessing the "dynamic property" of the author of the Book
model:
$book = Book::find(1);
echo $book->author->name;
many-to-many
Many-to-many associations are slightly more complicated than hasOne
and hasMany
associations. For example, a user can have many roles, and these roles are also shared by other users. For example, many users may have the role of "Administrator". To define this association, three database tables are required: users
, roles
and role_user
. The role_user
table is named alphabetically by the associated two models, and contains the user_id
and role_id
fields.
Many-to-many associations are defined by calling the result returned by the internal method belongsToMany
. For example, we define the roles
method in the User
model:
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
public function roles()
{
return $this->belongsToMany(Role::class);
}
}
Once the relationship is defined, you can get user roles via the roles
dynamic property:
$user = User::query()->find(1);
foreach ($user->roles as $role) {
//
}
Of course, like all other relational models, you can use the roles
method to add constraints to queries using chained calls:
$roles = User::find(1)->roles()->orderBy('name')->get();
As mentioned earlier, in order to determine the table name of the relational join table, Hyperf
will concatenate the names of the two relational models in alphabetical order. Of course, you can also skip this convention and pass the second parameter to the belongsToMany method:
return $this->belongsToMany(Role::class, 'role_user');
In addition to customizing the table name of the join table, you can also define the key name of the field in the table by passing additional parameters to the belongsToMany
method. The third parameter is the foreign key name of the model that defines this association in the join table, and the fourth parameter is the foreign key name of another model in the join table:
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
Get intermediate table fields
As you just learned, many-to-many relationships require an intermediate table to support, and Hyperf
provides some useful methods to interact with this table. For example, let's say our User
object has multiple Role
objects associated with it. After obtaining these association objects, the data in the intermediate table can be accessed using the model's pivot
attribute:
$user = User::find(1);
foreach ($user->roles as $role) {
echo $role->pivot->created_at;
}
It should be noted that each Role
model object we get is automatically assigned a pivot
attribute, which represents a model object of the intermediate table and can be used like other Hyperf
models.
By default, the pivot
object contains only the primary keys of the two relational models. If you have additional fields in the intermediate table, you must specify them when defining the relation:
return $this->belongsToMany(Role::class)->withPivot('column1', 'column2');
If you want the intermediate table to automatically maintain the created_at
and updated_at
timestamps, then add the withTimestamps
method when defining the association:
return $this->belongsToMany(Role::class)->withTimestamps();
custom pivot
attribute name
As mentioned earlier, properties from intermediate tables can be accessed using the pivot
attribute. However, you are free to customize the name of this property to better reflect its use in your application.
For example, if your app includes users who may subscribe, there may be a many-to-many relationship between users and blogs. If this is the case, you may wish to name the intermediate table accessor subscription
instead of pivot
. This can be done using the as
method when defining the relationship:
return $this->belongsToMany(Podcast::class)->as('subscription')->withTimestamps();
Once defined, you can access the intermediate table data with a custom name:
$users = User::with('podcasts')->get();
foreach ($users->flatMap->podcasts as $podcast) {
echo $podcast->subscription->created_at;
}
Filter relations by intermediate table
When defining a relationship, you can also use the wherePivot
and wherePivotIn
methods to filter the results returned by belongsToMany
:
return $this->belongsToMany('App\Role')->wherePivot('approved', 1);
return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);
Preloading
When accessing a Hyperf
relationship as an attribute, the associated data is "lazy loaded". This means that the associated data is not actually loaded until the property is accessed for the first time. However, Hyperf
can "preload" child associations when querying the parent model. Eager loading can alleviate the N+1 query problem. To illustrate the N + 1 query problem, consider a User
model associated with a Role
:
<?php
declare(strict_types=1);
namespace App\Models;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
public function role()
{
return $this->hasOne(Role::class, 'user_id', 'id');
}
}
Now, let's get all users and their corresponding roles
$users = User::query()->get();
foreach ($users as $user){
echo $user->role->name;
}
This loop will execute a query to get all users, and then execute a query to get roles for each user. If we have 10 people, this loop will run 11 queries: 1 for users and 10 additional queries for roles.
Thankfully, we were able to squeeze the operation down to just 2 queries using eager loading. At query time, you can use the with method to specify which associations you want to preload:
$users = User::query()->with('role')->get();
foreach ($users as $user){
echo $user->role->name;
}
In this example, only two queries are executed
SELECT * FROM `user`;
SELECT * FROM `role` WHERE id in (1, 2, 3, ...);