Orm
Orm is short for Object
Relational Mapper which does 2 things: it maps your database table rows to objects and it allows you
to establish relations between those objects.
It follows closely the
Active Record Pattern, but was also influenced by other systems.
Relations: Introduction
Configuration
Basic config
protected static $_has_many = array('comments');
This example, if specified in Model_Article, enables fetching of an array of
Model_Comment objects which have the field article_id matching the primary key of
a given Article instance through the property comments.
Fully configured
protected static $_has_many = array('comments' => array(
'model_to' => 'Model_Fancy_Comment',
'key_from' => 'article_id',
'key_to' => 'parent_article_id',
'cascade_save' => true,
'cascade_delete' => false,
// there are some more options for specific relation types
));
In the basic example, Orm automatically assumes the model name and field mapping. This example
explicitly specifies the class name of the target model, the fields used to relate them, and whether
actions performed on the current object should be cascaded to the target. It will return an array of
Model_Fancy_Comment object where the comment's parent_article_id field
corresponds to the current object's article_id. When saving an object the operation is
also performed on its loaded relations, deleting isn't cascaded by default but can be if you switch
this on.
There is a limitation when fetching relations and limiting their output: you can't use
complex where statements on the main object. That are queries using parenthesis for nesting conditions
using where_open(). Normal (non-nested) where queries can be applied without problems.
Technically it works like this: to limit the output the query on the base model's
table is actually a subquery with a limit set on it. Any of those nested where conditions are applied
on the result of the subquery and further joined relations.
Configuration Options
All of the configurations are optional when using the most common naming conventions.
model_to |
Calculated from alias |
If specified, must the full class name of the target model (ex. Model_Comment).
By default, this value is formed by prepending 'Model_' to the singular form of the alias
(ex. 'comments' becomes 'Model_Comment'). If your model is in another namespace, you must specify
the full namespace, excluding the leading backslash (ex. 'Admin\Model_User'), PHP doesn't support
relative namespaces in strings. |
key_from |
The key used for the relation in the current model (Usually id) |
Allows mapping the target model to an arbitrary field in the current model's table |
key_to |
Calculated from the current model name |
By default, a relationship from a Model_Article to many Model_Comments
would use the field article_id in the comments table |
cascade_save |
bool true |
Cascading means the operation on the model is repeated on its relation. Thus
cascading a save will save the relations as well, cascading a delete will delete the
relations as well. Be very careful with cascading delete!
You can override these options at runtime by passing true as the first
argument when calling save() or delete() on the originating model. |
cascade_delete |
bool false |
conditions |
array() |
Takes 'where' and 'order_by' keys. These are more limited than the normal usage:
where must be an array of arrays containing
array(field, compare, value). order_by contains an array of fields or
associative field => direction.
Note: these are always used and cannot be turned off. |
Usage
The Orm allows for both eager and lazy-loading of relationships. Eager loading means that some (or all)
relations are fetched in the original query. Lazy loading means that the relations aren't fetched until
you request them.
// eager loading, using joins:
$post = Model_Post::find('all', array('related' => array('comments')));
// or
$post = Model_Post::query()->related('comments')->get();
// $post->comments is available without any further querying after this
// or use lazy loading, it won't use joins but query a relation once requested
// first get a "post", 1 query without join
$post = Model_Post::find('first');
// now request the comments (not yet loaded), which will do another query without join automatically
$comments = $post->comments;
// alternatively, you can use get(), which allows additional conditions:
$comments = $post->get('comments', array('where' => array(array('field', '=', $value))));
Usage with where/order_by conditions
You can also additional conditions when fetching with the Orm. Note though that would also mean the
data retrieved will only be valid to the conditions with which you fetched them.
Additional conditions are only possible with eager loading, though with lazy loading any default
conditions (see config table above) will be used of course.
// using array querying
$post = Model_Post::find('first', array(
'related' => array(
'articles' => array(
'order_by' => array('id' => 'desc'),
'where' => array(
array('publish_date', '>', time()),
array('published', '=', 1),
),
),
),
));
// using method chaining
$post = Model_Post::query()->related('articles', array(
'order_by' => array('id' => 'desc'),
'where' => array(
array('publish_date', '>', time()),
array('published', 1), // when using '=' it can be omitted
),
)->get_one();
// but the same can also be done by prefixing the relation name to the column:
$post = Model_Post::query()->related('articles')
->order_by('articles.id', 'desc')
->where('articles.publish_date', '>', time())
->where('articles.published', 1)
->get_one();
Usage with nested relationships
It is also possible to fetch the relations of relations to an unlimited depth (though you might want to
be careful creating queries that require too many joins). All these can also be combined with where
and order_by conditions, some are exemplified below.
Note that the order matters here, if you want to fetch the relations of a relation you must
load the "parent" relation before its relation. Otherwise an exception will be thrown.
// using array querying
$post = Model_Post::find('first', array(
'related' => array(
'articles' => array(
'related' => array(
'user' => array(
'related' => array('profile'),
'where' => array('active', 1),
),
),
'order_by' => array(
'published' => 'desc',
),
),
),
));
// using only method chaining
$post = Model_Post::query()
->related('articles')
->related('articles.user')
->related('articles.user.profile')
->where('articles.user.active', '=', 1)
->order_by('articles.published', 'desc')
->get_one();
// or combine array & method chaining
$post = Model_Post::query()
->related('articles', array(
'related' => array('user' => array(
'where' => array('active' => 1),
)),
'order_by' => array('published', 'desc'),
))
->related('articles.user.profile')
->get_one();
Join types
By default the Orm will join relations using a 'left' join. To specify a different join you utilize the 'join_type' condition:
$post = Model_Post::find('first', array(
'related' => array(
'articles' => array(
'join_type' => 'inner',
'where' => array(
array('publish_date', '>', DB::expr(time())),
array('published', '=', DB::expr(1)),
),
'order_by' => array('id' => 'desc'),
),
),
));
Since 'where' clauses are executed before the JOIN is executed, you can not get a full OUTER JOIN results when
you include a 'where' clause, it will act like a pre-join filter. The solution is to define the filter on the
JOIN itself:
$post = Model_Post::find('all', array(
'related' => array(
'articles' => array(
'join_type' => 'left outer',
'join_on' => array(
array('publish_date', '>', DB::expr(time())),
array('published', '=', DB::expr(1)),
),
'order_by' => array('id' => 'desc'),
),
),
));
This will make the selection criteria part of the ON clause, instead of the WHERE clause.
Note that if you want to pass a literal value to a join, you will have to encapsulate it in a DB::expr() to avoid it being escaped as a column name.
Relation types
The Orm supports the following relationship types natively:
- Belongs To
Has the primary key of the relation kept in its table, belongs to 1 related object.
- Has One
Has its primary key saved in one other row of another table (which belongs to this), has 1
related object.
- Has Many
Has its primary key saved in many other rows of another table (which belong to this one), has
many related objects.
- Many to Many
Have their primary keys saved in a table in between which keeps pairs of primary keys from both
tables. Have and belong to many objects.