Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

There are a number of relationships that be defined, which will be explored in more detail on this page. These are:

  • has_one: A one-to-one link between two tables

  • has_many: A one-to-many link between tables

  • has_many_through: Connects two entities which don't have a direct reference, but are referenced using an intermediate table (relates many entities)

  • has_one_through: Connects two entities which don't have a direct reference, but are referenced using an intermediate table (relates one entity)

  • belongs_to: The inverse relation of has_one and has_many

has_one

has_one relation defines a one-to-one link between two tables. It's very similar to has_many however it expects only one related model to exist (it can be more than one, it would however return you only the first one). In Totara we don't have many examples of true one-to-one relationships, perhaps an example of that would be custom field tables: info_data table has one corresponding entry in info_data_param. To define a has one relation we'd use the following (for example the first table with custom fields is used):

...

Has_one - definition
Code Block
linenumberslanguagetruephp
class comp_type_info_data extends entity {

    public const TABLE = 'comp_type_info_data';

    /**
     * Param
     *
     * @return has_one
     */
    public function param(): has_one {
		// This method accepts another argument - key, which defaults to id and will be true in most cases.
        return $this->has_one(comp_type_info_data_param::class, 'dataid'); 
    }
}

class comp_type_info_data_param extends entity {
    public const TABLE = 'comp_type_info_data_param';
}

You can name your relation the way you want it, however it is recommended to name it meaningfully.

title
Warning

Naming clash

You should avoid naming clashes when defining your relation. You won't be able to name it using a name of a method that already exists on the model, and you shouldn't make a name of a relation matching a property that already exists on the entity. For example, if your entity has a property type (underlying table has a field type) you shouldn't name a relation type as it will cause warnings to appear. Considering a real-life application it is possible, however improbable. This is due to inconsistencies in naming that we have, for example:

Activity belongs to a course and logically you would name the corresponding relation course. In our case the column representing a key might be named course as well, this is quite rare though as usually the column would be named course_id so it wouldn't cause problems.

To query your relationship on an entity you might use the following syntax:

...

Has_one - query
Code Block
linenumberslanguagetruephp
$entity = comp_type_info_data::repository()
			->order_by('id')
			->first();

// This will load the related entity - or null if it does not exist
$entity->param()->one(); 

Alternatively, you can use the property syntax:

...

Has_one - query
linenumbers
Code Block
languagetruephp
$entity = comp_type_info_data::repository()
			->order_by('id')
			->first();

// This will load the related entity if it hasn't been and will cache the results on the model.
$entity->param; 

Example

Consider an example with the following:

  • An entity named client (the main entity)

  • A model named client

  • An entity named client_settings (the related entity)

  • A model named client_settings

The entity and model need to be set up.

We want, when we have a main model (client), to be able to fetch its related client_settings model, e.g. by calling:

$client_settings_model_related = $model_client->client_settings;

 

 

Entity class changes: 

client Entity class (main entity) 

Add a method to say the main entity has a 1one-on-1 one relationship to the other entity: 

Code Block
/**

...



 * @return has_one

...



 */

...



public function client_settings(): has_one {

...



    return $this->has_one(client_settings::class, 'client_id');

...



}

 

Add an annotation above the entity class, setting a property to access for the related entity: 

Code Block
/*

...


...

...



 @property-read client_settings $client_settings

...



 */

...



class client extends entity {

...


...

 

 

client_settings Entity class (related entity)

 

Add a method to say the related entity has a 1one-on-1 one relationship to the main entity: 

Code Block
/**

...



 * @return belongs_to

...



 */

...



public function client(): belongs_to {

...



    return $this->belongs_to(client::class, 'id');

...



}

...

 

 

client Model class (main model)

 

Add an annotation above the model class for the related model: 

Code Block
* @property-read client_settings_model $client_settings

...



