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

15 KiB

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 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.

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())

$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.

<% 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 'DataObject'. Ideally, the associated has_many (or belongs_to) should be specified with dot notation.

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"
    ];
}
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.

has_many

Defines 1-to-many joins. As you can see from the previous example, $has_many goes hand in hand with $has_one.

Please specify a $has_one-relationship on the related child-class as well, in order to have the necessary accessors available on both ends.
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 rather than the object.

$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:

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:

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.

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.

Please specify a $belongs_many_many-relationship on the related class as well, in order to have the necessary accessors available on both ends.

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 or ManyManyThroughList rather than the object.

$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.

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.
  • 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.

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.

$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. The joined record can be accessed via Join or TeamSupporter property (many_many through only)

<% 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:

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.

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 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 and ManyManyList, both of which provide very similar APIs, e.g. an add() and remove() method.

$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 for more info on the described relations.

use SilverStripe\ORM\DataObject;

class Team extends DataObject 
{
    private static $has_many = [
        "Players" => "Player"
    ];
    
    public function ActivePlayers() 
    {
        return $this->Players()->filter('Status', 'Active');
    }
}

Adding new records to a filtered `RelationList` like in the example above doesn't automatically set the filtered criteria on the added record.

Relations on Unsaved Objects

You can also set has_many and many_many relations before the DataObject is saved. This behavior uses the 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.

API Documentation