Return JSON CakePHP 3.0 中关联实体 ID 的数组 belongsToMany 关系

Return JSON array of associated entity IDs in CakePHP 3.0 belongsToMany relationship

如果我有两个 table,比如 foosbars,在多对多关系中(通过 bars_foos 加入 table,在这种情况下),在 CakePHP 3.0 中,最好的方法是在 RequestHandler 通过_serialize 属性?

具体来说,我希望能够访问 site/foos.json 并看到类似这样的内容:

{
  "foos": [
    {
      "id": 1,
      "name": "A foo",
      ...
      "bar_ids": [1, 3, 5]
    },
    {
      "id": 2,
      "name": "Foo too",
      ...
      "bar_ids": [2]
  ]
}

作为奖励,如果每个 Foo 也属于 ToMany Bazzes,这应该仍然有效,所以我最终得到,例如,

      ...
      "bar_ids": [1, 3, 5],
      "baz_ids": [37, 42]
      ...

是否有一种简单易行的方法可以轻松应用于许多模型并且不会导致过多的数据库查询?


到目前为止我尝试过的:

我已经设法通过两种方式大致实现了这一点,但要快速轻松地推广到许多模型,这两种方式似乎都不成熟。我对添加新查询或查询字段以获取可能已经存在于 CakePHP 中某处的信息的效率持谨慎态度,前提是我只能找到正确的语法来显示它。

虚拟字段

一种方法是通过 _getBarIds() 函数创建一个虚拟字段并在 Foo 实体中设置 protected $_virtual = ['bar_ids']。该函数使用 TableRegistry 查找和查询 bars_foos 连接 table 以获取与当前 Foo 关联的 Bar 实体,并将它们 return 作为 PHP 数组。关键功能大致是这样的:

// In src/Model/Entity/Foo.php, protected function _getBarIds:
$bars_foos = TableRegistry::get('Bookmarks_Tags');
$these_bars = $bars_foos->find()
    ->select(['bar_id'])
    ->where(['foo_id' => $this->id]);

这工作得相当好,但是为我的数据库中的每个关联手动添加这些 _get<Model>Ids() 函数并不吸引人,并且为我要检索的每一行执行一个或多个新的数据库命中肯定不是理想的.

控制器查询中的聚合

另一种方法是将连接添加到 Foos 控制器中的索引查询以聚合连接 tables。这里的关键组件如下所示:

// In src/Controller/FoosController.php, public function index:
$query = $this->Foos->find('all')
    ->select(['Foos.id', 'Foos.name',
      'bar_ids' => "'[' || string_agg(BarsFoos.bar_id::text, ',') || ']'"
    ])
    ->autofields(false) // don't include other fields from bars_foos
    ->join([
      'table' => 'bars_foos',
      'alias' => 'BarsFoos',
      'type' => 'LEFT',
      'conditions' => 'BarsFoos.foo_id = Foos.id'
    ])
    ->group(['Foos.id', 'Foos.name']);
$this->set('foos', $query);
$this->set('_serialize', ['foos']);

您可以在 string_agg(...) 到 return "[]" 周围包裹一个 coalesce(..., '') 而不是在没有任何关联的条形图时使用 NULL,然后抛出一个 distinct 如果多个连接在 bar_id 列中 return 重复,则在 string_agg 的第一个参数的开头,但这是基本思想。

这种方法对我更有吸引力,因为它在对数据库的一次查询中获取所有内容,但它也感觉有点过于手动,并且还有一个缺点,即 return 将数组作为字符串,以便在 JSON 中它显示为 "bar_ids": "[1, 3, 5]",用引号括起应该是数组而不是字符串的内容(无论如何,在我当前使用 PostgreSQL 的 string_agg 的实现中)。

这似乎不是一个特别疯狂的功能——我是不是错过了一些明显的东西,它提供了一种更简单的方法来完成一般的任务?

结果格式化程序

我可能会使用 containments and result formatters。包含关联只需要每个关联一个附加查询来检索关联数据,然后您可以使用它来创建您喜欢的任何附加属性。

这是一个基本示例,它应该很容易解释,它只是遍历所有检索到的行,并添加一个包含关联记录 ID 的新 属性。

$query = $this->Foos
    ->find()
    ->contain(['Bars'])
    ->formatResults(
        function ($results) {
            /* @var $results \Cake\Datasource\ResultSetInterface|\Cake\Collection\CollectionInterface */
            return $results->map(function ($row) {
                /* @var $row array|\Cake\DataSource\EntityInterface */
                $ids = [];
                foreach ($row['bars'] as $barRow) {
                    $ids[] = $barRow['id'];
                }
                $row['bar_ids'] = $ids;
                return $row;
            });
        }
    );      

可重复使用的格式化程序和自定义查找器

为了保持干爽,您可以让您的 table 提供格式化程序,甚至将所有这些都包装在自定义查找器中。

public function formatWhateverResults($results) {
    // ...
}

public function findWhatever(Query $query, array $options)
{
    $query
        ->contain(['Bars'])
        ->formatResults([$this, 'formatWhateverResults']);
    return $query;
}
$query = $this->Foos->find('whatever');

进一步自动化

你当然也可以进一步自动化,例如通过检查 tables 关联并处理所有包含的 belongsToMany 的 ID,比如

/**
 * @param \Cake\Datasource\ResultSetInterface|\Cake\Collection\CollectionInterface $results
 */
public function formatWhateverResults($results) {
    $associations = $this->associations()->type('BelongsToMany');
    return $results->map(function ($row) use ($associations) {
        /* @var $row array|\Cake\DataSource\EntityInterface */
        foreach ($associations as $assoc) {
            /* @var $assoc \Cake\ORM\Association */
            $property = $assoc->property();
            if (isset($row[$property])) {
                $ids = [];
                foreach ($row[$property] as $assocRow) {
                    $ids[] = $assocRow['id'];
                }
                $row[Inflector::singularize($property) . '_ids'] = $ids;
            }
        }
        return $row;
    });
}

另见