 */

...



class client extends model {

...



...

...

 



Add the related model to the accessor whitelist

...



protected $model_accessor_whitelist = [

...



    'tenant_entity',

...



    ...

...



    'client_settings'

...



];

...

 



For the ORM, you will need to add a method named like "get_[accessor property]"

...

 



public function get_client_settings(): client_settings_model {

...



    return client_settings_model::load_by_entity($this->entity->client_settings);

...



}

...

In the model, when saving the related entity on the main entity, you can save it like this and it will automatically fill in the related entity's ID (the foreign key): 

Code Block
$client_settings_entity  = new client_settings();

...



$client_settings_entity->time_created = time();

...



// Add a 1-on-1 relationship from client to client_settings

...



$entity->client_settings()->save($client_settings_entity);

...

Test 

For a test, you can check you can access the related entity . After after you have created a main entity with a relation on it, e.g.: 

Code Block
$name = 'test';

...



$description = 'description_test';

...



$model_client = client_model::create($name, $description); // the main model will automatically save a related entity (see above)

...

 



// Get the related model

...



$client_settings_model_related = $model_client->client_settings;

...

 



$this->assertEquals($model_client->id, $client_settings_model_related->client_id);

has_many

has_many relation defines a one-to-many link between tables. It behaves in a similar way to as one, however returns a collection of related models. Examples of tables related using has_many relations are everywhere: a course has many course modules, a course has many completions, a user has many course completions, a competency framework has many competencies, a competency may have many child competencies etc. For example we are going to use courses and course modules. To define the relation we'd need to do the following:

...

Has_many - definition
linenumbers
Code Block
languagetruephp
class course extends entity {

    public const TABLE = 'course';

    /**
     * Course modules
     *
     * @return has_many
     */
    public function modules(): has_many {
		// This method accepts another argument - key, which defaults to id and will be true in most cases.
        return $this->has_many(course_module::class, 'course'); 
    }
}

class course_module extends entity {
    public const TABLE = 'course_modules';
}

Essentially, the same naming considerations apply.

Info
title

Plural name

Also note that it makes sense to pluralise the name of your has_many relation methods. E.g. courses have modules - so the relation would be modules().

Code Block
languagephp
title


Has_many - query
Code Block
linenumberslanguagetruephp
$course = course::repository()
			->order_by('id')
			->first();

// This will load and return all the related course modules
$modules = $course->modules()->get(); 
// Or using the property access
$modules = $course->modules;
// SQL: SELECT * FROM {course_modules} "course_modules" where "course_modules".course = ?, [$course->id]

Worth noting that you can apply additional constraints when defining the relation:

...

...

Has_many - extra definition constraints
linenumbers
Code Block
languagetruephp
class course extends entity {

    public const TABLE = 'course';

    /**
     * Course
     *
     * @return has_many
     */
    public function active_modules(): has_many {
        return $this->has_many(course_module::class, 'course')
					->where('active', 1); // In addition to key filtering, where condition will be added as well.
    }
}


title
Warning

Or aggregated conditions considerations

Be careful adding or_where conditions as it might lead to logical problems, since all the constraints are just appended, or you will need to wrap it in a nested condition to preserve filtering related entities using the key correctly.

...

has_many_through relation connects two entities which don't have a direct reference, but are referenced using an intermediate table. It behaves similarly to has_many, however when relation is queried it joins an intermediate table which is used to connect these. Examples of tables related using has_many_through relation include competency and scale values (which are connected via scale assignment table).

...

Has_many_through - definition
linenumbers
Code Block
languagetruephp
class competency extends entity {

    public const TABLE = 'comp';

