Relationships
Relationship is the mechanism which associates tables by model and accesses values through properties.
Index
- Basic Relationship
- Performance
- Easy Relationship Definition
- Polymorphic Relationship
- The access modifiers of relationship properties
Basic Relationship
Let's learn how to define and use basic relations.
one-to-one
The table definitions used in this example are following:
-------------------------
customers table
int : id
string : name
int : group_id
string : phone
primarykey : id
index : group
-------------------------
addresses table
int : customer_id
string : zip
string : state
string : city
string : street
primarykey : customer_id
-------------------------
groups table
int : id
string : name
primarykey : id
-------------------------
Define Address
association on Customer
model:
clss Address extends Model
{
}
class Customer extends Model
{
public function address()
{
return $this->hasOne('App\Address');
}
}
Retrieving address.
$address = $customer->address;
if ($address !== null) {
echo $address->state;
}
Note: $customer->address
is a property ! When you access to address
property, the address
method will be called to get the object, and set it in the property. The code similar to following will be executed automatically:
function __get($name)
{
if ($name === 'address') {
$relation = $customer->address();
$model = $relation->get($this);
if ($model !== null) {
$customer->address = $model;
return $customer->address;
}
return null;
}
}
If the object was not found, it returns null
.
hasOne
The prototype of hasOne
method is:
hasOne(
string $className,
integer|string|string[] $foreignKey = null,
string[] $keyValuePropertyNames = null,
boolean $optimize = false
)
$className
Specify associated model class name, with complete name including namespaces.
$foreignKey
The default value of $foreignKey
is (model name in lowercase) + _id
. In this case, it is customer_id
. If the field name is not suitable, you can specify it directly:
public function address()
{
return $this->hasOne('App\Address', 'id');
}
What is really needed for searching in addresses
table is the index number. You can specify the number of the key.
public function address()
{
return $this->hasOne('App\Address', 0);
}
$keyValuePropertyNames
The properties which have same names as the names of primary key fields is used for searching. If you want to use different properties, you can specify them directly:
public function address()
{
return $this->hasOne('App\Address', 0, 'id');
}
one-to-many
The customer belongs to one of the groups. The model is following:
class Group extends Model
{
public function customers()
{
return $this->hasMany('App\Customer');
}
}
hasMany
Parameters of hasMany
are same as hasOne
.
Retrieving customers which belongs to the group ID 1.
$group = Group::find(1);
$customers = $group->customers;
echo count($customers);
If objects were not found, it returns an empty Collection
object.
belongs-to
The opposite of hasOne
and hasMany
is belongsTo
.
Note: belongsTo
method name must be different from property names in the Model.
clss Address extends Model
{
function customer()
{
return $this->belongsTo('App\Customer');
}
}
class Customer extends Model
{
// (omitted)
public function group()
{
return $this->belongsTo('App\Group');
}
}
belongsTo
The prototype of belongsTo
is:
belongsTo(
string $className,
string[] $keyValuePropertyNames = null,
integer|string|string[] $otherKey = 'id',
boolean $optimize = true
)
$keyValuePropertyNames
The property (model name in lowercase) + _id
is used for searching. If you want to use different properties, you can specify them directly:
public function group()
{
return $this->belongsTo('App\Group', 'group_id');
}
$otherKey
The default value of $otherKey
is id
. If you want to use different property names, you can specify them directly.
public function group()
{
return $this->belongsTo('App\Group', null, 'code');
}
Also, you can specify the number of the key.
public function group()
{
return $this->belongsTo('App\Group', null, 0);
}
Show group name the customer belongs to
$customer = Customer::find(1);
echo $customer->group->name;
many-to-many
Let's make it possible that multiple tags can be attached to a customer. Prepare tags in the tags
table.
The many-to-many relationship is defined with intermediate table. For this example, tag_customer
is an intermediate table.
The table definitions:
-------------------------
tags table
int : id
string : name
primarykey : id
-------------------------
tag_customer table
int : tag_id
int : customer_id
primarykey : tag_id, customer_id
index : customer_id, tag_id
-------------------------
The models are following:
class Tag extends Model
{
}
// Must define an intermediate class
class TagCustomer extends Model
{
// Converting table name to snake_case is not supported.
protected static $table = 'tag_customer';
}
class Customer extends Model
{
// (omitted)
public function tags()
{
return $this->belongsToMany('App\Tag');
}
}
belongsToMany
Let's look at omitted parameters.
belongsToMany(
string $className,
string $IntermediateClass = null,
integer|string|string[] $foreignKey = null,
string[] $intermediatePropertyNames = null
)
If you specify them explicitly, it becomes:
class Customer extends Model
{
// (omitted)
public function tags()
{
return $this->belongsToMany('App\Tag', 'App\TagCustomer', 'customer_id', 'tag_id');
}
}
Get the list of tags attached to a customer
$customer = Customer::find(1);
$tags = $customer->tags;
foreach($tags as $tag) {
// (omitted)
}
How was it handled? See the code below:
// This is a pseudocode.
class Customer extends Model
{
// (omitted)
public function tags()
{
$relation = $this->hasMany('App\TagCustomer', 'customer_id', 'id');
$tagCustomers = $relation->get($this);
$results = array();
$relation2 = TagCustomers::belongsTo('App\Tag', 'tag_id', 'id'); // static belongsTo actually does not exist.
foreach($tagCustomers as $row) {
$results[] = $relation2->get($row);
}
$this->tags = $results;
}
}
Get the list of customers from the tag
class Tag extends Model
{
public function customers()
{
return $this->belongsToMany('App\Customer');
}
}
$tag = Tag::find(1);
$customers = $tag->customers;
foreach($customers as $customer) {
// (omitted)
}
Performance
Lazy loading
Relationship methods access to the database for the first time when the property is read.
Eager loading
In general, there is no problem with lazy loading. However, if there are many objects, there is a problem of increasing the number of database accesses.
In this case, with
method can resolve the relation of objects in collection with a single access. Specify the method name of the relationships for with
.
$customers = Customer::with('group')->all();
echo $customer[0]->group->name; // No database access
with
is similar to algorithm of JOIN. In the Transactd PHP ORM, you can use two algorithms nested-loop
and sort-merge
. These two are used properly as follows:
- There are some duplication in the original values:
sort-merge
- The original values is unique:
nested-loop
You can choose algorithms by $optimize
parameter.
true
:sort-merge
false
:nested-loop
group_id
is often the same for multiple customers, so use sort-merge
.
public function group()
{
return $this->belongsTo('App\Group', 'group_id', 0, true /* sort-merge */);
}
The address differs for each customer, so use nested-loop
.
public function address()
{
return $this->hasOne('App\Address', 0, 'id', false /* nested-loop */);
}
$optimize
parameter only affect to with
method.
Note: with
method can not be used with findXX
or firstXX
methods. It is effective for retrieving models with get
or all
methods.
Whichever algorithm you select, the result is the same. Only performance is different.
Eager loading on Collection
The result Collection
can use Eager loading too.
Use loadRelations
to read objects associated with items in the collection at once.
$customers = Customer::all();
$customers->loadRelations(['address', 'group']); // load objects associated with address() and group()
Easy Relationship Definition
Above definitions are very similer to Laravel 5. This way is the best if there are tables that follows the rules of ActiveRecord.
However, relation
method is more simple way, which can be used regardless of the table definition rules. All of one-to-one, one-to-many, many-to-many and belongs-to relationships are substitutable with it.
public function address()
{
return $this->relation($className, $index, $keyValuePropertyNames, $optimize);
}
Any relationships can be defined with $className
, $index
and $keyValuePropertyNames
.
$className
is the name of the model class.$index
is the key number of$className
table for seraching.$keyValuePropertyNames
is property name(s) for values of key.
For belongsToMany
relationship, use addSubRelation
method like bellow:
class Tag extends Model
{
public function customers()
{
return $this->relation('App\TagCustomer', 0, 'id')
->addSubRelation('App\Customer', 0, 'customer_id');
}
}
Polymorphic Relationship
Polymorphic Relationship is an association from one table to multiple tables.
Polymorphic one-to-one
address
table stores not only address of customers, but also the address of vendors.
-------------------------
addresses table
string : owner_type
int : owner_id
string : zip
string : state
string : city
string : street
primarykey : owner_type, owner_id
-------------------------
vendors table
int : id
string : name
primarykey : id
-------------------------
class Vendor extends Model
{
public function address()
{
return $this->morphOne('App\Address', 'owner');
}
}
owner_type
field is holds class name of owner_id
with namespace.
You can use relation
method too.
class Vendor extends Model
{
public function address()
{
return $this->relation('App\Address', 0, ['[App\Vendor]', 'id']);
}
}
In this case, always use the fixed value App\Vendor
for owner_type
field. To specify a fixed value instead of a property name, enclose it with []
.
Definition of the opposite relationship
clss Address extends Model
{
function owner()
{
return $this->morphTo('owner');
}
// or
function owner()
{
// $className is unknown, so set to null.
return $this->relation(null, 0, ['owner_type', 'owner_id']);
}
}
Show owner names of addresses
$adresses = Address::all();
foreach($adresses as $address) {
echo $address->owner->name;
}
However, holding class names as string
type is not a usual way. Usually, classes are saved as integer
code. In this case, pass an array of class names to convert class codes to names.
// When type of 'owner_type' field in 'addresses' table is integer.
clss Address extends Model
{
protected static $classTypeMap = [1 => 'App\Vendor', 2 => 'App\Customer'];
function owner()
{
return $this->morphTo('owner', null, null, self::$classTypeMap);
}
// or
function owner()
{
// $className is unknown, so set to null.
return $this->relation(null, 0, ['owner_type', 'owner_id'])
->setMorphClassMap(self::$classTypeMap);
}
}
Polymorphic one-to-many
Let's make it possible that multiple addresses can be attached to customers or vendors.
-------------------------
addresses table
int : id
string : owner_type
int : owner_id
string : zip
string : state
string : city
string : street
primarykey : id
index : owner_type, owner_id (duplicatable)
-------------------------
class Customer extends Model
{
// (omitted)
public function address()
{
return $this->morphMany('App\Address', 'owner');
}
// or
public function address()
{
// The index numbers are different.
return $this->relation('App\Address', 1, ['[App\Customer]', 'id']);
}
}
The opposite relationship is the same as morphOne
.
Polymorphic many-to-many
Let's make it possible also that multiple tags can be attached to a vendor.
-------------------------
taggables table
string : taggable_type
int : taggable_id
int : tag_id
primarykey : taggable_type, taggable_id, tag_id
index : tag_id, taggable_type, taggable_id
-------------------------
class Taggable
{
}
class Tag extends Model
{
// Get all customers which have this tag.
public function customers()
{
return $this->morphedByMany('App\Customer', 'taggable', 'App\Taggable');
}
// Get all vendors which have this tag.
public function vendors()
{
return $this->morphedByMany('App\Vendor', 'taggable', 'App\Taggable');
}
// or
// Get all customers which have this tag.
public function customers()
{
return $this->relation('App\Taggable', 1, 'id')
->addSubRelation('App\Customer', 0, 'taggable_id');
}
// Get all vendors which have this tag.
public function vendors()
{
return $this->relation('App\Taggable', 1, 'id')
->addSubRelation('App\Vendor', 0, 'taggable_id');
}
}
The opposite relationship is:
class Customer extends Model
{
// (omitted)
function tags()
{
return $this->morphToMany('App\Tag', 'taggable', 'App\Taggable');
}
// or
function tags()
{
return $this->relation('App\Taggable', 0, ['[App\Customer]', 'id'])
->addSubRelation('App\Tag', 0, 'tag_id');
}
}
class Vendor extends Model
{
// (omitted)
function tags()
{
return $this->morphToMany('App\Tag', 'taggable', 'App\Taggable');
}
// or
function tags()
{
return $this->relation('App\Taggable', 0, ['[App\Customer]', 'id'])
->addSubRelation('App\Tag', 0, 'tag_id');
}
}
The access modifiers of relationship properties
The properties of the model used in the relation must be public
accessible. These propaties will be read and wrote from Relation
object. It can be used through getter or setter.
class Tag extends Model
{
protected $id; // It is OK, because getter/setter of it is public.
public function customers()
{
return $this->relation('App\TagCustomer', 0, 'id')
->addSubRelation('App\Customer', 0, 'customer_id');
}
public __get($name)
{
// The getter of 'id' must exist for relationship, because $this->id is protected.
if ($name === 'id') {
return $this->id;
}
return parent::__get($name); // Important for relationships.
}
public __set($name, $value)
{
// The setter of 'id' must exist for relationship, because $this->id is protected.
if ($name === 'id') {
$this->id = $value;
}
}
}
/* Followings must be 'public' for relationship:
* Tag::id
* TagCustomer::customer_id, tag_id (Fields of index number 1)
* Customer::id
*/