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
189 lines
6.6 KiB
PHP
189 lines
6.6 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Calculate an Aggregate on a particular field of a particular DataObject type (possibly with
|
|
* an additional filter before the aggregate)
|
|
*
|
|
* Implemented as a class to provide a semi-DSL method of calculating Aggregates. DataObject has a function
|
|
* that will create & return an instance of this class with the DataObject type and filter set,
|
|
* but at that point we don't yet know the aggregate function or field
|
|
*
|
|
* This class captures any XML_val or unknown call, and uses that to get the field & aggregate function &
|
|
* then return the result
|
|
*
|
|
* Two ways of calling
|
|
*
|
|
* $aggregate->XML_val(aggregate_function, array(field)) - For templates
|
|
* $aggregate->aggregate_function(field) - For PHP
|
|
*
|
|
* Aggregate functions are uppercased by this class, but are otherwise assumed to be valid SQL functions. Some
|
|
* examples: Min, Max, Avg
|
|
*
|
|
* Aggregates are often used as portions of a cacheblock key. They are therefore cached themselves, in the 'aggregate'
|
|
* cache, although the invalidation logic prefers speed over keeping valid data.
|
|
* The aggregate cache is cleared through {@link DataObject::flushCache()}, which in turn is called on
|
|
* {@link DataObject->write()} and other write operations.
|
|
* This means most write operations to the database will invalidate the cache correctly.
|
|
* Use {@link Aggregate::flushCache()} to manually clear.
|
|
*
|
|
* NOTE: The cache logic uses tags, and so a backend that supports tags is required. Currently only the File
|
|
* backend (and the two-level backend with the File backend as the slow store) meets this requirement
|
|
*
|
|
* @deprecated 3.1 Use DataList to aggregate data
|
|
*
|
|
* @author hfried
|
|
* @package framework
|
|
* @subpackage core
|
|
*/
|
|
class Aggregate extends ViewableData {
|
|
|
|
private static $cache = null;
|
|
|
|
/** Build & cache the cache object */
|
|
protected static function cache() {
|
|
return self::$cache ? self::$cache : (self::$cache = SS_Cache::factory('aggregate'));
|
|
}
|
|
|
|
/**
|
|
* Clear the aggregate cache for a given type, or pass nothing to clear all aggregate caches.
|
|
* {@link $class} is just effective if the cache backend supports tags.
|
|
*/
|
|
public static function flushCache($class=null) {
|
|
$cache = self::cache();
|
|
$capabilities = $cache->getBackend()->getCapabilities();
|
|
if($capabilities['tags'] && (!$class || $class == 'DataObject')) {
|
|
$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('aggregate'));
|
|
} elseif($capabilities['tags']) {
|
|
$tags = ClassInfo::ancestry($class);
|
|
foreach($tags as &$tag) {
|
|
$tag = preg_replace('/[^a-zA-Z0-9_]/', '_', $tag);
|
|
}
|
|
$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, $tags);
|
|
} else {
|
|
$cache->clean(Zend_Cache::CLEANING_MODE_ALL);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @deprecated 3.1 Use DataList to aggregate data
|
|
*
|
|
* @param string $type The DataObject type we are building an aggregate for
|
|
* @param string $filter (optional) An SQL filter to apply to the selected rows before calculating the aggregate
|
|
*/
|
|
public function __construct($type, $filter = '') {
|
|
Deprecation::notice('3.1', 'Call aggregate methods on a DataList directly instead. In templates'
|
|
. ' an example of the new syntax is <% cached List(Member).max(LastEdited) %> instead'
|
|
. ' (check partial-caching.md documentation for more details.)');
|
|
$this->type = $type;
|
|
$this->filter = $filter;
|
|
parent::__construct();
|
|
}
|
|
|
|
/**
|
|
* Build the SQLSelect to calculate the aggregate
|
|
* This is a seperate function so that subtypes of Aggregate can change just this bit
|
|
* @param string $attr - the SQL field statement for selection (i.e. "MAX(LastUpdated)")
|
|
* @return SQLSelect
|
|
*/
|
|
protected function query($attr) {
|
|
$query = DataList::create($this->type)->where($this->filter);
|
|
$query->setSelect($attr);
|
|
$query->setOrderBy(array());
|
|
$singleton->extend('augmentSQL', $query);
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Entry point for being called from a template.
|
|
*
|
|
* This gets the aggregate function
|
|
*
|
|
*/
|
|
public function XML_val($name, $args = null, $cache = false) {
|
|
$func = strtoupper( strpos($name, 'get') === 0 ? substr($name, 3) : $name );
|
|
$attribute = $args ? $args[0] : 'ID';
|
|
|
|
$table = null;
|
|
|
|
foreach (ClassInfo::ancestry($this->type, true) as $class) {
|
|
$fields = DataObject::database_fields($class, false);
|
|
if (array_key_exists($attribute, $fields)) { $table = $class; break; }
|
|
}
|
|
|
|
if (!$table) user_error("Couldn't find table for field $attribute in type {$this->type}", E_USER_ERROR);
|
|
|
|
$query = $this->query("$func(\"$table\".\"$attribute\")");
|
|
|
|
// Cache results of this specific SQL query until flushCache() is triggered.
|
|
$sql = $query->sql($parameters);
|
|
$cachekey = sha1($sql.'-'.var_export($parameters, true));
|
|
$cache = self::cache();
|
|
|
|
if (!($result = $cache->load($cachekey))) {
|
|
$result = (string)$query->execute()->value(); if (!$result) $result = '0';
|
|
$cache->save($result, null, array('aggregate', preg_replace('/[^a-zA-Z0-9_]/', '_', $this->type)));
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Entry point for being called from PHP.
|
|
*/
|
|
public function __call($method, $arguments) {
|
|
return $this->XML_val($method, $arguments);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A subclass of Aggregate that calculates aggregates for the result of a has_many query.
|
|
*
|
|
* @author hfried
|
|
* @package framework
|
|
* @subpackage core
|
|
*/
|
|
class Aggregate_Relationship extends Aggregate {
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DataObject $object The object that has_many somethings that we're calculating the aggregate for
|
|
* @param string $relationship The name of the relationship
|
|
* @param string $filter (optional) An SQL filter to apply to the relationship rows before calculating the
|
|
* aggregate
|
|
*/
|
|
public function __construct($object, $relationship, $filter = '') {
|
|
$this->object = $object;
|
|
$this->relationship = $relationship;
|
|
|
|
$this->has_many = $object->has_many($relationship);
|
|
$this->many_many = $object->many_many($relationship);
|
|
|
|
if (!$this->has_many && !$this->many_many) {
|
|
user_error("Could not find relationship $relationship on object class {$object->class} in"
|
|
. " Aggregate Relationship", E_USER_ERROR);
|
|
}
|
|
|
|
parent::__construct($this->has_many ? $this->has_many : $this->many_many[1], $filter);
|
|
}
|
|
|
|
protected function query($attr) {
|
|
if ($this->has_many) {
|
|
$query = $this->object->getComponentsQuery($this->relationship, $this->filter);
|
|
}
|
|
else {
|
|
$query = $this->object->getManyManyComponentsQuery($this->relationship, $this->filter);
|
|
}
|
|
|
|
$query->setSelect($attr);
|
|
$query->setGroupBy(array());
|
|
|
|
$singleton = singleton($this->type);
|
|
$singleton->extend('augmentSQL', $query);
|
|
|
|
return $query;
|
|
}
|
|
}
|