    /**
     * Scale values for this competency
     *
     * @return has_many_through
     */
    public function values(): has_many_through {
		// the arguments follow the path from the current one,
 		// through an intermediate table to the target table.
		// current -> intermediate -> target
        return $this->has_many_through(
            competency_scale_assignment::class,  // the intermediate entity class name
            competency_scale_value::class,       // the target entity class name
            'frameworkid',						 // the foreign key in the current table to the intermediate table
            'frameworkid',						 // the key to link to in the intermediate table
            'scaleid', 							 // the foreign key in the intermediate table to the target table
			'scaleid'							 // the key in the target table
		);
    }
}

class competency_scale_value extends entity {
    public const TABLE = 'comp_scale_values';
}

class competency_scale_assignment extends entity {
    public const TABLE = 'comp_scale_assignments';
}

To query your relationship on an entity you might use the following syntax:

...

Has_many_through - query
Code Block
linenumberslanguagetruephp
$competency = competency::repository()
			->order_by('id')
			->first();

$competency->values;
// SQL: SELECT * FROM {comp_scale_values} "scale_values" JOIN {comp_scale_assignments} "scale_assignments" ON "scale_values".scaleid = "scale_assignments".scaleid where "scale_assignments".frameworkid = ?, [$competency->frameworkid]

All other things relative to has_many relation can be applied to has_many_through relations.

Warning
title

Limitations

has_many_through relation implementation has the following limitations:

  • Saving related models using relations isn't supported

  • Querying specific columns at eager-loading is not supported

has_one_through

has_one_through relation connects two entities which don't have a direct reference, but are referenced using an intermediate table. It works like has_many_through but only relates one entity instead of many. Examples of tables related using has_one_through relation include competency and scales (which are connected via scale assignment table).

...

...

Has_many - definition
linenumbers
Code Block
languagetruephp
class competency extends entity {

    public const TABLE = 'comp';

    /**
     * Scale for this competency
     *
     * @return has_one_through
     */
    public function scale(): has_one_through {
		// the arguments follow the path from the current one,
 		// through an intermediate table to the target table.
		// current -> intermediate -> target
        return $this->has_one_through(
            competency_scale_assignment::class,  // the intermediate entity class name
            scale::class,       				 // the target entity class name
            'frameworkid',						 // the foreign key in the current table to the intermediate table
            'frameworkid',						 // the key to link to in the intermediate table
            'scaleid', 							 // the foreign key in the intermediate table to the target table
			'id'								 // the key in the target table
		);
    }
}

class competency_scale extends entity {
    public const TABLE = 'comp_scale';
}

class competency_scale_assignment extends entity {
    public const TABLE = 'comp_scale_assignments';
}

To query your relationship on an entity you might use the following syntax:

...

Has_many - query
linenumbers
Code Block
languagetruephp
$competency = competency::repository()
			->order_by('id')
			->first();

$course->scale; 
// SQL: SELECT * FROM {comp_scale} "scale" JOIN {comp_scale_assignments} "scale_assignments" ON "scale".id = "scale_assignments".scaleid where "scale_assignments".frameworkid = ?, [$competency->frameworkid]

All other things relative to has_one relation can be applied to has_one_through relations.

Warning
title

Limitations

has_one_through relation implementation has the following limitations:

  • Saving related models using relations isn't supported

  • Querying specific columns at eager-loading is not supported

belongs_to

belongs_to is the inverse relation of has_one and has_many. The indication of the fact that it is a belongs_to relation is when you define your foreign key right on your entity. For example a course_module belongs to a course as course_modules table has a course field that references a course table. Let's use the examples from the above to define belongs to relation.

...

Belongs_to (has many inverse) - definition
Code Block
linenumberslanguagetruephp
class course extends entity {

    public const TABLE = 'course';

    /**
     * Course modules
     *
     * @return has_many
     */
    public function modules(): has_many {
		// This method accepts another argument - key, which defaults to id and will be true in most cases.
        return $this->has_many(course_module::class, 'course'); 
    }
}

class course_module extends entity {
    public const TABLE = 'course_modules';

