Issue #1446600 by chx: Add EntityFieldQuery (pseudo-)join support.

8.0.x
catch 2013-01-07 13:03:23 +00:00
parent c2858a0fcd
commit 9fd8864cf4
4 changed files with 328 additions and 71 deletions

View File

@ -48,32 +48,15 @@ class Query extends QueryBase {
$entity_type = $this->entityType;
$entity_info = entity_get_info($entity_type);
if (!isset($entity_info['base_table'])) {
throw new QueryException("No base table, nothing to query.");
throw new QueryException("No base table, invalid query.");
}
$configurable_fields = array_map(function ($data) use ($entity_type) {
return isset($data['bundles'][$entity_type]);
}, field_info_field_map());
$base_table = $entity_info['base_table'];
// Assemble a list of entity tables, primarily for use in
// \Drupal\field_sql_storage\Entity\Tables::ensureEntityTable().
$entity_tables = array();
$simple_query = TRUE;
// ensureEntityTable() decides whether an entity property will be queried
// from the data table or the base table based on where it finds the
// property first. The data table is prefered, which is why it gets added
// before the base table.
if (isset($entity_info['data_table'])) {
$entity_tables[$entity_info['data_table']] = drupal_get_schema($entity_info['data_table']);
$simple_query = FALSE;
}
$entity_tables[$base_table] = drupal_get_schema($base_table);
$sqlQuery = $this->connection->select($base_table, 'base_table', array('conjunction' => $this->conjunction));
$sqlQuery->addMetaData('configurable_fields', $configurable_fields);
$sqlQuery->addMetaData('entity_type', $entity_type);
// Determines the key of the column to join on. This is either the entity
// id key or the revision id key, depending on whether the entity type
// supports revisions.
$id_key = 'id';
$id_field = $entity_info['entity_keys']['id'];
$fields[$id_field] = TRUE;
if (empty($entity_info['entity_keys']['revision'])) {
@ -87,10 +70,6 @@ class Query extends QueryBase {
$revision_field = $entity_info['entity_keys']['revision'];
$fields[$revision_field] = TRUE;
$sqlQuery->addField('base_table', $revision_field);
// Now revision id is column 0 and the value column is 1.
if ($this->age == FIELD_LOAD_CURRENT) {
$id_key = 'revision';
}
}
// Now add the value column for fetchAllKeyed(). This is always the
// entity id.
@ -116,14 +95,7 @@ class Query extends QueryBase {
}
// This now contains first the table containing entity properties and
// last the entity base table. They might be the same.
$sqlQuery->addMetaData('entity_tables', $entity_tables);
$sqlQuery->addMetaData('age', $this->age);
// This contains the relevant SQL field to be used when joining entity
// tables.
$sqlQuery->addMetaData('entity_id_field', $entity_info['entity_keys'][$id_key]);
// This contains the relevant SQL field to be used when joining field
// tables.
$sqlQuery->addMetaData('field_id_field', $id_key == 'id' ? 'entity_id' : 'revision_id');
$sqlQuery->addMetaData('simple_query', $simple_query);
$this->condition->compile($sqlQuery);
if ($this->count) {

View File

@ -18,7 +18,7 @@ class QueryFactory {
$this->connection = $connection;
}
function get($entity_type, $conjunction) {
function get($entity_type, $conjunction = 'AND') {
return new Query($entity_type, $conjunction, $this->connection);
}
}

View File

@ -59,19 +59,145 @@ class Tables {
* of this in a query for a condition or sort.
*/
function addField($field, $type, $langcode) {
$parts = explode('.', $field);
$property = $parts[0];
$configurable_fields = $this->sqlQuery->getMetaData('configurable_fields');
if (!empty($configurable_fields[$property]) || substr($property, 0, 3) == 'id:') {
$field_name = $property;
$table = $this->ensureFieldTable($field_name, $type, $langcode);
// Default to .value.
$column = isset($parts[1]) ? $parts[1] : 'value';
$sql_column = _field_sql_storage_columnname($field_name, $column);
}
else {
$sql_column = $property;
$table = $this->ensureEntityTable($property, $type, $langcode);
$entity_type = $this->sqlQuery->getMetaData('entity_type');
$age = $this->sqlQuery->getMetaData('age');
// This variable ensures grouping works correctly. For example:
// ->condition('tags', 2, '>')
// ->condition('tags', 20, '<')
// ->condition('node_reference.nid.entity.tags', 2)
// The first two should use the same table but the last one needs to be a
// new table. So for the first two, the table array index will be 'tags'
// while the third will be 'node_reference.nid.tags'.
$index_prefix = '';
$specifiers = explode('.', $field);
$base_table = 'base_table';
$count = count($specifiers) - 1;
// This will contain the definitions of the last specifier seen by the
// system.
$propertyDefinitions = array();
$entity_info = entity_get_info($entity_type);
for ($key = 0; $key <= $count; $key ++) {
// If there is revision support and only the current revision is being
// queried then use the revision id. Otherwise, the entity id will do.
if (!empty($entity_info['entity_keys']['revision']) && $age == FIELD_LOAD_CURRENT) {
// This contains the relevant SQL field to be used when joining entity
// tables.
$entity_id_field = $entity_info['entity_keys']['revision'];
// This contains the relevant SQL field to be used when joining field
// tables.
$field_id_field = 'revision_id';
}
else {
$entity_id_field = $entity_info['entity_keys']['id'];
$field_id_field = 'entity_id';
}
// This can either be the name of an entity property (non-configurable
// field), a field API field (a configurable field).
$specifier = $specifiers[$key];
// First, check for field API fields by trying to retrieve the field specified.
// Normally it is a field name, but field_purge_batch() is passing in
// id:$field_id so check that first.
if (substr($specifier, 0, 3) == 'id:') {
$field = field_info_field_by_id(substr($specifier, 3));
}
else {
$field = field_info_field($specifier);
}
// If we managed to retrieve the field, process it.
if ($field) {
// Find the field column.
$column = FALSE;
if ($key < $count) {
$next = $specifiers[$key + 1];
// Is this a field column?
if (isset($field['columns'][$next]) || in_array($next, field_reserved_columns())) {
// Use it.
$column = $next;
// Do not process it again.
$key++;
}
// If there are more specifiers, the next one must be a
// relationship. Either the field name followed by a relationship
// specifier, for example $node->field_image->entity. Or a field
// column followed by a relationship specifier, for example
// $node->field_image->fid->entity. In both cases, prepare the
// property definitions for the relationship. In the first case,
// also use the property definitions for column.
if ($key < $count) {
$relationship_specifier = $specifiers[$key + 1];
$propertyDefinitions = typed_data()
->create(array('type' => $field['type'] . '_field'))
->getPropertyDefinitions();
// If the column is not yet known, ie. the
// $node->field_image->entity case then use the id source as the
// column.
if (!$column && isset($propertyDefinitions[$relationship_specifier]['settings']['id source'])) {
// If this is a valid relationship, use the id source.
// Otherwise, the code executing the relationship will throw an
// exception anyways so no need to do it here.
$column = $propertyDefinitions[$relationship_specifier]['settings']['id source'];
}
// Prepare the next index prefix.
$next_index_prefix = "$relationship_specifier.$column";
}
}
else {
// If this is the last specifier, default to value.
$column = 'value';
}
$table = $this->ensureFieldTable($index_prefix, $field, $type, $langcode, $base_table, $entity_id_field, $field_id_field);
$sql_column = _field_sql_storage_columnname($field['field_name'], $column);
}
// This is an entity property (non-configurable field).
else {
// ensureEntityTable() decides whether an entity property will be
// queried from the data table or the base table based on where it
// finds the property first. The data table is prefered, which is why
// it gets added before the base table.
$entity_tables = array();
if (isset($entity_info['data_table'])) {
$this->sqlQuery->addMetaData('simple_query', FALSE);
$entity_tables[$entity_info['data_table']] = drupal_get_schema($entity_info['data_table']);
}
$entity_tables[$entity_info['base_table']] = drupal_get_schema($entity_info['base_table']);
$sql_column = $specifier;
$table = $this->ensureEntityTable($index_prefix, $specifier, $type, $langcode, $base_table, $entity_id_field, $entity_tables);
}
// If there are more specifiers to come, it's a relationship.
if ($key < $count) {
// Computed fields have prepared their property definition already, do
// it for properties as well.
if (!$propertyDefinitions) {
// Create a relevant entity to find the definition for this
// property.
$values = array();
// If there are bundles, pick one. It does not matter which,
// properties exist on all bundles.
if (!empty($entity_info['entity keys']['bundle'])) {
$bundles = array_keys($entity_info['bundles']);
$values[$entity_info['entity keys']['bundle']] = reset($bundles);
}
$entity = entity_create($entity_type, $values);
$propertyDefinitions = $entity->$specifier->getPropertyDefinitions();
$relationship_specifier = $specifiers[$key + 1];
$next_index_prefix = $relationship_specifier;
}
// Check for a valid relationship.
if (isset($propertyDefinitions[$relationship_specifier]['constraints']['entity type']) && isset($propertyDefinitions[$relationship_specifier]['settings']['id source'])) {
// If it is, use the entity type.
$entity_type = $propertyDefinitions[$relationship_specifier]['constraints']['entity type'];
$entity_info = entity_get_info($entity_type);
// Add the new entity base table using the table and sql column.
$join_condition= '%alias.' . $entity_info['entity_keys']['id'] . " = $table.$sql_column";
$base_table = $this->sqlQuery->leftJoin($entity_info['base_table'], NULL, $join_condition);
$propertyDefinitions = array();
$key++;
$index_prefix .= "$next_index_prefix.";
}
else {
throw new QueryException(format_string('Invalid specifier @next.', array('@next' => $next)));
}
}
}
return "$table.$sql_column";
}
@ -83,18 +209,13 @@ class Tables {
* @return string
* @throws \Drupal\Core\Entity\Query\QueryException
*/
protected function ensureEntityTable($property, $type, $langcode) {
$entity_tables = $this->sqlQuery->getMetaData('entity_tables');
if (!$entity_tables) {
throw new QueryException('Can not query entity properties without entity tables.');
}
protected function ensureEntityTable($index_prefix, $property, $type, $langcode, $base_table, $id_field, $entity_tables) {
foreach ($entity_tables as $table => $schema) {
if (isset($schema['fields'][$property])) {
if (!isset($this->entityTables[$table])) {
$id_field = $this->sqlQuery->getMetaData('entity_id_field');
$this->entityTables[$table] = $this->addJoin($type, $table, "%alias.$id_field = base_table.$id_field", $langcode);
if (!isset($this->entityTables[$index_prefix . $table])) {
$this->entityTables[$index_prefix . $table] = $this->addJoin($type, $table, "%alias.$id_field = $base_table.$id_field", $langcode);
}
return $this->entityTables[$table];
return $this->entityTables[$index_prefix . $table];
}
}
throw new QueryException(format_string('@property not found', array('@property' => $property)));
@ -108,31 +229,17 @@ class Tables {
* @return string
* @throws \Drupal\Core\Entity\Query\QueryException
*/
protected function ensureFieldTable(&$field_name, $type, $langcode) {
if (!isset($this->fieldTables[$field_name])) {
// This is field_purge_batch() passing in a field id.
if (substr($field_name, 0, 3) == 'id:') {
$field = field_info_field_by_id(substr($field_name, 3));
}
else {
$field = field_info_field($field_name);
}
if (!$field) {
throw new QueryException(format_string('field @field_name not found', array('@field_name' => $field_name)));
}
// This is really necessary only for the id: case but it can't be run
// before throwing the exception.
$field_name = $field['field_name'];
protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field) {
$field_name = $field['field_name'];
if (!isset($this->fieldTables[$index_prefix . $field_name])) {
$table = $this->sqlQuery->getMetaData('age') == FIELD_LOAD_CURRENT ? _field_sql_storage_tablename($field) : _field_sql_storage_revision_tablename($field);
$field_id_field = $this->sqlQuery->getMetaData('field_id_field');
$entity_id_field = $this->sqlQuery->getMetaData('entity_id_field');
if ($field['cardinality'] != 1) {
$this->sqlQuery->addMetaData('simple_query', FALSE);
}
$entity_type = $this->sqlQuery->getMetaData('entity_type');
$this->fieldTables[$field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = base_table.$entity_id_field AND %alias.entity_type = '$entity_type'", $langcode);
$this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field AND %alias.entity_type = '$entity_type'", $langcode);
}
return $this->fieldTables[$field_name];
return $this->fieldTables[$index_prefix . $field_name];
}
protected function addJoin($type, $table, $join_condition, $langcode) {

View File

@ -0,0 +1,178 @@
<?php
/**
* @file
* Definition of Drupal\Core\Entity\Tests\EntityQueryRelationshipTest.
*/
namespace Drupal\system\Tests\Entity;
use Drupal\simpletest\WebTestBase;
/**
* Tests Entity Query API relationship functionality.
*/
class EntityQueryRelationshipTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('entity_test', 'taxonomy');
/**
* @var \Drupal\field_sql_storage\Entity\QueryFactory
*/
protected $factory;
/**
* Term entities.
*
* @var array
*/
protected $terms;
/**
* User entities.
*
* @var array
*/
public $accounts;
/**
* entity_test entities.
*
* @var array
*/
protected $entities;
/**
* The name of the taxonomy field used for test.
*
* @var string
*/
protected $fieldName;
/**
* The results returned by EntityQuery.
*
* @var array
*/
protected $queryResults;
public static function getInfo() {
return array(
'name' => 'Entity Query relationship',
'description' => 'Tests the Entity Query relationship API',
'group' => 'Entity API',
);
}
protected function setUp() {
parent::setUp();
// We want a taxonomy term reference field. It needs a vocabulary, terms,
// a field and an instance. First, create the vocabulary.
$vocabulary = entity_create('taxonomy_vocabulary', array(
'vid' => drupal_strtolower($this->randomName()),
));
$vocabulary->save();
// Second, create the field.
$this->fieldName = strtolower($this->randomName());
$field = array(
'field_name' => $this->fieldName,
'type' => 'taxonomy_term_reference',
);
$field['settings']['allowed_values']['vocabulary'] = $vocabulary->id();
field_create_field($field);
// Third, create the instance.
$instance = array(
'entity_type' => 'entity_test',
'field_name' => $this->fieldName,
'bundle' => 'entity_test',
);
field_create_instance($instance);
// Create two terms and also two accounts.
for ($i = 0; $i <= 1; $i++) {
$term = entity_create('taxonomy_term', array(
'name' => $this->randomName(),
'vid' => $vocabulary->id(),
));
$term->save();
$this->terms[] = $term;
$this->accounts[] = $this->drupalCreateUser();
}
// Create three entity_test entities, the 0th entity will point to the
// 0th account and 0th term, the 1st and 2nd entity will point to the
// 1st account and 1st term.
for ($i = 0; $i <= 2; $i++) {
$entity = entity_create('entity_test', array());
$entity->name->value = $this->randomName();
$index = $i ? 1 : 0;
$entity->user_id->value = $this->accounts[$index]->uid;
$entity->{$this->fieldName}->tid = $this->terms[$index]->tid;
$entity->save();
$this->entities[] = $entity;
}
$this->factory = drupal_container()->get('entity.query');
}
/**
* Tests querying.
*/
public function testQuery() {
// This returns the 0th entity as that's only one pointing to the 0th
// account.
$this->queryResults = $this->factory->get('entity_test')
->condition("user_id.entity.name", $this->accounts[0]->name)
->execute();
$this->assertResults(array(0));
// This returns the 1st and 2nd entity as those point to the 1st account.
$this->queryResults = $this->factory->get('entity_test')
->condition("user_id.entity.name", $this->accounts[0]->name, '<>')
->execute();
$this->assertResults(array(1, 2));
// This returns all three entities because all of them point to an
// account.
$this->queryResults = $this->factory->get('entity_test')
->exists("user_id.entity.name")
->execute();
$this->assertResults(array(0, 1, 2));
// This returns no entities because all of them point to an account.
$this->queryResults = $this->factory->get('entity_test')
->notExists("user_id.entity.name")
->execute();
$this->assertEqual(count($this->queryResults), 0);
// This returns the 0th entity as that's only one pointing to the 0th
// term (test without specifying the field column).
$this->queryResults = $this->factory->get('entity_test')
->condition("$this->fieldName.entity.name", $this->terms[0]->name)
->execute();
$this->assertResults(array(0));
// This returns the 0th entity as that's only one pointing to the 0th
// term (test with specifying the column name).
$this->queryResults = $this->factory->get('entity_test')
->condition("$this->fieldName.tid.entity.name", $this->terms[0]->name)
->execute();
$this->assertResults(array(0));
// This returns the 1st and 2nd entity as those point to the 1st term.
$this->queryResults = $this->factory->get('entity_test')
->condition("$this->fieldName.entity.name", $this->terms[0]->name, '<>')
->execute();
$this->assertResults(array(1, 2));
}
/**
* Assert the results.
*
* @param array $expected
* A list of indexes in the $this->entities array.
*/
protected function assertResults($expected) {
$this->assertEqual(count($this->queryResults), count($expected));
foreach ($expected as $key) {
$id = $this->entities[$key]->id();
$this->assertEqual($this->queryResults[$id], $id);
}
}
}