mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-08-26 12:36:07 +02:00
496 lines
15 KiB
Markdown
496 lines
15 KiB
Markdown
title: Relations between Records
|
|
summary: Relate models together using the ORM using has_one, has_many, and many_many.
|
|
|
|
# Relations between Records
|
|
|
|
In most situations you will likely see more than one [DataObject](api:SilverStripe\ORM\DataObject) and several classes in your data model may relate
|
|
to one another. An example of this is a `Player` object may have a relationship to one or more `Team` or `Coach` classes
|
|
and could take part in many `Games`. Relations are a key part of designing and building a good data model.
|
|
|
|
Relations are built through static array definitions on a class, in the format `<relationship-name> => <classname>`.
|
|
SilverStripe supports a number of relationship types and each relationship type can have any number of relations.
|
|
|
|
## has_one
|
|
|
|
A 1-to-1 relation creates a database-column called "`<relationship-name>`ID", in the example below this would be
|
|
"TeamID" on the "Player"-table.
|
|
|
|
```php
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
class Team extends DataObject
|
|
{
|
|
private static $db = [
|
|
'Title' => 'Varchar'
|
|
];
|
|
|
|
private static $has_many = [
|
|
'Players' => 'Player'
|
|
];
|
|
}
|
|
class Player extends DataObject
|
|
{
|
|
private static $has_one = [
|
|
"Team" => "Team",
|
|
];
|
|
}
|
|
```
|
|
|
|
This defines a relationship called `Team` which links to a `Team` class. The `ORM` handles navigating the relationship
|
|
and provides a short syntax for accessing the related object.
|
|
|
|
At the database level, the `has_one` creates a `TeamID` field on `Player`. A `has_many` field does not impose any database changes. It merely injects a new method into the class to access the related records (in this case, `Players()`)
|
|
|
|
```php
|
|
$player = Player::get()->byId(1);
|
|
|
|
$team = $player->Team();
|
|
// returns a 'Team' instance.
|
|
|
|
echo $player->Team()->Title;
|
|
// returns the 'Title' column on the 'Team' or `getTitle` if it exists.
|
|
```
|
|
|
|
The relationship can also be navigated in [templates](../templates).
|
|
|
|
```ss
|
|
<% with $Player %>
|
|
<% if $Team %>
|
|
Plays for $Team.Title
|
|
<% end_if %>
|
|
<% end_with %>
|
|
```
|
|
|
|
## Polymorphic has_one
|
|
|
|
A has_one can also be polymorphic, which allows any type of object to be associated.
|
|
This is useful where there could be many use cases for a particular data structure.
|
|
|
|
An additional column is created called "`<relationship-name>`Class", which along
|
|
with the ID column identifies the object.
|
|
|
|
To specify that a has_one relation is polymorphic set the type to [api:SilverStripe\ORM\DataObject]
|
|
Ideally, the associated has_many (or belongs_to) should be specified with dot notation.
|
|
|
|
```php
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
class Player extends DataObject
|
|
{
|
|
private static $has_many = [
|
|
"Fans" => "Fan.FanOf"
|
|
];
|
|
}
|
|
class Team extends DataObject
|
|
{
|
|
private static $has_many = [
|
|
"Fans" => "Fan.FanOf"
|
|
];
|
|
}
|
|
|
|
// Type of object returned by $fan->FanOf() will vary
|
|
class Fan extends DataObject
|
|
{
|
|
|
|
// Generates columns FanOfID and FanOfClass
|
|
private static $has_one = [
|
|
"FanOf" => DataObject::class,
|
|
];
|
|
}
|
|
```
|
|
|
|
<div class="warning" markdown='1'>
|
|
Note: The use of polymorphic relationships can affect query performance, especially
|
|
on joins, and also increases the complexity of the database and necessary user code.
|
|
They should be used sparingly, and only where additional complexity would otherwise
|
|
be necessary. E.g. Additional parent classes for each respective relationship, or
|
|
duplication of code.
|
|
</div>
|
|
|
|
## has_many
|
|
|
|
Defines 1-to-many joins. As you can see from the previous example, `$has_many` goes hand in hand with `$has_one`.
|
|
|
|
<div class="alert" markdown='1'>
|
|
Please specify a $has_one-relationship on the related child-class as well, in order to have the necessary accessors
|
|
available on both ends.
|
|
</div>
|
|
|
|
```php
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
class Team extends DataObject
|
|
{
|
|
private static $db = [
|
|
'Title' => 'Varchar'
|
|
];
|
|
|
|
private static $has_many = [
|
|
'Players' => 'Player'
|
|
];
|
|
}
|
|
class Player extends DataObject
|
|
{
|
|
|
|
private static $has_one = [
|
|
"Team" => "Team",
|
|
];
|
|
}
|
|
```
|
|
|
|
Much like the `has_one` relationship, `has_many` can be navigated through the `ORM` as well. The only difference being
|
|
you will get an instance of [HasManyList](api:SilverStripe\ORM\HasManyList) rather than the object.
|
|
|
|
```php
|
|
$team = Team::get()->first();
|
|
|
|
echo $team->Players();
|
|
// [HasManyList]
|
|
|
|
echo $team->Players()->Count();
|
|
// returns '14';
|
|
|
|
foreach($team->Players() as $player) {
|
|
echo $player->FirstName;
|
|
}
|
|
```
|
|
|
|
To specify multiple `$has_many` to the same object you can use dot notation to distinguish them like below:
|
|
|
|
```php
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
class Person extends DataObject
|
|
{
|
|
private static $has_many = [
|
|
"Managing" => "Company.Manager",
|
|
"Cleaning" => "Company.Cleaner",
|
|
];
|
|
}
|
|
class Company extends DataObject
|
|
{
|
|
private static $has_one = [
|
|
"Manager" => "Person",
|
|
"Cleaner" => "Person"
|
|
];
|
|
}
|
|
```
|
|
|
|
Multiple `$has_one` relationships are okay if they aren't linking to the same object type. Otherwise, they have to be
|
|
named.
|
|
|
|
If you're using the default scaffolded form fields with multiple `has_one` relationships, you will end up with a CMS field for each relation. If you don't want these you can remove them by their IDs:
|
|
|
|
```php
|
|
public function getCMSFields()
|
|
{
|
|
$fields = parent::getCMSFields();
|
|
$fields->removeByName(array('ManagerID', 'CleanerID'));
|
|
return $fields;
|
|
}
|
|
```
|
|
|
|
## belongs_to
|
|
|
|
Defines a 1-to-1 relationship with another object, which declares the other end of the relationship with a
|
|
corresponding `$has_one`. A single database column named `<relationship-name>ID` will be created in the object with the
|
|
`$has_one`, but the $belongs_to by itself will not create a database field. This field will hold the ID of the object
|
|
declaring the `$belongs_to`.
|
|
|
|
Similarly with `$has_many`, dot notation can be used to explicitly specify the `$has_one` which refers to this relation.
|
|
This is not mandatory unless the relationship would be otherwise ambiguous.
|
|
|
|
```php
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
class Team extends DataObject
|
|
{
|
|
|
|
private static $has_one = [
|
|
'Coach' => 'Coach'
|
|
];
|
|
}
|
|
class Coach extends DataObject
|
|
{
|
|
|
|
private static $belongs_to = [
|
|
'Team' => 'Team.Coach'
|
|
];
|
|
}
|
|
```
|
|
|
|
## many_many
|
|
|
|
Defines many-to-many joins, which uses a third table created between the two to join pairs.
|
|
There are two ways in which this can be declared, which are described below, depending on
|
|
how the developer wishes to manage this join table.
|
|
|
|
<div class="warning" markdown='1'>
|
|
Please specify a $belongs_many_many-relationship on the related class as well, in order
|
|
to have the necessary accessors available on both ends.
|
|
</div>
|
|
|
|
Much like the `has_one` relationship, `many_many` can be navigated through the `ORM` as well.
|
|
The only difference being you will get an instance of [ManyManyList](api:SilverStripe\ORM\ManyManyList) or
|
|
[ManyManyThroughList](api:SilverStripe\ORM\ManyManyThroughList) rather than the object.
|
|
|
|
```php
|
|
$team = Team::get()->byId(1);
|
|
|
|
$supporters = $team->Supporters();
|
|
// returns a 'ManyManyList' instance.
|
|
```
|
|
|
|
### Automatic many_many table
|
|
|
|
If you specify only a single class as the other side of the many-many relationship, then a
|
|
table will be automatically created between the two (this-class)_(relationship-name), will
|
|
be created with a pair of ID fields.
|
|
|
|
Extra fields on the mapping table can be created by declaring a `many_many_extraFields`
|
|
config to add extra columns.
|
|
|
|
```php
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
class Team extends DataObject
|
|
{
|
|
private static $many_many = [
|
|
"Supporters" => "Supporter",
|
|
];
|
|
|
|
private static $many_many_extraFields = [
|
|
'Supporters' => [
|
|
'Ranking' => 'Int'
|
|
]
|
|
];
|
|
}
|
|
|
|
class Supporter extends DataObject
|
|
{
|
|
private static $belongs_many_many = [
|
|
"Supports" => "Team",
|
|
];
|
|
}
|
|
```
|
|
|
|
### many_many through relationship joined on a separate DataObject
|
|
|
|
If necessary, a third DataObject class can instead be specified as the joining table,
|
|
rather than having the ORM generate an automatically scaffolded table. This has the following
|
|
advantages:
|
|
|
|
- Allows versioning of the mapping table, including support for the
|
|
[ownership api](/developer_guides/model/versioning).
|
|
- Allows support of other extensions on the mapping table (e.g. subsites).
|
|
- Extra fields can be managed separately to the joined dataobject, even via a separate
|
|
GridField or form.
|
|
|
|
This is declared via array syntax, with the following keys on the many_many:
|
|
- `through` Class name of the mapping table
|
|
- `from` Name of the has_one relationship pointing back at the object declaring many_many
|
|
- `to` Name of the has_one relationship pointing to the object declaring belongs_many_many.
|
|
|
|
Note: The `through` class must not also be the name of any field or relation on the parent
|
|
or child record.
|
|
|
|
The syntax for `belongs_many_many` is unchanged.
|
|
|
|
```php
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
class Team extends DataObject
|
|
{
|
|
private static $many_many = [
|
|
"Supporters" => [
|
|
'through' => 'TeamSupporter',
|
|
'from' => 'Team',
|
|
'to' => 'Supporter',
|
|
]
|
|
];
|
|
}
|
|
class Supporter extends DataObject
|
|
{
|
|
private static $belongs_many_many = [
|
|
"Supports" => "Team",
|
|
];
|
|
}
|
|
class TeamSupporter extends DataObject
|
|
{
|
|
private static $db = [
|
|
'Ranking' => 'Int',
|
|
];
|
|
|
|
private static $has_one = [
|
|
'Team' => 'Team',
|
|
'Supporter' => 'Supporter',
|
|
];
|
|
}
|
|
```
|
|
|
|
In order to filter on the join table during queries, you can use the class name of the joining table
|
|
for any sql conditions.
|
|
|
|
```php
|
|
$team = Team::get()->byId(1);
|
|
$supporters = $team->Supporters()->where(['"TeamSupporter"."Ranking"' => 1]);
|
|
```
|
|
|
|
Note: ->filter() currently does not support joined fields natively due to the fact that the
|
|
query for the join table is isolated from the outer query controlled by DataList.
|
|
|
|
|
|
### Using many_many in templates
|
|
|
|
The relationship can also be navigated in [templates](../templates).
|
|
The joined record can be accessed via `Join` or `TeamSupporter` property (many_many through only)
|
|
|
|
```ss
|
|
<% with $Supporter %>
|
|
<% loop $Supports %>
|
|
Supports $Title <% if $TeamSupporter %>(rank $TeamSupporter.Ranking)<% end_if %>
|
|
<% end_if %>
|
|
<% end_with %>
|
|
```
|
|
|
|
You can also use `$Join` in place of the join class alias (`$TeamSupporter`), if your template
|
|
is class-agnostic and doesn't know the type of the join table.
|
|
|
|
## belongs_many_many
|
|
|
|
The belongs_many_many represents the other side of the relationship on the target data class.
|
|
When using either a basic many_many or a many_many through, the syntax for belongs_many_many is the same.
|
|
|
|
To specify multiple $many_manys between the same classes, specify use the dot notation to
|
|
distinguish them like below:
|
|
|
|
|
|
```php
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
class Category extends DataObject
|
|
{
|
|
|
|
private static $many_many = [
|
|
'Products' => 'Product',
|
|
'FeaturedProducts' => 'Product'
|
|
];
|
|
}
|
|
|
|
class Product extends DataObject
|
|
{
|
|
private static $belongs_many_many = [
|
|
'Categories' => 'Category.Products',
|
|
'FeaturedInCategories' => 'Category.FeaturedProducts'
|
|
];
|
|
}
|
|
```
|
|
|
|
If you're unsure about whether an object should take on `many_many` or `belongs_many_many`,
|
|
the best way to think about it is that the object where the relationship will be edited
|
|
(i.e. via checkboxes) should contain the `many_many`. For instance, in a `many_many` of
|
|
Product => Categories, the `Product` should contain the `many_many`, because it is much
|
|
more likely that the user will select Categories for a Product than vice-versa.
|
|
|
|
## Cascading deletions
|
|
|
|
Relationships between objects can cause cascading deletions, if necessary, through configuration of the
|
|
`cascade_deletes` config on the parent class.
|
|
|
|
```php
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
class ParentObject extends DataObject {
|
|
private static $has_one = [
|
|
'Child' => ChildObject::class,
|
|
];
|
|
private static $cascade_deletes = [
|
|
'Child',
|
|
];
|
|
}
|
|
class ChildObject extends DataObject {
|
|
}
|
|
```
|
|
|
|
In this example, when the Parent object is deleted, the Child specified by the has_one relation will also
|
|
be deleted. Note that all relation types (has_many, many_many, belongs_many_many, belongs_to, and has_one)
|
|
are supported, as are methods that return lists of objects but do not correspond to a physical database relation.
|
|
|
|
If your object is versioned, cascade_deletes will also act as "cascade unpublish", such that any unpublish
|
|
on a parent object will trigger unpublish on the child, similarly to how `owns` causes triggered publishing.
|
|
See the [versioning docs](/developer_guides/versioning) for more information on ownership.
|
|
|
|
## Adding relations
|
|
|
|
Adding new items to a relations works the same, regardless if you're editing a **has_many** or a **many_many**. They are
|
|
encapsulated by [HasManyList](api:SilverStripe\ORM\HasManyList) and [ManyManyList](api:SilverStripe\ORM\ManyManyList), both of which provide very similar APIs, e.g. an `add()`
|
|
and `remove()` method.
|
|
|
|
```php
|
|
$team = Team::get()->byId(1);
|
|
|
|
// create a new supporter
|
|
$supporter = new Supporter();
|
|
$supporter->Name = "Foo";
|
|
$supporter->write();
|
|
|
|
// add the supporter.
|
|
$team->Supporters()->add($supporter);
|
|
```
|
|
|
|
## Custom Relations
|
|
|
|
You can use the ORM to get a filtered result list without writing any SQL. For example, this snippet gets you the
|
|
"Players"-relation on a team, but only containing active players.
|
|
|
|
See [DataObject::$has_many](api:SilverStripe\ORM\DataObject::$has_many) for more info on the described relations.
|
|
|
|
```php
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
class Team extends DataObject
|
|
{
|
|
private static $has_many = [
|
|
"Players" => "Player"
|
|
];
|
|
|
|
public function ActivePlayers()
|
|
{
|
|
return $this->Players()->filter('Status', 'Active');
|
|
}
|
|
}
|
|
|
|
```
|
|
|
|
<div class="notice" markdown="1">
|
|
Adding new records to a filtered `RelationList` like in the example above doesn't automatically set the filtered
|
|
criteria on the added record.
|
|
</div>
|
|
|
|
## Relations on Unsaved Objects
|
|
|
|
You can also set *has_many* and *many_many* relations before the `DataObject` is saved. This behavior uses the
|
|
[UnsavedRelationList](api:SilverStripe\ORM\UnsavedRelationList) and converts it into the correct `RelationList` when saving the `DataObject` for the first
|
|
time.
|
|
|
|
This unsaved lists will also recursively save any unsaved objects that they contain.
|
|
|
|
As these lists are not backed by the database, most of the filtering methods on `DataList` cannot be used on a list of
|
|
this type. As such, an `UnsavedRelationList` should only be used for setting a relation before saving an object, not
|
|
for displaying the objects contained in the relation.
|
|
|
|
## Related Lessons
|
|
* [Working with data relationships -- has_many](https://www.silverstripe.org/learn/lessons/v4/working-with-data-relationships-has-many-1)
|
|
* [Working with data relationships -- many_many](https://www.silverstripe.org/learn/lessons/v4/working-with-data-relationships-many-many-1)
|
|
|
|
## Related Documentation
|
|
|
|
* [Introduction to the Data Model and ORM](data_model_and_orm)
|
|
* [Lists](lists)
|
|
|
|
## API Documentation
|
|
|
|
* [HasManyList](api:SilverStripe\ORM\HasManyList)
|
|
* [ManyManyList](api:SilverStripe\ORM\ManyManyList)
|
|
* [DataObject](api:SilverStripe\ORM\DataObject)
|