	/**
     * A course entity
     *
     *
     * @return belongs_to
     */
	public function a_course(): belongs_to {
        // This is the example where the name of the foreign key will match the most logical relation name, so we call it a_course instead of course.
		// Belongs to takes key as another argument which we omit here as in the majority of the circumstances it will
        return $this->belongs_to(course::class, 'course'); 
    }
}


title
Info

Note on saving related models

You cannot insert related models for belongs_to relationship, however it's still possible to update them.

The inverse of the has_one model will be exactly the same. Querying belongs_to relationship is exactly the same as well as querying has_one/has_many. You have options to access it using a method or a property which will cache the result for subsequent calls.

title
Info

Extra conditions

Even though it's possible to add extra conditions on the related model repository, it may not make practical sense, as applying foreign key constraint should already narrow it down to exactly one record.

...

Example:

Code Block
linenumbers
languagephptrue
$course = course::repository()
			->order_by('id')
			->first();

// This is a collection of module entities filtered down on the database level
$modules = $course->modules()
   ->where('name', 'module name')
   ->get();


Info
title

Interact with a relation method as it's a repository

Also using method relation, you don't have to get a collection at the end, you might use count or recordset for example, as well as specify columns you are selecting.

...

Also if you use eager loading (see next section) accessing the related entities via the property makes most sense as the entities will already be cached.

Code Block
languagephplinenumberstrue
$course = course::repository()
			->order_by('id')
			->first();

// This is a collection of module entities which is cached.
$modules = $course->modules;

...

To eager load related models you can use with method on the repository when querying relations. The advantage of eager loading is that generally it solves n+1 problem comparing to you querying a related model on a collection of items. For example, imagine you want to load courses with their respective course modules:

...

Lazy loading vs eager loading
Code Block
linenumberslanguagetruephp
// ...
$courses = course::repository()->get();

foreach ($courses as $course) {
    // You want to get each course module here
    foreach ($courses->modules as $cm) {
        // Do something...
    }
}

// Running the above code will result in having an n+1 problem as it would first execute a query to get all the courses and then in the loop it will run a query to fetch each course module.
// SELECT * FROM {course} course;
// SELECT * FROM {course_modules} course_modules WHERE course = ? [$course->id]
// ... repeat it as many times as many courses you have

To avoid the issue described above you should eager load your relation. To eager load a relation you need to use with.

...

...

Lazy loading vs eager loading
linenumbers
Code Block
languagetruephp
// You specify the relation name you want to loadit is also possible to specify multiple relations to load if you supply an array of relation names
$courses = course::repository()
    ->with('modules')  // multiple: ->with(['relation_a', 'relation_b']) or just  ->with('relation_a')->with('relation_b') 
    ->get();

foreach ($courses as $course) {
    // You want to get each course module here
    foreach($courses->modules as $cm) {
        // Do something...
    }
}

// Running the above code will result in executing two queries only:
// When the collection will be loaded, before the result is returned to the user, an extra query will run.
// SELECT * FROM {course} course;
// SELECT * FROM {course_modules} course_modules WHERE course IN (?,?,?,...) [array_column($courses, 'id')]

// After the query to fetch all the related entities has ran, it will automatically inject the correct entities into the main collection
// So by the time you are iterating over courses your modules will be there already and will not trigger extra database queries.

It is also possible to specify multiple relations to load if you supply an array of relation names or call with multiple times:

...

Lazy loading vs eager loading
Code Block
linenumberslanguagetruephp
// Array syntax
$courses = course::repository()
    ->with(['relation_a', 'relation_b'])   
    ->get();

// Fluent interface
$courses = course::repository()
    ->with('relation_a')
    ->with('relation_b')
    ->get();

A lot of methods that make sense allow you to eager load the relations, including: first, first_or_fail, one, get, paginate and load_more.

title
Info

Eager loading on recordsets

Currently eager loading relations on recordsets is not supported, due to the fact that we'd have to know all the ids in advance. Alternatively to save memory, eager loading works fine with results pagination. Related models, however, are not paginated.

As an alternative for specifying with on the repository you may define on the entity itself to always eager load certain relations.

...

Belongs to (has many inverse) - definition
Code Block
linenumberslanguagetruephp
class course extends entity {

