mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
d8e9af8af8
Database abstraction broken up into controller, connector, query builder, and schema manager, each independently configurable via YAML / Injector Creation of new DBQueryGenerator for database specific generation of SQL Support for parameterised queries, move of code base to use these over escaped conditions Refactor of SQLQuery into separate query classes for each of INSERT UPDATE DELETE and SELECT Support for PDO Installation process upgraded to use new ORM SS_DatabaseException created to handle database errors, maintaining details of raw sql and parameter details for user code designed interested in that data. Renamed DB static methods to conform correctly to naming conventions (e.g. DB::getConn -> DB::get_conn) 3.2 upgrade docs Performance Optimisation and simplification of code to use more concise API API Ability for database adapters to register extensions to ConfigureFromEnv.php
1106 lines
33 KiB
Markdown
Executable File
1106 lines
33 KiB
Markdown
Executable File
# Datamodel
|
|
|
|
SilverStripe uses an [object-relational model](http://en.wikipedia.org/wiki/Object-relational_model)
|
|
that assumes the following connections:
|
|
|
|
* Each database-table maps to a PHP class
|
|
* Each database-row maps to a PHP object
|
|
* Each database-column maps to a property on a PHP object
|
|
|
|
All data tables in SilverStripe are defined as subclasses of `[api:DataObject]`.
|
|
|
|
Inheritance is supported in the data model: separate tables will be linked
|
|
together, the data spread across these tables. The mapping and saving/loading
|
|
logic is handled by SilverStripe, you don't need to worry about writing SQL most
|
|
of the time.
|
|
|
|
Most of the ORM customizations are possible through [PHP5 Object
|
|
Overloading](http://www.onlamp.com/pub/a/php/2005/06/16/overloading.html)
|
|
handled in the `[api:Object]`-class.
|
|
|
|
See [database-structure](/reference/database-structure) for in-depth information
|
|
on the database-schema and the ["sql queries" topic](/reference/sqlquery) in
|
|
case you need to drop down to the bare metal.
|
|
|
|
## Generating the Database Schema
|
|
|
|
The SilverStripe database-schema is generated automatically by visiting the URL.
|
|
`http://localhost/dev/build`
|
|
|
|
<div class="notice" markdown='1'>
|
|
Note: You need to be logged in as an administrator to perform this command,
|
|
unless your site is in "[dev mode](/topics/debugging)", or the command is run
|
|
through CLI.
|
|
</div>
|
|
|
|
## Querying Data
|
|
|
|
Every query to data starts with a `DataList::create(<class>)` or `<class>::get()`
|
|
call. For example, this query would return all of the `Member` objects:
|
|
|
|
:::php
|
|
$members = Member::get();
|
|
|
|
The ORM uses a "fluent" syntax, where you specify a query by chaining together
|
|
different methods. Two common methods are `filter()` and `sort()`:
|
|
|
|
:::php
|
|
$members = Member::get()->filter(array(
|
|
'FirstName' => 'Sam'
|
|
))->sort('Surname');
|
|
|
|
Those of you who know a bit about SQL might be thinking "it looks like you're
|
|
querying all members, and then filtering to those with a first name of 'Sam'.
|
|
|
|
Isn't this very slow?" Is isn't, because the ORM doesn't actually execute the
|
|
SQL query until you iterate on the result with a `foreach()` or `<% loop %>`.
|
|
The ORM is smart enough to generate a single efficient query at the last moment
|
|
in time without needing to post process the result set in PHP. In MySQL the
|
|
query generated by the ORM may look something like this for the previous query.
|
|
|
|
:::
|
|
SELECT * FROM Member WHERE FirstName = 'Sam' ORDER BY Surname
|
|
|
|
An example of the query process in action:
|
|
|
|
:::php
|
|
// The SQL query isn't executed here...
|
|
$members = Member::get();
|
|
// ...or here
|
|
$members = $members->filter(array('FirstName' => 'Sam'));
|
|
// ...or even here
|
|
$members = $members->sort('Surname');
|
|
|
|
// *This* is where the query is executed
|
|
foreach($members as $member) {
|
|
echo "<p>$member->FirstName $member->Surname</p>";
|
|
}
|
|
|
|
This also means that getting the count of a list of objects will be done with a
|
|
single, efficient query.
|
|
|
|
:::php
|
|
$members = Member::get()->filter(array(
|
|
'FirstName' => 'Sam'
|
|
))->sort('Surname');
|
|
|
|
// This will create an single SELECT COUNT query similar to -
|
|
// SELECT COUNT(*) FROM Members WHERE FirstName = 'Sam'
|
|
echo $members->Count();
|
|
|
|
|
|
### Returning a single DataObject
|
|
|
|
There are a couple of ways of getting a single DataObject from the ORM. If you
|
|
know the ID number of the object, you can use `byID($id)`:
|
|
|
|
:::php
|
|
$member = Member::get()->byID(5);
|
|
|
|
If you have constructed a query that you know should return a single record, you
|
|
can call `First()`:
|
|
|
|
:::php
|
|
$member = Member::get()->filter(array(
|
|
'FirstName' => 'Sam', 'Surname' => 'Minnee'
|
|
))->First();
|
|
|
|
|
|
### Sort
|
|
|
|
Quite often you would like to sort a list. Doing this on a list could be done in
|
|
a few ways.
|
|
|
|
If would like to sort the list by `FirstName` in a ascending way (from A to Z).
|
|
|
|
:::php
|
|
$members = Member::get()->sort('FirstName', 'ASC'); // ASC or DESC
|
|
$members = Member::get()->sort('FirstName'); // Ascending is implied
|
|
|
|
To reverse the sort
|
|
|
|
:::php
|
|
$members = Member::get()->sort('FirstName', 'DESC');
|
|
|
|
// or..
|
|
$members = Member::get()->sort('FirstName', 'ASC')->reverse();
|
|
|
|
However you might have several entries with the same `FirstName` and would like
|
|
to sort them by `FirstName` and `LastName`
|
|
|
|
:::php
|
|
$member = Member::get()->sort(array(
|
|
'FirstName' => 'ASC',
|
|
'LastName'=>'ASC'
|
|
));
|
|
|
|
You can also sort randomly
|
|
|
|
:::php
|
|
$member = Member::get()->sort('RAND()')
|
|
|
|
### Filter
|
|
|
|
As you might expect, the `filter()` method filters the list of objects that gets
|
|
returned. The previous example included this filter, which returns all Members
|
|
with a first name of "Sam".
|
|
|
|
:::php
|
|
$members = Member::get()->filter(array('FirstName' => 'Sam'));
|
|
|
|
In SilverStripe 2, we would have passed `"\"FirstName\" = 'Sam'` to make this
|
|
query. Now, we pass an array, `array('FirstName' => 'Sam')`, to minimize the
|
|
risk of SQL injection bugs. The format of this array follows a few rules:
|
|
|
|
* Each element of the array specifies a filter. You can specify as many
|
|
filters as you like, and they **all** must be true.
|
|
* The key in the filter corresponds to the field that you want to filter by.
|
|
* The value in the filter corresponds to the value that you want to filter to.
|
|
|
|
So, this would return only those members called "Sam Minnée".
|
|
|
|
:::php
|
|
$members = Member::get()->filter(array(
|
|
'FirstName' => 'Sam',
|
|
'Surname' => 'Minnée',
|
|
));
|
|
|
|
There is also a short hand way of getting Members with the FirstName of Sam.
|
|
|
|
:::php
|
|
$members = Member::get()->filter('FirstName', 'Sam');
|
|
|
|
Or if you want to find both Sam and Sig.
|
|
|
|
:::php
|
|
$members = Member::get()->filter(
|
|
'FirstName', array('Sam', 'Sig')
|
|
);
|
|
|
|
Then there is the most complex task when you want to find Sam and Sig that has
|
|
either Age 17 or 74.
|
|
|
|
:::php
|
|
$members = Member::get()->filter(array(
|
|
'FirstName' => array('Sam', 'Sig'),
|
|
'Age' => array(17, 74)
|
|
));
|
|
|
|
// SQL: WHERE ("FirstName" IN ('Sam', 'Sig) AND "Age" IN ('17', '74))
|
|
|
|
In case you want to match multiple criteria non-exclusively (with an "OR"
|
|
disjunctive),use the `filterAny()` method instead:
|
|
|
|
:::php
|
|
$members = Member::get()->filterAny(array(
|
|
'FirstName' => 'Sam',
|
|
'Age' => 17,
|
|
));
|
|
// SQL: WHERE ("FirstName" = 'Sam' OR "Age" = '17')
|
|
|
|
You can also combine both conjunctive ("AND") and disjunctive ("OR") statements.
|
|
|
|
:::php
|
|
$members = Member::get()
|
|
->filter(array(
|
|
'LastName' => 'Minnée'
|
|
))
|
|
->filterAny(array(
|
|
'FirstName' => 'Sam',
|
|
'Age' => 17,
|
|
));
|
|
// WHERE ("LastName" = 'Minnée' AND ("FirstName" = 'Sam' OR "Age" = '17'))
|
|
|
|
### Filter with PHP / filterByCallback
|
|
|
|
It is also possible to filter by a PHP callback, however this will force the
|
|
data model to fetch all records and loop them in PHP, thus `filter()` or `filterAny()`
|
|
are to be preferred over `filterByCallback()`.
|
|
Please note that because `filterByCallback()` has to run in PHP, it will always return
|
|
an `ArrayList` (even if called on a `DataList`, this however might change in future).
|
|
The first parameter to the callback is the item, the second parameter is the list itself.
|
|
The callback will run once for each record, if the callback returns true, this record
|
|
will be added to the list of returned items.
|
|
The below example will get all Members that have an expired or not encrypted password.
|
|
|
|
:::php
|
|
$membersWithBadPassword = Member::get()->filterByCallback(function($item, $list) {
|
|
if ($item->isPasswordExpired() || $item->PasswordEncryption = 'none') {
|
|
return true;
|
|
}
|
|
});
|
|
|
|
### Exclude
|
|
|
|
The `exclude()` method is the opposite to the filter in that it removes entries
|
|
from a list.
|
|
|
|
If we would like to remove all members from the list with the FirstName of Sam.
|
|
|
|
:::php
|
|
$members = Member::get()->exclude('FirstName', 'Sam');
|
|
|
|
Remove both Sam and Sig is as easy as.
|
|
|
|
:::php
|
|
$members = Member::get()->exclude('FirstName', array('Sam','Sig'));
|
|
|
|
As you can see it follows the same pattern as filter, so for removing only Sam
|
|
Minnée from the list:
|
|
|
|
:::php
|
|
$members = Member::get()->exclude(array(
|
|
'FirstName' => 'Sam',
|
|
'Surname' => 'Minnée',
|
|
));
|
|
|
|
And removing Sig and Sam with that are either age 17 or 74.
|
|
|
|
:::php
|
|
$members = Member::get()->exclude(array(
|
|
'FirstName' => array('Sam', 'Sig'),
|
|
'Age' => array(17, 43)
|
|
));
|
|
|
|
This would be equivalent to a SQL query of
|
|
|
|
:::
|
|
... WHERE ("FirstName" NOT IN ('Sam','Sig) OR "Age" NOT IN ('17', '74));
|
|
|
|
### Search Filter Modifiers
|
|
|
|
The where clauses showcased in the previous two sections (filter and exclude)
|
|
specify exact matches by default. However, there are a number of suffixes that
|
|
you can put on field names to change this behavior such as `":StartsWith"`,
|
|
`":EndsWith"`, `":PartialMatch"`, `":GreaterThan"`, `":GreaterThanOrEqual"`, `":LessThan"`, `":LessThanOrEqual"`,
|
|
`":Negation"`.
|
|
|
|
Each of these suffixes is represented in the ORM as a subclass of
|
|
`[api:SearchFilter]`. Developers can define their own SearchFilters if needing
|
|
to extend the ORM filter and exclude behaviors.
|
|
|
|
These suffixes can also take modifiers themselves. The modifiers currently
|
|
supported are `":not"`, `":nocase"` and `":case"`. These negate the filter,
|
|
make it case-insensitive and make it case-sensitive respectively. The default
|
|
comparison uses the database's default. For MySQL and MSSQL, this is
|
|
case-insensitive. For PostgreSQL, this is case-sensitive.
|
|
|
|
The following is a query which will return everyone whose first name doesn't
|
|
start with S, who has logged in since 1/1/2011.
|
|
|
|
:::php
|
|
$members = Member::get()->filter(array(
|
|
'FirstName:StartsWith:Not' => 'S'
|
|
'LastVisited:GreaterThan' => '2011-01-01'
|
|
));
|
|
|
|
### Subtract
|
|
|
|
You can subtract entries from a DataList by passing in another DataList to
|
|
`subtract()`
|
|
|
|
:::php
|
|
$allSams = Member::get()->filter('FirstName', 'Sam');
|
|
$allMembers = Member::get();
|
|
$noSams = $allMembers->subtract($allSams);
|
|
|
|
Though for the above example it would probably be easier to use `filter()` and
|
|
`exclude()`. A better use case could be when you want to find all the members
|
|
that does not exist in a Group.
|
|
|
|
:::php
|
|
// ... Finding all members that does not belong to $group.
|
|
$otherMembers = Member::get()->subtract($group->Members());
|
|
|
|
### Limit
|
|
|
|
You can limit the amount of records returned in a DataList by using the
|
|
`limit()` method.
|
|
|
|
:::php
|
|
// Returning the first 5 members, sorted alphabetically by Surname
|
|
$members = Member::get()->sort('Surname')->limit(5);
|
|
|
|
`limit()` accepts two arguments, the first being the amount of results you want
|
|
returned, with an optional second parameter to specify the offset, which allows
|
|
you to tell the system where to start getting the results from. The offset, if
|
|
not provided as an argument, will default to 0.
|
|
|
|
:::php
|
|
// Return 10 members with an offset of 4 (starting from the 5th result).
|
|
// Note that the argument order is different from a MySQL LIMIT clause
|
|
$members = Member::get()->sort('Surname')->limit(10, 4);
|
|
|
|
### Raw SQL options for advanced users
|
|
|
|
Occasionally, the system described above won't let you do exactly what you need
|
|
to do. In these situations, we have methods that manipulate the SQL query at a
|
|
lower level. When using these, please ensure that all table & field names are
|
|
escaped with double quotes, otherwise some DB back-ends (e.g. PostgreSQL) won't
|
|
work.
|
|
|
|
Under the hood, query generation is handled by the `[api:DataQuery]` class. This
|
|
class does provide more direct access to certain SQL features that `DataList`
|
|
abstracts away from you.
|
|
|
|
In general, we advise against using these methods unless it's absolutely
|
|
necessary. If the ORM doesn't do quite what you need it to, you may also
|
|
consider extending the ORM with new data types or filter modifiers (that
|
|
documentation still needs to be written)
|
|
|
|
<div class="notice" markdown="1">
|
|
See [the security topic](/topics/security#parameterised-queries) for details on safe database querying and why parameterised queries
|
|
are so necessary here.
|
|
</div>
|
|
|
|
#### SQL WHERE Predicates with Parameters
|
|
|
|
If using `DataObject::get()` (which returns a `DataList` instance) you can specify a WHERE clause fragment
|
|
(that will be combined with other filters using AND) with the `where()` method, or `whereAny()` to add a list
|
|
of clauses combined with OR.
|
|
|
|
Placeholders within a predicate are denoted by the question mark symbol, and should not be quoted.
|
|
|
|
For example:
|
|
|
|
:::php
|
|
$members = Member::get()->where(array('"FirstName" = ?' => 'Sam'));
|
|
|
|
If using `SQLSelect` you should use `addWhere`, `setWhere`, `addWhereAny`, or `setWhereAny` to modify the query.
|
|
|
|
Using the parameterised query syntax you can either provide a single variable as a parameter, an array of parameters
|
|
if the SQL has multiple value placeholders, or simply pass an indexed array of strings for literal SQL.
|
|
|
|
Although parameters can be escaped and directly inserted into the SQL condition (See `Convert::raw2sql()'),
|
|
the parameterised syntax is the preferred method of declaring conditions on a query.
|
|
|
|
Column names must still be double quoted, and for consistency and compatibility with other code, should also
|
|
be prefixed with the table name.
|
|
|
|
E.g.
|
|
|
|
:::php
|
|
<?php
|
|
|
|
$query = Table::get();
|
|
|
|
// multiple predicates with parameters
|
|
$query = $query->where(array(
|
|
'"Table"."Column" = ?' => $column,
|
|
'"Table"."Name" = ?' => $value
|
|
));
|
|
|
|
// Shorthand for simple column comparison (as above), omitting the '?'
|
|
// These will each be expanded internally to '"Table"."Column" = ?'
|
|
$query = $query->where(array(
|
|
'"Table"."Column"' => $column,
|
|
'"Table"."Name"' => $value
|
|
));
|
|
|
|
// Multiple predicates, some with multiple parameters.
|
|
// The parameters should ideally not be an associative array.
|
|
$query = $query->where(array(
|
|
'"Table"."ColumnOne" = ? OR "Table"."ColumnTwo" != ?' => array(1, 4),
|
|
'"Table"."ID" != ?' => $value
|
|
));
|
|
|
|
// Multiple predicates, each with explicitly typed parameters.
|
|
//
|
|
// The purpose of this syntax is to provide not only parameter values, but
|
|
// to also instruct the database connector on how to treat this value
|
|
// internally (subject to the database API supporting this feature).
|
|
//
|
|
// SQLQuery distinguishes these from predicates with multiple parameters
|
|
// by checking for the 'value' key in any array parameter given
|
|
$query = $query->whereAny(array(
|
|
'"Table"."Column"' => array(
|
|
'value' => $value,
|
|
'type' => 'string' // or any php type
|
|
),
|
|
'"Table"."HasValue"' => array(
|
|
'value' => 0,
|
|
'type' => 'boolean'
|
|
)
|
|
));
|
|
|
|
#### Run-Time Evaluated Conditions with SQLConditionGroup
|
|
|
|
Conditional expressions and groups may be encapsulated within a class (implementing
|
|
the SQLConditionGroup interface) and evaluated at the time of execution.
|
|
|
|
This is useful for conditions which may be placed into a query before the details
|
|
of that condition are fully specified.
|
|
|
|
E.g.
|
|
|
|
:::php
|
|
<?php
|
|
|
|
class RandomGroup implements SQLConditionGroup {
|
|
public $field = null;
|
|
public function conditionSQL(&$parameters) {
|
|
$parameters = array();
|
|
return "{$this->field} < RAND()";
|
|
}
|
|
}
|
|
|
|
$query = SQLSelect::create()
|
|
->setFrom('"MyObject"')
|
|
->setWhere($condition = new RandomCondition());
|
|
$condition->field = '"Score"';
|
|
$items = $query->execute();
|
|
|
|
#### Direct SQL Predicate
|
|
|
|
Conditions can be a literal piece of SQL which doesn't involve any parameters or values
|
|
at all, or can using safely SQL-encoded values, as it was originally.
|
|
|
|
<div class="warning" markdown='1'>
|
|
In nearly every instance it's preferrable to use the parameterised syntax, especially dealing
|
|
with variable parameters, even if those values were not submitted by the user.
|
|
See [the security topic](/topics/security#parameterised-queries) for details.
|
|
</div>
|
|
|
|
For instance, the following are all valid ways of adding SQL conditions directly to a query
|
|
|
|
:::php
|
|
<?php
|
|
// the entire predicate as a single string
|
|
$query->addWhere("\"Column\" = 'Value'");
|
|
|
|
// multiple predicates as an array
|
|
$query->addWhere(array("\"Column\" = 'Value'", "\"Column\" != 'Value'"));
|
|
|
|
// Shorthand for the above using argument expansion
|
|
$query->addWhere("\"Column\" = 'Value'", "\"Column\" != 'Value'");
|
|
|
|
// Literal SQL condition
|
|
$query->addWhere('"Created" > NOW()"');
|
|
|
|
#### Joining
|
|
|
|
You can specify a join with the innerJoin and leftJoin methods. Both of these
|
|
methods have the same arguments:
|
|
|
|
* The name of the table to join to
|
|
* The filter clause for the join
|
|
* An optional alias
|
|
|
|
For example:
|
|
|
|
:::php
|
|
// Without an alias
|
|
$members = Member::get()
|
|
->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
|
|
|
|
$members = Member::get()
|
|
->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "Rel");
|
|
|
|
Passing a *$join* statement to DataObject::get will filter results further by
|
|
the JOINs performed against the foreign table. **It will NOT return the
|
|
additionally joined data.** The returned *$records* will always be a
|
|
`[api:DataObject]`.
|
|
|
|
## Properties
|
|
|
|
|
|
### Definition
|
|
|
|
Data is defined in the static variable $db on each class, in the format:
|
|
`<property-name>` => "data-type"
|
|
|
|
:::php
|
|
class Player extends DataObject {
|
|
private static $db = array(
|
|
"FirstName" => "Varchar",
|
|
"Surname" => "Varchar",
|
|
"Description" => "Text",
|
|
"Status" => "Enum(array('Active', 'Injured', 'Retired'))",
|
|
"Birthday" => "Date"
|
|
);
|
|
}
|
|
|
|
See [data-types](data-types) for all available types.
|
|
|
|
### Overloading
|
|
|
|
"Getters" and "Setters" are functions that help us save fields to our data
|
|
objects. By default, the methods getField() and setField() are used to set data
|
|
object fields. They save to the protected array, $obj->record. We can overload
|
|
the default behavior by making a function called "get`<fieldname>`" or
|
|
"set`<fieldname>`".
|
|
|
|
:::php
|
|
class Player extends DataObject {
|
|
private static $db = array(
|
|
"Status" => "Enum(array('Active', 'Injured', 'Retired'))"
|
|
);
|
|
|
|
// access through $myPlayer->Status
|
|
public function getStatus() {
|
|
// check if the Player is actually... born already!
|
|
return (!$this->obj("Birthday")->InPast()) ? "Unborn" : $this->Status;
|
|
}
|
|
|
|
|
|
### Customizing
|
|
|
|
We can create new "virtual properties" which are not actually listed in
|
|
`private static $db` or stored in the database-row.
|
|
|
|
Here we combined a Player's first name and surname, accessible through
|
|
$myPlayer->Title.
|
|
|
|
:::php
|
|
class Player extends DataObject {
|
|
|
|
public function getTitle() {
|
|
return "{$this->FirstName} {$this->Surname}";
|
|
}
|
|
|
|
// access through $myPlayer->Title = "John Doe";
|
|
// just saves data on the object, please use $myPlayer->write() to save
|
|
// the database-row
|
|
public function setTitle($title) {
|
|
list($firstName, $surName) = explode(' ', $title);
|
|
$this->FirstName = $firstName;
|
|
$this->Surname = $surName;
|
|
}
|
|
}
|
|
|
|
<div class="warning" markdown='1'>
|
|
**CAUTION:** It is common practice to make sure that pairs of custom
|
|
getters/setter deal with the same data, in a consistent format.
|
|
</div>
|
|
|
|
<div class="warning" markdown='1'>
|
|
**CAUTION:** Custom setters can be hard to debug: Please double check if you
|
|
could transform your data in more straight-forward logic embedded to your custom
|
|
controller or form-saving.
|
|
</div>
|
|
|
|
### Default Values
|
|
|
|
Define the default values for all the $db fields. This example sets the
|
|
"Status"-column on Player to "Active" whenever a new object is created.
|
|
|
|
:::php
|
|
class Player extends DataObject {
|
|
|
|
private static $defaults = array(
|
|
"Status" => 'Active',
|
|
);
|
|
}
|
|
|
|
<div class="notice" markdown='1'>
|
|
Note: Alternatively you can set defaults directly in the database-schema (rather
|
|
than the object-model). See [data-types](data-types) for details.
|
|
</div>
|
|
|
|
### Casting
|
|
|
|
Properties defined in *static $db* are automatically casted to their
|
|
[data-types](data-types) when used in templates.
|
|
|
|
You can also cast the return-values of your custom functions (e.g. your "virtual
|
|
properties"). Calling those functions directly will still return whatever type
|
|
your PHP code generates, but using the *obj()*-method or accessing through a
|
|
template will cast the value according to the $casting-definition.
|
|
|
|
:::php
|
|
class Player extends DataObject {
|
|
|
|
private static $casting = array(
|
|
"MembershipFee" => 'Currency',
|
|
);
|
|
|
|
// $myPlayer->MembershipFee() returns a float (e.g. 123.45)
|
|
// $myPlayer->obj('MembershipFee') returns a object of type Currency
|
|
// In a template: <% loop $MyPlayer %>MembershipFee.Nice<% end_loop %>
|
|
// returns a casted string (e.g. "$123.45")
|
|
public function getMembershipFee() {
|
|
return $this->Team()->BaseFee * $this->MembershipYears;
|
|
}
|
|
}
|
|
|
|
|
|
## Relations
|
|
|
|
Relations are built through static array definitions on a class, in the format
|
|
`<relationship-name> => <classname>`.
|
|
|
|
### 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
|
|
// access with $myPlayer->Team()
|
|
class Player extends DataObject {
|
|
|
|
private static $has_one = array(
|
|
"Team" => "Team",
|
|
);
|
|
}
|
|
|
|
SilverStripe's `[api:SiteTree]` base-class for content-pages uses a 1-to-1
|
|
relationship to link to its parent element in the tree:
|
|
|
|
:::php
|
|
// access with $mySiteTree->Parent()
|
|
class SiteTree extends DataObject {
|
|
private static $has_one = array(
|
|
"Parent" => "SiteTree",
|
|
);
|
|
}
|
|
|
|
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.
|
|
|
|
::php
|
|
|
|
class Player extends DataObject {
|
|
private static $has_many = array(
|
|
"Fans" => "Fan.FanOf"
|
|
);
|
|
}
|
|
|
|
class Team extends DataObject {
|
|
private static $has_many = array(
|
|
"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 = array(
|
|
"FanOf" => "DataObject"
|
|
);
|
|
}
|
|
|
|
<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. A database-column named ""`<relationship-name>`ID""
|
|
will to be created in the child-class.
|
|
|
|
<div class="warning" markdown='1'>
|
|
**CAUTION:** 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
|
|
// access with $myTeam->Players() or $player->Team()
|
|
class Team extends DataObject {
|
|
|
|
private static $has_many = array(
|
|
"Players" => "Player",
|
|
);
|
|
}
|
|
|
|
class Player extends DataObject {
|
|
|
|
private static $has_one = array(
|
|
"Team" => "Team",
|
|
);
|
|
}
|
|
|
|
|
|
To specify multiple $has_manys to the same object you can use dot notation to
|
|
distinguish them like below
|
|
|
|
:::php
|
|
class Person extends DataObject {
|
|
|
|
private static $has_many = array(
|
|
"Managing" => "Company.Manager",
|
|
"Cleaning" => "Company.Cleaner",
|
|
);
|
|
}
|
|
|
|
class Company extends DataObject {
|
|
|
|
private static $has_one = array(
|
|
"Manager" => "Person",
|
|
"Cleaner" => "Person"
|
|
);
|
|
}
|
|
|
|
|
|
Multiple $has_one relationships are okay if they aren't linking to the same
|
|
object type.
|
|
|
|
:::php
|
|
/**
|
|
* THIS IS BAD
|
|
*/
|
|
class Team extends DataObject {
|
|
|
|
private static $has_many = array(
|
|
"Players" => "Player",
|
|
);
|
|
}
|
|
|
|
class Player extends DataObject {
|
|
private static $has_one = array(
|
|
"Team" => "Team",
|
|
"AnotherTeam" => "Team",
|
|
);
|
|
}
|
|
|
|
|
|
### 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
|
|
|
|
class Torso extends DataObject {
|
|
// HeadID will be generated on the Torso table
|
|
private static $has_one = array(
|
|
'Head' => 'Head'
|
|
);
|
|
}
|
|
|
|
class Head extends DataObject {
|
|
// No database field created. The '.Head' suffix could be omitted
|
|
private static $belongs_to = array(
|
|
'Torso' => 'Torso.Head'
|
|
);
|
|
}
|
|
|
|
|
|
### many_many
|
|
|
|
Defines many-to-many joins. A new table, (this-class)_(relationship-name), will
|
|
be created with a pair of ID fields.
|
|
|
|
<div class="warning" markdown='1'>
|
|
**CAUTION:** 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>
|
|
|
|
:::php
|
|
// access with $myTeam->Categories() or $myCategory->Teams()
|
|
class Team extends DataObject {
|
|
|
|
private static $many_many = array(
|
|
"Categories" => "Category",
|
|
);
|
|
}
|
|
|
|
class Category extends DataObject {
|
|
|
|
private static $belongs_many_many = array(
|
|
"Teams" => "Team",
|
|
);
|
|
}
|
|
|
|
|
|
### 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 `[api:HasManyList]` and
|
|
`[api:ManyManyList]`, both of which provide very similar APIs, e.g. an `add()`
|
|
and `remove()` method.
|
|
|
|
:::php
|
|
class Team extends DataObject {
|
|
|
|
// see "many_many"-description for a sample definition of class "Category"
|
|
private static $many_many = array(
|
|
"Categories" => "Category",
|
|
);
|
|
|
|
public function addCategories(SS_List $cats) {
|
|
foreach($cats as $cat) $this->Categories()->add($cat);
|
|
}
|
|
}
|
|
|
|
|
|
### Custom Relations
|
|
|
|
You can use the flexible datamodel 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 `[api:DataObject::$has_many]` for more info on the described relations.
|
|
|
|
:::php
|
|
class Team extends DataObject {
|
|
private static $has_many = array(
|
|
"Players" => "Player"
|
|
);
|
|
|
|
// can be accessed by $myTeam->ActivePlayers()
|
|
public function ActivePlayers() {
|
|
return $this->Players()->filter('Status', 'Active');
|
|
}
|
|
}
|
|
|
|
Note: 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 behaviour uses the `[api: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.
|
|
|
|
## Validation and Constraints
|
|
|
|
Traditionally, validation in SilverStripe has been mostly handled on the
|
|
controller through [form validation](/topics/forms#form-validation).
|
|
|
|
While this is a useful approach, it can lead to data inconsistencies if the
|
|
record is modified outside of the controller and form context.
|
|
|
|
Most validation constraints are actually data constraints which belong on the
|
|
model. SilverStripe provides the `[api:DataObject->validate()]` method for this
|
|
purpose.
|
|
|
|
By default, there is no validation - objects are always valid!
|
|
However, you can overload this method in your
|
|
DataObject sub-classes to specify custom validation,
|
|
or use the hook through `[api:DataExtension]`.
|
|
|
|
Invalid objects won't be able to be written - a [api:ValidationException]` will
|
|
be thrown and no write will occur.
|
|
|
|
It is expected that you call validate() in your own application to test that an
|
|
object is valid before attempting a write, and respond appropriately if it isn't.
|
|
|
|
The return value of `validate()` is a `[api:ValidationResult]` object.
|
|
You can append your own errors in there.
|
|
|
|
Example: Validate postcodes based on the selected country
|
|
|
|
:::php
|
|
class MyObject extends DataObject {
|
|
|
|
private static $db = array(
|
|
'Country' => 'Varchar',
|
|
'Postcode' => 'Varchar'
|
|
);
|
|
|
|
public function validate() {
|
|
$result = parent::validate();
|
|
if($this->Country == 'DE' && $this->Postcode && strlen($this->Postcode) != 5) {
|
|
$result->error('Need five digits for German postcodes');
|
|
}
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
<div class="hint" markdown='1'>
|
|
**Tip:** If you decide to add unique or other indexes to your model via
|
|
`static $indexes`, see [DataObject](/reference/dataobject) for details.
|
|
</div>
|
|
|
|
## Maps
|
|
|
|
A map is an array where the array indexes contain data as well as the values.
|
|
You can build a map from any DataList like this:
|
|
|
|
:::php
|
|
$members = Member::get()->map('ID', 'FirstName');
|
|
|
|
This will return a map where the keys are Member IDs, and the values are the
|
|
corresponding FirstName values. Like everything else in the ORM, these maps are
|
|
lazy loaded, so the following code will only query a single record from the
|
|
database:
|
|
|
|
:::php
|
|
$members = Member::get()->map('ID', 'FirstName');
|
|
echo $member[5];
|
|
|
|
This functionality is provided by the `SS_Map` class, which can be used to build
|
|
a map around any `SS_List`.
|
|
|
|
:::php
|
|
$members = Member::get();
|
|
$map = new SS_Map($members, 'ID', 'FirstName');
|
|
|
|
Note: You can also retrieve a single property from all contained records
|
|
through `[api:SS_List->column()]`.
|
|
|
|
## Data Handling
|
|
|
|
When saving data through the object model, you don't have to manually escape
|
|
strings to create SQL-safe commands. You have to make sure though that certain
|
|
properties are not overwritten, e.g. *ID* or *ClassName*.
|
|
|
|
### Creation
|
|
|
|
:::php
|
|
$myPlayer = new Player();
|
|
$myPlayer->Firstname = "John"; // sets property on object
|
|
$myPlayer->write(); // writes row to database
|
|
|
|
|
|
### Update
|
|
|
|
:::php
|
|
$myPlayer = Player::get()->byID(99);
|
|
if($myPlayer) {
|
|
$myPlayer->Firstname = "John"; // sets property on object
|
|
$myPlayer->write(); // writes row to database
|
|
}
|
|
|
|
|
|
### Batch Update
|
|
|
|
:::php
|
|
$myPlayer->update(
|
|
ArrayLib::filter_keys(
|
|
$_REQUEST,
|
|
array('Birthday', 'Firstname')
|
|
)
|
|
);
|
|
|
|
|
|
Alternatively you can use *castedUpdate()* to respect the
|
|
[data-types](/topics/data-types). This is preferred to manually casting data
|
|
before saving.
|
|
|
|
:::php
|
|
$myPlayer->castedUpdate(
|
|
ArrayLib::filter_keys(
|
|
$_REQUEST,
|
|
array('Birthday', 'Firstname')
|
|
)
|
|
);
|
|
|
|
|
|
### onBeforeWrite
|
|
|
|
You can customize saving-behaviour for each DataObject, e.g. for adding workflow
|
|
or data customization. The function is triggered when calling *write()* to save
|
|
the object to the database. This includes saving a page in the CMS or altering a
|
|
ModelAdmin record.
|
|
|
|
Example: Disallow creation of new players if the currently logged-in player is
|
|
not a team-manager.
|
|
|
|
:::php
|
|
class Player extends DataObject {
|
|
|
|
private static $has_many = array(
|
|
"Teams"=>"Team"
|
|
);
|
|
|
|
public function onBeforeWrite() {
|
|
// check on first write action, aka "database row creation"
|
|
// (ID-property is not set)
|
|
if(!$this->ID) {
|
|
$currentPlayer = Member::currentUser();
|
|
if(!$currentPlayer->IsTeamManager()) {
|
|
user_error('Player-creation not allowed', E_USER_ERROR);
|
|
exit();
|
|
}
|
|
}
|
|
|
|
// check on every write action
|
|
if(!$this->record['TeamID']) {
|
|
user_error('Cannot save player without a valid team', E_USER_ERROR);
|
|
exit();
|
|
}
|
|
|
|
// CAUTION: You are required to call the parent-function, otherwise
|
|
// SilverStripe will not execute the request.
|
|
parent::onBeforeWrite();
|
|
}
|
|
}
|
|
|
|
|
|
<div class="notice" markdown='1'>
|
|
Note: There are no separate methods for *onBeforeCreate* and *onBeforeUpdate*.
|
|
Please check for the existence of $this->ID to toggle these two modes, as shown
|
|
in the example above.
|
|
</div>
|
|
|
|
### onBeforeDelete
|
|
|
|
Triggered before executing *delete()* on an existing object.
|
|
|
|
Example: Checking for a specific [permission](/reference/permission) to delete
|
|
this type of object. It checks if a member is logged in who belongs to a group
|
|
containing the permission "PLAYER_DELETE".
|
|
|
|
:::php
|
|
class Player extends DataObject {
|
|
private static $has_many = array(
|
|
"Teams"=>"Team"
|
|
);
|
|
|
|
public function onBeforeDelete() {
|
|
if(!Permission::check('PLAYER_DELETE')) {
|
|
Security::permissionFailure($this);
|
|
exit();
|
|
}
|
|
|
|
parent::onBeforeDelete();
|
|
}
|
|
}
|
|
|
|
### Saving data with forms
|
|
|
|
See [forms](/topics/forms).
|
|
|
|
### Saving data with custom SQL
|
|
|
|
See the ["sql queries" topic](/reference/sqlquery) for custom *INSERT*,
|
|
*UPDATE*, *DELETE* queries.
|
|
|
|
## Extending DataObjects
|
|
|
|
You can add properties and methods to existing `[api:DataObjects]`s like
|
|
`[api:Member]` (a core class) without hacking core code or subclassing. See
|
|
`[api:DataExtension]` for a general description, and `[api:Hierarchy]` for the
|
|
most popular examples.
|
|
|
|
## FAQ
|
|
|
|
### What's the difference between DataObject::get() and a relation-getter?
|
|
|
|
You can work with both in pretty much the same way, but relationship-getters
|
|
return a special type of collection:
|
|
|
|
A `[api:HasManyList]` or a `[api:ManyManyList]` with relation-specific
|
|
functionality.
|
|
|
|
:::php
|
|
$myTeams = $myPlayer->Team(); // returns HasManyList
|
|
$myTeam->add($myOtherPlayer);
|