Как вложить соединения с CakePHP?


Я пытаюсь вести себя прилично. Итак, вместо использования следующего синтаксиса SQL:

select *
from   tableA INNER JOIN
       tableB on tableA.id = tableB.tableA_id LEFT OUTER JOIN
       ( tableC INNER JOIN tableD on tableC.tableD_id = tableD.id)
       on tableC.tableA_id = tableA.id

Я хотел бы использовать CakePHP model->find(). Это позволит мне также использовать Paginator, поскольку, насколько я понимаю, это не будет работать с пользовательскими SQL-запросами (если только вы не закодируете один запрос на разбиение на страницы для модели, которая кажется мне немного негибкой).

Что я пробовал до сих пор:

/* inside tableA_controller.php, inside an action, e.g. "view" */
$this->paginate['recursive'] = -1; # suppress model associations for now
$this->paginate['joins'] = array(
    array(
        'table' => 'tableB',
        'alias' => 'TableB',
        'type'  => 'inner',
        'conditions' => 'TableB.tableA_id = TableA.id',
    ),
    array(
        'table' => 'tableC',
        'alias' => 'TableC',
        'type'  => 'left',
        'conditions' => 'TableC.tableA_id = TableA.id',
        'joins' = array( # this would be the obvious way to do it, but doesn't work
            array(
                'table' => 'tableD',
                'alias' => 'TableD',
                'type'  => 'inner',
                'conditions' => 'TableC.tableD_id = TableD.id'
            )
        )
    )
)

То есть вложение соединений в структуру. Но это не работает (CakePHP просто игнорирует вложенные 'joins' элемент, который был примерно таким, как я ожидал, но печальным.

Я видел подсказки в комментариях о том, как выполнять подзапросы (в предложении where) с помощью построителя инструкций. Можно ли использовать подобный трюк здесь?

Author: Daren Thomas, 2010-05-06

2 answers

Оказывается, ты не можешь. По крайней мере, не с синтаксисом, приведенным выше, и не с CakePHP 1.2.6. Я просмотрел источник (ура! для открытия фреймворков с исходным кодом!) и нашел файл cake/libs/model/datasources/dbo_source.php, содержащий код для соединений.

Все начинается с DboSource::renderStatement(), который выполняет неглубокий обход массива $query['joins'], заменяя эти определения соединений фрагментами SQL с помощью DboSource::buildJoinStatement($join), который выполняет некоторую очистку аргументов (заполнение пробелов и т. Д.), А затем вызывает DboSource::renderJoinStatement для создания фрагмента SQL из одно предложение соединения.

я: Это должно быть легко исправить!

Мне сказали не редактировать материал в cake/libs, поэтому вместо этого я скопировал файл dbo_source.php в app/models/datasources/ для редактирования. Затем я взял свой топор и преобразовал неглубокую прогулку массива $query['joins'] в DboSource::renderStatement() в новый метод DboSource::buildJoinStatementArray(), в результате чего появились эти два метода:

function buildStatement($query, $model) {
    $query = array_merge(array('offset' => null, 'joins' => array()), $query);

    # refactored (extract method) to make recursion easier
    $query['joins'] = $this->buildJoinStatementArray($query['joins']);

    return $this->renderStatement('select', array(
        'conditions' => $this->conditions($query['conditions'], true, true, $model),
        'fields' => implode(', ', $query['fields']),
        'table' => $query['table'],
        'alias' => $this->alias . $this->name($query['alias']),
        'order' => $this->order($query['order']),
        'limit' => $this->limit($query['limit'], $query['offset']),
        'joins' => implode(' ', $query['joins']),
        'group' => $this->group($query['group'])
    ));
}
/**
 * Replaces the join statement array syntax with SQL join clauses.
 */
function buildJoinStatementArray($joins) {
    if (!empty($joins)) {
        $count = count($joins);
        for ($i = 0; $i < $count; $i++) {
            if (is_array($joins[$i])) {
                $joins[$i] = $this->buildJoinStatement($joins[$i]); # $joins[$i] now contains something like "LEFT JOIN users As User on User.group_id = Group.id"
            }
        }
    }
    return $joins;
}

Как только у меня появился DboSource::buildJoinStatementArray(), пришло время изменить DboSource::buildJoinStatement() - все, что я сделал, это добавил проверку для $data['joins'] и альтернативный метод рендеринга для этого случай:

function buildJoinStatement($join) {
    $data = array_merge(array(
        'type' => null,
        'alias' => null,
        'table' => 'join_table',
        'conditions' => array()
    ), $join);

    if (!empty($data['alias'])) {
        $data['alias'] = $this->alias . $this->name($data['alias']);
    }
    if (!empty($data['conditions'])) {
        $data['conditions'] = trim($this->conditions($data['conditions'], true, false));
    }

    # allow for nested joins
    if (!empty($data['joins']) and is_array($data['joins'])) {
        $data['joins'] = $this->buildJoinStatementArray($data['joins']);
        return $this->renderNestedJoinStatement($data);
    }
    else
    {
        return $this->renderJoinStatement($data);
    }
}

Новый метод renderNestedJoinStatement() очень похож на DboSource::renderJoinStatement():

/**
 * Renders a final SQL JOIN that contains nested join statements
 *
 * @param array $data
 * @return string
 */
function renderNestedJoinStatement($data) {
    extract($data);
    $nestedJoins = implode(' ', $joins);
    return trim("{$type} JOIN ({$table} {$alias} {$nestedJoins})ON ({$conditions})");
}
 2
Author: Daren Thomas, 2010-05-06 14:36:29

Если я правильно понял, у вас есть следующие отношения (надеюсь, в ваших моделях):

TableA hasMany TableB.
TableA hasMany TableC.

TableB belongsTo TableA.

TableC belongsTo TableA.
TableC belongsTo TableD. (might be hasOne)

TableD hasMany TableC. (might be hasOne)

Если вы используете сдерживаемое поведение (я очень рекомендую его и устанавливаю его на уровне app_model для наследования всеми моделями), я думаю, вы можете сделать что-то подобное...

$this->TableA->find(
  'all',
  array(
    'contain' => array(
      'TableB',
      'TableC' => array(
        'TableD'
      )
    ),
    'conditions' => array(...),
    'order' => array(...)
  )
);

Если вам нужно выбрать определенные поля, вам нужно будет указать их в параметре contain, например, здесь я ограничиваю возвращаемые поля TableB:

$this->TableA->find(
  'all',
  array(
    'contain' => array(
      'TableB' => array(
        'fields' => array(
          'field_1',
          'field_2'
        ),
      ),
      'TableC' => array(
        'TableD'
      )
    ),
    'conditions' => array(...),
    'order' => array(...)
  )
);

Возвращенные данные должно быть так:

  [0] => array(
    [TableA] => array(
      [id] => 12,
      [name] => 'Foo'
    ),
    [TableB] => array(
      [id] => 23,
      [table_a_id] => 12,
      [name] => 'Bah'
    ),
    [TableC] => array(
      [id] => 45,
      [table_a_id] => 12,
      [table_d_id] => 67,
      [name] => 'Woo',
      [TableD] => array(
        [0] => array(
          [id] => 67,
          [table_a_id] => 12,
          [name] => 'Wah'
        )
      )
    )
  )

Однако я никогда не делал этого там, где вложенная таблица является родительской для контейнера (табличной и табличной), поэтому это может не сработать, но, вероятно, стоит попробовать.

 0
Author: ianmjones, 2010-05-06 12:38:50