silverstripe-framework/docs/en/02_Developer_Guides/00_Model/02_Relations.md

642 lines
20 KiB
Markdown
Raw Normal View History

---
2014-10-27 04:40:02 +01:00
title: Relations between Records
2014-10-28 04:45:46 +01:00
summary: Relate models together using the ORM using has_one, has_many, and many_many.
icon: link
---
2014-10-27 04:40:02 +01:00
# 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
2014-10-27 04:40:02 +01:00
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
Many-to-1 and 1-to-1 relationships create a database-column called "`<relationship-name>`ID", in the example below this would be "TeamID" on the "Player"-table.
2014-10-27 04:40:02 +01:00
```php
use SilverStripe\ORM\DataObject;
2014-10-27 04:40:02 +01:00
class Player extends DataObject
{
private static $has_one = [
"Team" => Team::class,
];
}
class Team extends DataObject
{
private static $db = [
'Title' => 'Varchar'
];
2014-10-27 04:40:02 +01:00
private static $has_many = [
'Players' => Player::class,
];
}
```
2014-10-27 04:40:02 +01:00
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.
To create a has_one/has_many relationship to core classes (File, Image, etc), reference the Classname::class, like below.
```php
use SilverStripe\ORM\DataObject;
use SilverStripe\Assets\Image;
use SilverStripe\Assets\File;
class Team extends DataObject
{
private static $has_many = [
'Teamphoto' => Image::class,
'Lineup' => File::class
];
}
```
2014-10-28 04:45:46 +01:00
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);
2014-10-27 04:40:02 +01:00
$team = $player->Team();
// returns a 'Team' instance.
2014-10-27 04:40:02 +01:00
echo $player->Team()->Title;
// returns the 'Title' column on the 'Team' or `getTitle` if it exists.
```
2014-10-27 04:40:02 +01:00
The relationship can also be navigated in [templates](../templates).
```ss
<% with $Player %>
<% if $Team %>
Plays for $Team.Title
<% end_if %>
<% end_with %>
```
2014-10-27 04:40:02 +01:00
## 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;
2017-08-07 05:11:17 +02:00
class Player extends DataObject
{
private static $has_many = [
"Fans" => Fan::class.".FanOf",
];
}
class Team extends DataObject
{
private static $has_many = [
"Fans" => Fan::class.".FanOf",
];
}
2017-08-07 05:11:17 +02:00
// Type of object returned by $fan->FanOf() will vary
class Fan extends DataObject
{
2017-08-03 05:35:09 +02:00
// Generates columns FanOfID and FanOfClass
private static $has_one = [
"FanOf" => DataObject::class,
];
}
```
[warning]
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.
[/warning]
2014-10-27 04:40:02 +01:00
## has_many
2014-10-28 04:45:46 +01:00
Defines 1-to-many joins. As you can see from the previous example, `$has_many` goes hand in hand with `$has_one`.
2014-10-27 04:40:02 +01:00
[alert]
2014-10-27 04:40:02 +01:00
Please specify a $has_one-relationship on the related child-class as well, in order to have the necessary accessors
available on both ends. To add a $has_one-relationship on core classes, yml config settings can be used:
```yml
SilverStripe\Assets\Image:
has_one:
MyDataObject: MyDataObject
```
[/alert]
2014-10-27 04:40:02 +01:00
```php
use SilverStripe\ORM\DataObject;
2014-10-27 04:40:02 +01:00
class Team extends DataObject
{
private static $db = [
'Title' => 'Varchar',
];
2014-10-27 04:40:02 +01:00
private static $has_many = [
'Players' => Player::class,
];
}
class Player extends DataObject
{
2014-10-27 04:40:02 +01:00
private static $has_one = [
"Team" => Team::class,
];
}
```
2014-10-27 04:40:02 +01:00
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.
2014-10-27 04:40:02 +01:00
```php
$team = Team::get()->first();
2014-10-27 04:40:02 +01:00
echo $team->Players();
// [HasManyList]
2014-10-27 04:40:02 +01:00
echo $team->Players()->Count();
// returns '14';
2014-10-27 04:40:02 +01:00
foreach($team->Players() as $player) {
echo $player->FirstName;
}
```
2014-10-27 04:40:02 +01:00
To specify multiple `$has_many` to the same object you can use dot notation to distinguish them like below:
2014-10-27 04:40:02 +01:00
```php
use SilverStripe\ORM\DataObject;
2017-08-03 05:35:09 +02:00
class Person extends DataObject
{
private static $has_many = [
"Managing" => Company::class.".Manager",
"Cleaning" => Company::class.".Cleaner",
];
}
class Company extends DataObject
{
private static $has_one = [
"Manager" => Person::class,
"Cleaner" => Person::class,
];
}
```
2014-10-27 04:40:02 +01:00
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:
2017-08-07 05:11:17 +02:00
```php
public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->removeByName(['ManagerID', 'CleanerID']);
return $fields;
}
2017-08-07 05:11:17 +02:00
```
2014-10-27 04:40:02 +01:00
## 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
2014-10-27 04:40:02 +01:00
declaring the `$belongs_to`.
Similarly with `$has_many`, dot notation can be used to explicitly specify the `$has_one` which refers to this relation.
2014-10-27 04:40:02 +01:00
This is not mandatory unless the relationship would be otherwise ambiguous.
```php
use SilverStripe\ORM\DataObject;
2017-08-03 05:35:09 +02:00
class Team extends DataObject
{
private static $has_one = [
'Coach' => Coach::class
];
}
class Coach extends DataObject
{
private static $belongs_to = [
'Team' => Team::class.'.Coach'
];
}
```
2014-10-27 04:40:02 +01:00
## 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.
2014-10-27 04:40:02 +01:00
[warning]
Please specify a $belongs_many_many-relationship on the related class as well, in order
to have the necessary accessors available on both ends.
[/warning]
2014-10-27 04:40:02 +01:00
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;
2017-08-07 05:11:17 +02:00
class Team extends DataObject
{
private static $many_many = [
"Supporters" => Supporter::class,
];
private static $many_many_extraFields = [
2017-08-07 05:11:17 +02:00
'Supporters' => [
'Ranking' => 'Int'
2017-08-07 05:11:17 +02:00
]
];
}
2017-08-07 05:11:17 +02:00
class Supporter extends DataObject
{
private static $belongs_many_many = [
"Supports" => Team::class,
];
}
```
To ensure this `many_many` is sorted by "Ranking" by default you can add this to your config:
```yaml
Team_Supporters:
default_sort: '"Team_Supporter"."Ranking" ASC'
```
`Team_Supporters` is the table name automatically generated for the many_many relation in this case.
### 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.
Just like a any normal DataObject, you can apply a default sort which will be applied when
accessing many many through relations.
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;
2017-08-07 05:11:17 +02:00
class Team extends DataObject
{
private static $many_many = [
2017-08-07 05:11:17 +02:00
"Supporters" => [
'through' => TeamSupporter::class,
'from' => 'Team',
'to' => 'Supporter',
2017-08-07 05:11:17 +02:00
]
];
}
class Supporter extends DataObject
{
// Prior to 4.2.0, this also needs to include the reverse relation name via dot-notation
// i.e. 'Supports' => Team::class . '.Supporters'
private static $belongs_many_many = [
'Supports' => Team::class,
];
}
class TeamSupporter extends DataObject
{
private static $db = [
2017-08-07 05:11:17 +02:00
'Ranking' => 'Int',
];
private static $has_one = [
'Team' => Team::class,
'Supporter' => Supporter::class,
];
private static $default_sort = '"TeamSupporter"."Ranking" ASC';
}
```
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]);
```
2014-10-27 04:40:02 +01:00
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.
### Polymorphic many_many (Experimental)
Using many_many through, it is possible to support polymorphic relations on the mapping table.
Note, that this feature is currently experimental, and has certain limitations:
- This feature only works with many_many through
- This feature will only allow polymorphic many_many, but not belongs_many_many. However,
you can have a has_many relation to the mapping table on this side, and iterate through this
to collate parent records.
For instance, this is how you would link an arbitrary object to many_many tags.
```php
use SilverStripe\ORM\DataObject;
class SomeObject extends DataObject
{
// This same many_many may also exist on other classes
private static $many_many = [
"Tags" => [
'through' => TagMapping::class,
'from' => 'Parent',
'to' => 'Tag',
]
];
}
class Tag extends DataObject
{
// has_many works, but belongs_many_many will not
private static $has_many = [
'TagMappings' => TagMapping::class,
];
/**
* Example iterator placeholder for belongs_many_many.
* This is a list of arbitrary types of objects
* @return Generator|DataObject[]
*/
public function TaggedObjects()
{
foreach ($this->TagMappings() as $mapping) {
yield $mapping->Parent();
}
}
}
class TagMapping extends DataObject
{
private static $has_one = [
'Parent' => DataObject::class, // Polymorphic has_one
'Tag' => Tag::class,
];
}
```
### Using many_many in templates
2014-10-27 04:40:02 +01:00
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;
2017-08-07 05:11:17 +02:00
class Category extends DataObject
{
private static $many_many = [
'Products' => Product::class,
'FeaturedProducts' => Product::class,
];
}
2017-08-03 05:35:09 +02:00
class Product extends DataObject
{
private static $belongs_many_many = [
'Categories' => Category::class.'.Products',
'FeaturedInCategories' => Category::class.'.FeaturedProducts',
];
}
```
2014-10-28 04:45:46 +01:00
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.
2014-10-28 04:45:46 +01:00
2017-08-08 06:46:34 +02:00
## Cascading deletions
Relationships between objects can cause cascading deletions, if necessary, through configuration of the
`cascade_deletes` config on the parent class.
```php
2017-10-26 02:22:02 +02:00
use SilverStripe\ORM\DataObject;
2017-08-08 06:46:34 +02:00
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/model/versioning) for more information on ownership.
2017-08-08 06:46:34 +02:00
[alert]
Declaring cascade_deletes implies delete permissions on the listed objects.
Built-in controllers using delete operations check canDelete() on the owner, but not on the owned object.
[/alert]
## Cascading duplications
Similar to `cascade_deletes` there is also a `cascade_duplicates` config which works in much the same way.
When you invoke `$dataObject->duplicate()`, relation names specified by this config will be duplicated
and saved against the new clone object.
Note that duplications will act differently depending on the kind of relation:
- Exclusive relationships (e.g. has_many, belongs_to) will be explicitly duplicated.
- Non-exclusive many_many will not be duplicated, but the mapping table values will instead
be copied for this record.
- Non-exclusive has_one relationships are not normally necessary to duplicate, since both parent and clone
can normally share the same relation ID. However, if this is declared in `cascade_duplicates` any
has one will be similarly duplicated as though it were an exclusive relationship.
For example:
```php
use SilverStripe\ORM\DataObject;
class ParentObject extends DataObject {
private static $many_many = [
'RelatedChildren' => ChildObject::class,
];
private static $cascade_duplicates = [ 'RelatedChildren' ];
}
class ChildObject extends DataObject {
}
```
When duplicating objects you can disable recursive duplication by passing in `false` to the second
argument of duplicate().
E.g.
```php
$parent = ParentObject::get()->first();
$dupe = $parent->duplicate(true, false);
```
2014-10-27 04:40:02 +01:00
## 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()`
2014-10-27 04:40:02 +01:00
and `remove()` method.
```php
$team = Team::get()->byId(1);
2014-10-27 04:40:02 +01:00
// create a new supporter
$supporter = new Supporter();
$supporter->Name = "Foo";
$supporter->write();
2014-10-27 04:40:02 +01:00
// add the supporter.
$team->Supporters()->add($supporter);
```
2014-10-27 04:40:02 +01:00
## Custom Relations
You can use the ORM to get a filtered result list without writing any SQL. For example, this snippet gets you the
2014-10-27 04:40:02 +01:00
"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.
2014-10-27 04:40:02 +01:00
```php
use SilverStripe\ORM\DataObject;
2014-10-27 04:40:02 +01:00
class Team extends DataObject
{
private static $has_many = [
"Players" => Player::class
];
public function ActivePlayers()
{
return $this->Players()->filter('Status', 'Active');
2017-08-07 05:11:17 +02:00
}
}
2017-08-03 05:35:09 +02:00
```
2014-10-27 04:40:02 +01:00
[notice]
Adding new records to a filtered `RelationList` like in the example above doesn't automatically set the filtered
2014-10-27 04:40:02 +01:00
criteria on the added record.
[/notice]
2014-10-27 04:40:02 +01:00
## 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
2014-10-27 04:40:02 +01:00
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
2014-10-27 04:40:02 +01:00
for displaying the objects contained in the relation.
## Link Tracking
You can control the visibility of the `Link Tracking` tab by setting the `show_sitetree_link_tracking` config.
This defaults to `false` for most `DataObject`'s.
It is also possible to control the visibility of the `File Tracking` tab by setting the `show_file_link_tracking` config.
2017-11-27 04:39:17 +01:00
## 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)
2014-10-27 04:40:02 +01:00
## 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)
* [LinkTracking](api:SilverStripe\CMS\Model\SiteTreeLinkTracking)