Merge remote-tracking branch 'origin/3.2' into 3.3

# Conflicts:
#	tests/model/DataObjectLazyLoadingTest.php
#	tests/model/VersionedTest.yml
This commit is contained in:
Damian Mooyman 2016-01-25 14:11:37 +13:00
commit 7c448bb4a2
9 changed files with 328 additions and 81 deletions

View File

@ -59,6 +59,48 @@ The relationship can also be navigated in [templates](../templates).
<% end_if %>
<% end_with %>
## Polymorphic has_one
A has_one can also be polymorphic, which allows any type of object to be associated.
This is useful where there could be many use cases for a particular data structure.
An additional column is created called "`<relationship-name>`Class", which along
with the ID column identifies the object.
To specify that a has_one relation is polymorphic set the type to 'DataObject'.
Ideally, the associated has_many (or belongs_to) should be specified with dot notation.
::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. As you can see from the previous example, `$has_many` goes hand in hand with `$has_one`.

View File

@ -1,13 +1,22 @@
title: SQLQuery
summary: Write and modify direct database queries through SQLQuery.
title: SQL Queries
summary: Write and modify direct database queries through SQLExpression subclasses.
# SQLQuery
# SQLSelect
A [api:SQLQuery] object represents a SQL query, which can be serialized into a SQL statement. Dealing with low-level
SQL such as `mysql_query()` is not encouraged, since the ORM provides powerful abstraction API's.
## Introduction
For example, if you want to run a simple `COUNT` SQL statement, the following three statements are functionally
equivalent:
An object representing a SQL select query, which can be serialized into a SQL statement.
It is easier to deal with object-wrappers than string-parsing a raw SQL-query.
This object is used by the SilverStripe ORM internally.
Dealing with low-level SQL is not encouraged, since the ORM provides
powerful abstraction APIs (see [datamodel](/developer_guides/data_model_and_orm).
Starting with SilverStripe 3, records in collections are lazy loaded,
and these collections have the ability to run efficient SQL
such as counts or returning a single column.
For example, if you want to run a simple `COUNT` SQL statement,
the following three statements are functionally equivalent:
:::php
// Through raw SQL.
@ -20,95 +29,254 @@ equivalent:
// Through the ORM.
$count = Member::get()->count();
If you do use raw SQL, you'll run the risk of breaking
various assumptions the ORM and code based on it have:
<div class="info">
The SQLQuery object is used by the SilverStripe ORM internally. By understanding SQLQuery, you can modify the SQL that
the ORM creates.
* Custom getters/setters (object property can differ from database column)
* DataObject hooks like onBeforeWrite() and onBeforeDelete()
* Automatic casting
* Default values set through objects
* Database abstraction
We'll explain some ways to use *SELECT* with the full power of SQL,
but still maintain a connection to the ORM where possible.
<div class="warning" markdown="1">
Please read our [security topic](/developer_guides/security) to find out
how to properly prepare user input and variables for use in queries
</div>
## Usage
### Select
### SELECT
Selection can be done by creating an instance of `SQLSelect`, which allows
management of all elements of a SQL SELECT query, including columns, joined tables,
conditional filters, grouping, limiting, and sorting.
E.g.
:::php
$sqlQuery = new SQLQuery();
<?php
$sqlQuery = new SQLSelect();
$sqlQuery->setFrom('Player');
$sqlQuery->selectField('FieldName', 'Name');
$sqlQuery->selectField('YEAR("Birthday")', 'Birthyear');
$sqlQuery->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"');
$sqlQuery->addWhere('YEAR("Birthday") = 1982');
$sqlQuery->addWhere(array('YEAR("Birthday") = ?' => 1982));
// $sqlQuery->setOrderBy(...);
// $sqlQuery->setGroupBy(...);
// $sqlQuery->setHaving(...);
// $sqlQuery->setLimit(...);
// $sqlQuery->setDistinct(true);
// Get the raw SQL (optional)
$rawSQL = $sqlQuery->sql();
// Get the raw SQL (optional) and parameters
$rawSQL = $sqlQuery->sql($parameters);
// Execute and return a Query object
$result = $sqlQuery->execute();
// Iterate over results
foreach($result as $row) {
echo $row['BirthYear'];
echo $row['BirthYear'];
}
The `$result` is an array lightly wrapped in a database-specific subclass of `[api:Query]`. This class implements the
*Iterator*-interface, and provides convenience-methods for accessing the data.
The result of `SQLSelect::execute()` is an array lightly wrapped in a database-specific subclass of `[api:SS_Query]`.
This class implements the *Iterator*-interface, and provides convenience-methods for accessing the data.
### Delete
### DELETE
Deletion can be done either by calling `DB::query`/`DB::prepared_query` directly,
by creating a `SQLDelete` object, or by transforming a `SQLQuery` into a `SQLDelete`
object instead.
For example, creating a `SQLDelete` object
:::php
$sqlQuery->setDelete(true);
<?php
### Insert / Update
$query = SQLDelete::create()
->setFrom('"SiteTree"')
->setWhere(array('"SiteTree"."ShowInMenus"' => 0));
$query->execute();
<div class="alert" markdown="1">
Currently not supported through the `SQLQuery` class, please use raw `DB::query()` calls instead.
</div>
Alternatively, turning an existing `SQLQuery` into a delete
:::php
DB::query('UPDATE "Player" SET "Status"=\'Active\'');
<?php
### Joins
$query = SQLQuery::create()
->setFrom('"SiteTree"')
->setWhere(array('"SiteTree"."ShowInMenus"' => 0))
->toDelete();
$query->execute();
Directly querying the database
:::php
<?php
DB::prepared_query('DELETE FROM "SiteTree" WHERE "SiteTree"."ShowInMenus" = ?', array(0));
### INSERT/UPDATE
INSERT and UPDATE can be performed using the `SQLInsert` and `SQLUpdate` classes.
These both have similar aspects in that they can modify content in
the database, but each are different in the way in which they behave.
Previously, similar operations could be performed by using the `DB::manipulate`
function which would build the INSERT and UPDATE queries on the fly. This method
still exists, but internally uses `SQLUpdate` / `SQLInsert`, although the actual
query construction is now done by the `DBQueryBuilder` object.
Each of these classes implements the interface `SQLWriteExpression`, noting that each
accepts write key/value pairs in a number of similar ways. These include the following
api methods:
* `addAssignments` - Takes a list of assignments as an associative array of key -> value pairs,
but also supports SQL expressions as values if necessary.
* `setAssignments` - Replaces all existing assignments with the specified list
* `getAssignments` - Returns all currently given assignments, as an associative array
in the format `array('Column' => array('SQL' => array('parameters)))`
* `assign` - Singular form of addAssignments, but only assigns a single column value.
* `assignSQL` - Assigns a column the value of a specified SQL expression without parameters
`assignSQL('Column', 'SQL)` is shorthand for `assign('Column', array('SQL' => array()))`
SQLUpdate also includes the following api methods:
* `clear` - Clears all assignments
* `getTable` - Gets the table to update
* `setTable` - Sets the table to update. This should be ANSI quoted.
E.g. `$query->setTable('"SiteTree"');`
SQLInsert also includes the following api methods:
* `clear` - Clears all rows
* `clearRow` - Clears all assignments on the current row
* `addRow` - Adds another row of assignments, and sets the current row to the new row
* `addRows` - Adds a number of arrays, each representing a list of assignment rows,
and sets the current row to the last one.
* `getColumns` - Gets the names of all distinct columns assigned
* `getInto` - Gets the table to insert into
* `setInto` - Sets the table to insert into. This should be ANSI quoted.
E.g. `$query->setInto('"SiteTree"');`
E.g.
:::php
<?php
$update = SQLUpdate::create('"SiteTree"')->where(array('ID' => 3));
// assigning a list of items
$update->addAssignments(array(
'"Title"' => 'Our Products',
'"MenuTitle"' => 'Products'
));
// Assigning a single value
$update->assign('"MenuTitle"', 'Products');
// Assigning a value using parameterised expression
$title = 'Products';
$update->assign('"MenuTitle"', array(
'CASE WHEN LENGTH("MenuTitle") > LENGTH(?) THEN "MenuTitle" ELSE ? END' =>
array($title, $title)
));
// Assigning a value using a pure SQL expression
$update->assignSQL('"Date"', 'NOW()');
// Perform the update
$update->execute();
In addition to assigning values, the SQLInsert object also supports multi-row
inserts. For database connectors and API that don't have multi-row insert support
these are translated internally as multiple single row inserts.
For example,
:::php
<?php
$insert = SQLInsert::create('"SiteTree"');
// Add multiple rows in a single call. Note that column names do not need
// to be symmetric
$insert->addRows(array(
array('"Title"' => 'Home', '"Content"' => '<p>This is our home page</p>'),
array('"Title"' => 'About Us', '"ClassName"' => 'AboutPage')
));
// Adjust an assignment on the last row
$insert->assign('"Content"', '<p>This is about us</p>');
// Add another row
$insert->addRow(array('"Title"' => 'Contact Us'));
$columns = $insert->getColumns();
// $columns will be array('"Title"', '"Content"', '"ClassName"');
$insert->execute();
### Value Checks
Raw SQL is handy for performance-optimized calls,
e.g. when you want a single column rather than a full-blown object representation.
Example: Get the count from a relationship.
:::php
$sqlQuery = new SQLQuery();
$sqlQuery->setFrom('Player');
$sqlQuery->addSelect('COUNT("Player"."ID")');
$sqlQuery->addWhere('"Team"."ID" = 99');
$sqlQuery->addWhere(array('"Team"."ID"' => 99));
$sqlQuery->addLeftJoin('Team', '"Team"."ID" = "Player"."TeamID"');
$count = $sqlQuery->execute()->value();
Note that in the ORM, this call would be executed in an efficient manner as well:
:::php
$count = $myTeam->Players()->count();
### Mapping
Creates a map based on the first two columns of the query result.
This can be useful for creating dropdowns.
Example: Show player names with their birth year, but set their birth dates as values.
:::php
$sqlQuery = new SQLQuery();
$sqlQuery = new SQLSelect();
$sqlQuery->setFrom('Player');
$sqlQuery->setSelect('ID');
$sqlQuery->setSelect('Birthdate');
$sqlQuery->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear');
$map = $sqlQuery->execute()->map();
$field = new DropdownField('Birthdates', 'Birthdates', $map);
echo $map;
Note that going through SQLSelect is just necessary here
because of the custom SQL value transformation (`YEAR()`).
An alternative approach would be a custom getter in the object definition.
// returns array(
// 1 => "Foo - 1920",
// 2 => "Bar - 1936"
// );
:::php
class Player extends DataObject {
private static $db = array(
'Name' => 'Varchar',
'Birthdate' => 'Date'
);
function getNameWithBirthyear() {
return date('y', $this->Birthdate);
}
}
$players = Player::get();
$map = $players->map('Name', 'NameWithBirthyear');
## Related Documentation
## Related
* [Introduction to the Data Model and ORM](data_model_and_orm)
## API Documentation
* [api:DataObject]
* [api:SQLQuery]
* [api:SQLSelect]
* [api:DB]
* [api:Query]
* [api:Database]

View File

@ -27,6 +27,11 @@ The extension is automatically applied to `SiteTree` class. For more information
[Extending](../extending) and the [Configuration](../configuration) documentation.
</div>
<div class="warning" markdown="1">
Versioning only works if you are adding the extension to the base class. That is, the first subclass
of `DataObject`. Adding this extension to children of the base class will have unpredictable behaviour.
</div>
## Database Structure
Depending on how many stages you configured, two or more new tables will be created for your records. In the above, this

View File

@ -126,8 +126,8 @@ If you're familiar with it, here's the short version of what you need to know. O
* **Squash your commits, so that each commit addresses a single issue.** After you rebase your work on top of the upstream master, you can squash multiple commits into one. Say, for instance, you've got three commits in related to Issue #100. Squash all three into one with the message "Description of the issue here (fixes #100)" We won't accept pull requests for multiple commits related to a single issue; it's up to you to squash and clean your commit tree. (Remember, if you squash commits you've already pushed to GitHub, you won't be able to push that same branch again. Create a new local branch, squash, and push the new squashed branch.)
* **Choose the correct branch**: Assume the current release is 3.0.3, and 3.1.0 is in beta state.
Most pull requests should go against the `3.1.x-dev` *pre-release branch*, only critical bugfixes
against the `3.0.x-dev` *release branch*. If you're changing an API or introducing a major feature,
Most pull requests should go against the `3.1` *pre-release branch*, only critical bugfixes
against the `3.0` *release branch*. If you're changing an API or introducing a major feature,
the pull request should go against `master` (read more about our [release process](release_process)). Branches are periodically merged "upwards" (3.0 into 3.1, 3.1 into master).
### Editing files directly on GitHub.com

View File

@ -36,7 +36,7 @@ Make sure you know the basic concepts of PHP5 before attempting to follow the tu
## SilverStripe Concepts
The [Developer Gudes](/developer_guides) contain more detailed documentation on certain SilverStripe topics, 'how to'
The [Developer Guides](/developer_guides) contain more detailed documentation on certain SilverStripe topics, 'how to'
examples and reference documentation.
[CHILDREN Folder=02_Developer_Guides]

View File

@ -660,26 +660,31 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
// Get ID field
$id = $manipulation[$table]['id'] ? $manipulation[$table]['id'] : $manipulation[$table]['fields']['ID'];
if(!$id) user_error("Couldn't find ID in " . var_export($manipulation[$table], true), E_USER_ERROR);
if(!$id) {
user_error("Couldn't find ID in " . var_export($manipulation[$table], true), E_USER_ERROR);
}
if($this->migratingVersion) {
$manipulation[$table]['fields']['Version'] = $this->migratingVersion;
}
// If we haven't got a version #, then we're creating a new version.
// Otherwise, we're just copying a version to another table
if(empty($manipulation[$table]['fields']['Version'])) {
$version = isset($manipulation[$table]['fields']['Version'])
? $manipulation[$table]['fields']['Version']
: null;
if($version < 0 || $this->_nextWriteWithoutVersion) {
// Putting a Version of -1 is a signal to leave the version table alone, despite their being no version
unset($manipulation[$table]['fields']['Version']);
} elseif(empty($version)) {
// If we haven't got a version #, then we're creating a new version.
// Otherwise, we're just copying a version to another table
$this->augmentWriteVersioned($manipulation, $table, $id);
}
// Putting a Version of -1 is a signal to leave the version table alone, despite their being no version
if($manipulation[$table]['fields']['Version'] < 0 || $this->_nextWriteWithoutVersion) {
// For base classes of versioned data objects
if(!$this->hasVersionField($table)) {
unset($manipulation[$table]['fields']['Version']);
}
// For base classes of versioned data objects
if(!$this->hasVersionField($table)) unset($manipulation[$table]['fields']['Version']);
// Grab a version number - it should be the same across all tables.
if(isset($manipulation[$table]['fields']['Version'])) {
$thisVersion = $manipulation[$table]['fields']['Version'];

View File

@ -13,6 +13,7 @@ class DataObjectLazyLoadingTest extends SapphireTest {
// These are all defined in DataObjectTest.php and VersionedTest.php
protected $extraDataObjects = array(
// From DataObjectTest
'DataObjectTest_Team',
'DataObjectTest_Fixture',
'DataObjectTest_SubTeam',
@ -31,8 +32,10 @@ class DataObjectLazyLoadingTest extends SapphireTest {
'DataObjectTest_Staff',
'DataObjectTest_CEO',
'DataObjectTest_Fan',
'VersionedLazy_DataObject',
'VersionedLazySub_DataObject',
'DataObjectTest_Play',
'DataObjectTest_Ploy',
'DataObjectTest_Bogey',
// From VersionedTest
'VersionedTest_DataObject',
'VersionedTest_Subclass',
'VersionedTest_AnotherSubclass',
@ -41,6 +44,9 @@ class DataObjectLazyLoadingTest extends SapphireTest {
'VersionedTest_WithIndexes',
'VersionedTest_PublicStage',
'VersionedTest_PublicViaExtension',
// From DataObjectLazyLoadingTest
'VersionedLazy_DataObject',
'VersionedLazySub_DataObject',
);
public function testQueriedColumnsID() {

View File

@ -156,13 +156,13 @@ class VersionedTest extends SapphireTest {
"\"VersionedTest_DataObject\".\"ID\" ASC");
// Check that page 3 has gone
$this->assertNotNull($remainingPages);
$this->assertEquals(array("Page 1", "Page 2"), $remainingPages->column('Title'));
$this->assertEquals(array("Page 1", "Page 2", "Subclass Page 1"), $remainingPages->column('Title'));
// Get all including deleted
$allPages = Versioned::get_including_deleted("VersionedTest_DataObject", "\"ParentID\" = 0",
"\"VersionedTest_DataObject\".\"ID\" ASC");
// Check that page 3 is still there
$this->assertEquals(array("Page 1", "Page 2", "Page 3"), $allPages->column('Title'));
$this->assertEquals(array("Page 1", "Page 2", "Page 3", "Subclass Page 1"), $allPages->column('Title'));
// Check that the returned pages have the correct IDs
$this->assertEquals($allPageIDs, $allPages->column('ID'));
@ -171,7 +171,7 @@ class VersionedTest extends SapphireTest {
Versioned::reading_stage("Live");
$allPages = Versioned::get_including_deleted("VersionedTest_DataObject", "\"ParentID\" = 0",
"\"VersionedTest_DataObject\".\"ID\" ASC");
$this->assertEquals(array("Page 1", "Page 2", "Page 3"), $allPages->column('Title'));
$this->assertEquals(array("Page 1", "Page 2", "Page 3", "Subclass Page 1"), $allPages->column('Title'));
// Check that the returned pages still have the correct IDs
$this->assertEquals($allPageIDs, $allPages->column('ID'));
@ -210,7 +210,7 @@ class VersionedTest extends SapphireTest {
}
public function testRollbackTo() {
$page1 = $this->objFromFixture('VersionedTest_DataObject', 'page1');
$page1 = $this->objFromFixture('VersionedTest_AnotherSubclass', 'subclass1');
$page1->Content = 'orig';
$page1->write();
$page1->publish('Stage', 'Live');
@ -227,6 +227,17 @@ class VersionedTest extends SapphireTest {
$this->assertTrue($page1->Version > $changedVersion, 'Create a new higher version number');
$this->assertEquals('orig', $page1->Content, 'Copies the content from the old version');
// check db entries
$version = DB::prepared_query("SELECT MAX(\"Version\") FROM \"VersionedTest_DataObject_versions\" WHERE \"RecordID\" = ?",
array($page1->ID)
)->value();
$this->assertEquals($page1->Version, $version, 'Correct entry in VersionedTest_DataObject_versions');
$version = DB::prepared_query("SELECT MAX(\"Version\") FROM \"VersionedTest_AnotherSubclass_versions\" WHERE \"RecordID\" = ?",
array($page1->ID)
)->value();
$this->assertEquals($page1->Version, $version, 'Correct entry in VersionedTest_AnotherSubclass_versions');
}
public function testDeleteFromStage() {
@ -320,6 +331,7 @@ class VersionedTest extends SapphireTest {
$noversion = new DataObject();
$versioned = new VersionedTest_DataObject();
$versionedSub = new VersionedTest_Subclass();
$versionedAno = new VersionedTest_AnotherSubclass();
$versionField = new VersionedTest_UnversionedWithField();
$this->assertFalse(
@ -331,8 +343,14 @@ class VersionedTest extends SapphireTest {
'The versioned ext adds an Int version field.'
);
$this->assertEquals(
'Int', $versionedSub->hasOwnTableDatabaseField('Version'),
'Sub-classes of a versioned model have a Version field.'
null,
$versionedSub->hasOwnTableDatabaseField('Version'),
'Sub-classes of a versioned model don\'t have a Version field.'
);
$this->assertEquals(
null,
$versionedAno->hasOwnTableDatabaseField('Version'),
'Sub-classes of a versioned model don\'t have a Version field.'
);
$this->assertEquals(
'Varchar', $versionField->hasOwnTableDatabaseField('Version'),
@ -924,10 +942,6 @@ class VersionedTest_Subclass extends VersionedTest_DataObject implements TestOnl
private static $db = array(
"ExtraField" => "Varchar",
);
private static $extensions = array(
"Versioned('Stage', 'Live')"
);
}
/**

View File

@ -1,25 +1,32 @@
VersionedTest_DataObject:
page1:
Title: Page 1
page2:
Title: Page 2
page3:
Title: Page 3
page2a:
Parent: =>VersionedTest_DataObject.page2
Title: Page 2a
page2b:
Parent: =>VersionedTest_DataObject.page2
Title: Page 2b
page3a:
Parent: =>VersionedTest_DataObject.page3
Title: Page 3a
page3b:
Parent: =>VersionedTest_DataObject.page3
Title: Page 3b
page1:
Title: Page 1
page2:
Title: Page 2
page3:
Title: Page 3
page2a:
Parent: =>VersionedTest_DataObject.page2
Title: Page 2a
page2b:
Parent: =>VersionedTest_DataObject.page2
Title: Page 2b
page3a:
Parent: =>VersionedTest_DataObject.page3
Title: Page 3a
page3b:
Parent: =>VersionedTest_DataObject.page3
Title: Page 3b
VersionedTest_PublicStage:
public1:
Title: 'Some page'
VersionedTest_PublicViaExtension:
public2:
Title: 'Another page'
VersionedTest_AnotherSubclass:
subclass1:
Title: 'Subclass Page 1'
AnotherField: 'Bob'