Как вложить объединения с CakePHP? - PullRequest
6 голосов
/ 06 мая 2010

Я пытаюсь себя вести. Таким образом, вместо использования следующего синтаксиса 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) с помощью построителя операторов. Можно ли использовать подобный трюк здесь?

Ответы [ 2 ]

2 голосов
/ 06 мая 2010

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

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

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

Мне сказали не редактировать материал в 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})");
}
1 голос
/ 06 мая 2010

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

TableA hasMany TableB.
TableA hasMany TableC.

TableB belongsTo TableA.

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

TableD hasMany TableC. (might be hasOne)

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

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

Если вам нужно выбрать определенные поля, вам нужно будет указать их в параметре содержимого, например, здесь я ограничиваю возвращаемые поля 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'
        )
      )
    )
  )

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...