    public const TABLE = 'course';

    /**
     * List of always eager-loaded relations.
     *
     * @var string[]
     */
    protected $with = [
        'modules'
    ];

    /**
     * Course modules
     *
     * @return has_many
     */
    public function modules(): has_many {
        return $this->has_many(course_module::class, 'course');
    }
}

...

Sometimes you don't want to download all the fields on the related entity, and it's possible to specify only some columns to load when eager loading related models using the following notation: 'relation_name:column1,column2,column3,etc':

...

...

Lazy loading vs eager loading
linenumbers
Code Block
languagetruephp
// ...
$courses = course::repository()
    ->with('modules:instance,section,idnumber') // We want to get only these columns
    ->get();

foreach ($courses as $course) {
    // You want to get each course module here
    foreach($courses->modules as $cm) {
        var_dump($cm->to_array());   
    }
}

// It will output something like:
/*

[
   'id' => 1,
   'instance' => 22,
   'section' => '...',
   'idnumber' => '...',
   'course' => 2,
]
*/


Info
title

Specifying columns to load on a relation

If you omit the id column, it will be automatically prepended to the array of the columns to load anyway, as well as foreign key. If you omit foreign key, it will be appended automatically to the list of columns to load, due to internal implementation.

...

Sometimes you want to apply conditions when eager loading related models, for example order them in a specific way or add some extra constraints. It is possible without creating a dedicated method on the entity to load a filtered relation.

...

Lazy loading vs eager loading
linenumbers
Code Block
languagetruephp
// ...
$courses = course::repository()
    ->with(['modules' => function(entity_repository $repository) {
        $repository->where('visibleoncoursepage', true)
                   ->order_by('added', 'desc');
    }])
    ->get();
// ...

// This ^^ will only load related models which have "visibleoncoursepage" flag set to true and the collection will be sorted by added field in a descending order.

// It is also possible to specify both: columns to load as well as extra conditions:
// ...
    ->with(['modules:added,module' => function(entity_repository $repository) {
        $repository->where('visibleoncoursepage', true)
                   ->order_by('added', 'desc');
    }])
// ...

...

It is possible to insert related entities using corresponding relation methods. The advantage of using this method is that it takes care of inserting a key into the related entity automatically. For example you can use a save method.

Inserting related entities
Code Block
linenumbers
languagephp
titleInserting related entities
true
// ...
$course = course::repository()->first();

$course->modules()->save(new course_module(
    [
        'module' => 5,
        // ...,
    ]));

// This will automatically insert "course" (foreign key) property into the course_module entity.


title
Info

Inserting availability

Not all the relation types support inserting related models, for example belongs_to relation does not.


title
Info

Inserting on collections

Currently inserting related entities for collections is not supported.

...

It is possible to perform bulk actions on related entities using relation method notation on the entity.

Updating

...

Lazy loading vs eager loading
Code Block
linenumberslanguagetruephp
// ...
$course = course::repository()->first(); // Will retrieve a course with id 1 for example

$course->modules()->update([
   'visible' => false,
]);

// Running the above query will set visible flag to false for all modules with course id 1

...

The delete method on the repository will work out of the box as update method described above.

...

Lazy loading vs eager loading
linenumbers
Code Block
languagetruephp
// ...
$course = course::repository()->first(); // Will retrieve a course with id 1 for example

$course->modules()->delete();

// Running the above query will delete all modules with course id 1

...

Features which might get implemented in the future:

  • Many-to-many relation (having two tables connected using an intermediate table)

  • Polymorphic relation

  • Querying existence/absence of the relationship, e.g. calling where_has(), where_doesnt_have() on the entity repository

  • Counting related models.

  • Attach/detach feature for belongs_to relation

Since we designed our ORM based on Laravel's one, the relationships follow the same idea. You can read about Laravel Eloquent ORM relationships here: https://laravel.com/docs/5.8/eloquent-relationships