mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
a4ee0f4dad
API CHANGE: Deprecate SQLMap.
641 lines
20 KiB
Markdown
641 lines
20 KiB
Markdown
# 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: seperate tables will be linked together, the data spread across these tables. The mapping and saving/loading
|
|
logic is handled by sapphire, you don't need to worry about writing SQL most of the time.
|
|
|
|
The advanced object-relational layer in SilverStripe is one of the main reasons for requiring PHP5. Most of its
|
|
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.
|
|
|
|
## Generating the database-schema
|
|
|
|
The SilverStripe database-schema is generated automatically by visiting the URL.
|
|
`http://<mysite>/dev/build`
|
|
|
|
<div class="notice" markdown='1'>
|
|
Note: You need to be logged in as an administrator to perform this command.
|
|
</div>
|
|
|
|
## Querying Data
|
|
|
|
Every query to data starts with a `DataList::create($class)` call. For example, this query would return all of the Member objects:
|
|
|
|
:::php
|
|
$members = DataList::create('Member');
|
|
|
|
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 = DataList::create('Member')->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
|
|
query until you iterate on the result with a `foreach()` or `<% control %>`.
|
|
|
|
:::php
|
|
// The SQL query isn't executed here...
|
|
$members = DataList::create('Member');
|
|
// ...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 = DataList::create('Member')->filter(array('FirstName' => 'Sam'))->sort('Surname');
|
|
// This will create an single SELECT COUNT query.
|
|
echo $members->Count();
|
|
|
|
All of this lets you focus on writing your application, and not worrying too much about whether or not your queries are efficient.
|
|
|
|
### 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 = DataList::create('Member')->byID(5);
|
|
|
|
If you have constructed a query that you know should return a single record, you can call `First()`:
|
|
|
|
:::php
|
|
$member = DataList::create('Member')->filter(array('FirstName' => 'Sam', 'Surname' => 'Minnee'))->First();
|
|
|
|
|
|
### Filters
|
|
|
|
**FUN FACT:** This isn't implemented in the code yet, but will be shortly.
|
|
|
|
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 = DataList::create('Member')->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 minimise 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 = DataList::create('Member')->filter(array(
|
|
'FirstName' => 'Sam',
|
|
'Surname' => 'Minnée',
|
|
));
|
|
|
|
By default, these filters specify case-insensitive exact matches. There are a number of suffixes that you can put on
|
|
field names to change this: `":StartsWith"`, `":EndsWith"`, `":Contains"`, `":GreaterThan"`, `":LessThan"`, `":Not"`,
|
|
and `":MatchCase"`. `":Not"` and `":MatchCase"` are special in that you can add it to any of the other filters.
|
|
|
|
This query will return everyone whose first name doesn't start with S, who have logged on since 1/1/2011.
|
|
|
|
:::php
|
|
$members = DataList::create('Member')->filter(array(
|
|
'FirstName:StartsWith:Not' => 'S'
|
|
'LastVisited:GreaterThan' => '2011-01-01'
|
|
));
|
|
|
|
If you wish to match against any of a number of columns, you can list several field names, separated by commas. This
|
|
will return all members whose first name or surname contain the string 'sam'.
|
|
|
|
:::php
|
|
$members = DataList::create('Member')->filter(array(
|
|
'FirstName,Surname:Contains' => 'sam'
|
|
));
|
|
|
|
If you wish to match against any of a number of values, you can pass an array as the value. This will return all
|
|
members whose first name is either Sam or Ingo.
|
|
|
|
:::php
|
|
$members = DataList::create('Member')->filter(array(
|
|
'FirstName' => array('sam', 'ingo'),
|
|
));
|
|
|
|
### Relation filters
|
|
|
|
So far we have only filtered a data list by fields on the object that you're requesting. For simple cases, this might
|
|
be okay, but often, a data model is made up of a number of related objects. For example, in SilverStripe each member
|
|
can be placed in a number of groups, and each group has a number of permissions.
|
|
|
|
For this, Sapphire ORM supports **Relation Filters**. Any ORM request can be filtered by fields on a related object by
|
|
specifying the filter key as `<relation-name>.<field-in-related-object>`. You can chain relations together as many
|
|
times as is necessary.
|
|
|
|
For example, this will return all members assigned ot a group that has a permission record with the code "ADMIN". In other words, it will return all administrators.
|
|
|
|
:::php
|
|
$members = DataList::create('Member')->filter(array(
|
|
'Groups.Permissions.Code' => 'ADMIN',
|
|
));
|
|
|
|
Note that we are just joining to these tables to filter the records. Even if a member is in more than 1 administrator group, unique members will still be returned by this query.
|
|
|
|
The other features of filters can be applied to relation filters as well. This will return all members in groups whose
|
|
names start with 'A' or 'B'.
|
|
|
|
:::php
|
|
$members = DataList::create('Member')->filter(array(
|
|
'Groups.Title:StartsWith' => array('A', 'B'),
|
|
));
|
|
|
|
You can even follow a relation back to the original model class! This will return all members are in at least 1 group that also has a member called Sam.
|
|
|
|
:::php
|
|
$members = DataList::create('Member')->filter(array(
|
|
'Groups.Members.FirstName' => 'Sam'
|
|
));
|
|
|
|
### Raw SQL options for advanced users
|
|
|
|
Occassionally, the system described above won't let you do exactly what you need to do. In these situtations, 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.
|
|
|
|
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)
|
|
|
|
#### Where clauses
|
|
|
|
You can specify a WHERE clause fragment (that will be combined with other filters using AND) with the `where()` method:
|
|
|
|
:: php
|
|
$members = DataList::create('Member')->where("\"FirstName\" = 'Sam'")
|
|
|
|
#### 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 = DataList::create('Member')->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
|
|
|
|
$members = DataList::create('Member')->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 {
|
|
static $db = array(
|
|
"FirstName" => "Varchar",
|
|
"Surname" => "Varchar",
|
|
"Description" => "Text",
|
|
"Status" => "Enum('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 behaviour by making a function called "get`<fieldname>`" or "set`<fieldname>`".
|
|
|
|
:::php
|
|
class Player extends DataObject {
|
|
static $db = array(
|
|
"Status" => "Enum('Active, Injured, Retired')"
|
|
);
|
|
|
|
// access through $myPlayer->Status
|
|
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 *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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
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: <% control MyPlayer %>MembershipFee.Nice<% end_control %> returns a casted string (e.g. "$123.45")
|
|
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 {
|
|
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 {
|
|
static $has_one = array(
|
|
"Parent" => "SiteTree",
|
|
);
|
|
}
|
|
|
|
### 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 {
|
|
static $has_many = array(
|
|
"Players" => "Player",
|
|
);
|
|
}
|
|
class Player extends DataObject {
|
|
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 {
|
|
static $has_many = array(
|
|
"Managing" => "Company.Manager",
|
|
"Cleaning" => "Company.Cleaner",
|
|
);
|
|
}
|
|
|
|
class Company {
|
|
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 {
|
|
static $has_many = array(
|
|
"Players" => "Player",
|
|
);
|
|
}
|
|
class Player extends DataObject {
|
|
static $has_one = array(
|
|
"Team" => "Team",
|
|
"AnotherTeam" => "Team",
|
|
);
|
|
}
|
|
|
|
|
|
### 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 {
|
|
static $many_many = array(
|
|
"Categories" => "Category",
|
|
);
|
|
}
|
|
class Category extends DataObject {
|
|
static $belongs_many_many = array(
|
|
"Teams" => "Team",
|
|
);
|
|
}
|
|
|
|
|
|
### Adding relations
|
|
|
|
Inside sapphire it doesn't matter if you're editing a *has_many*- or a *many_many*-relationship. You need to get a
|
|
`[api:ComponentSet]`.
|
|
|
|
:::php
|
|
class Team extends DataObject {
|
|
// see "many_many"-description for a sample definition of class "Category"
|
|
static $many_many = array(
|
|
"Categories" => "Category",
|
|
);
|
|
|
|
/**
|
|
|
|
* @param DataObjectSet
|
|
*/
|
|
function addCategories($additionalCategories) {
|
|
$existingCategories = $this->Categories();
|
|
|
|
// method 1: Add many by iteration
|
|
foreach($additionalCategories as $category) {
|
|
$existingCategories->add($category);
|
|
}
|
|
|
|
// method 2: Add many by ID-List
|
|
$existingCategories->addMany(array(1,2,45,745));
|
|
}
|
|
}
|
|
|
|
|
|
### 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 {
|
|
static $has_many = array(
|
|
"Players" => "Player"
|
|
);
|
|
|
|
// can be accessed by $myTeam->ActivePlayers()
|
|
function ActivePlayers() {
|
|
return $this->Players("Status='Active'");
|
|
}
|
|
}
|
|
|
|
## 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 = DataList::create('Member')->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 = DataList::create('Member')->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 = DataList::create('Member');
|
|
$map = new SS_Map($members, 'ID', 'FirstName');
|
|
|
|
## 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 = DataObject::get_by_id('Player',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 security. These functions are private, obviously
|
|
it wouldn't make sense to call them externally on the object. They are triggered when calling *write()*.
|
|
|
|
Example: Disallow creation of new players if the currently logged-in player is not a team-manager.
|
|
|
|
:::php
|
|
class Player extends DataObject {
|
|
static $has_many = array(
|
|
"Teams"=>"Team"
|
|
);
|
|
|
|
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-connection', E_USER_ERROR);
|
|
exit();
|
|
}
|
|
|
|
// CAUTION: You are required to call the parent-function, otherwise sapphire 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 {
|
|
static $has_many = array(
|
|
"Teams"=>"Team"
|
|
);
|
|
|
|
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 `[api: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.
|
|
Please see `[api:DataExtension]` for a general description, and `[api:Hierarchy]` for our most
|
|
popular examples.
|
|
|
|
|
|
|
|
## FAQ
|
|
|
|
### Whats 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:ComponentSet]` with relation-specific functionality.
|
|
|
|
:::php
|
|
$myTeam = DataObject::get_by_id('Team',$myPlayer->TeamID); // returns DataObject
|
|
$myTeam->add(new Player()); // fails
|
|
|
|
$myTeam = $myPlayer->Team(); // returns Componentset
|
|
$myTeam->add(new Player()); // works
|
|
|