【问题标题】:Return JSON array of associated entity IDs in CakePHP 3.0 belongsToMany relationship在 CakePHP 3.0 belongsToMany 关系中返回关联实体 ID 的 JSON 数组
【发布时间】:2015-07-06 16:08:10
【问题描述】:

如果我有两个表,比如foosbars,在多对多关系中(在这种情况下通过bars_foos 连接表),CakePHP 3.0 中包含的最佳方法是什么RequestHandler 通过 _serialize 属性返回的 JSON 中与每个 Foo 关联的 Bars 的 ID 的数组?

具体来说,我希望能够访问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 查找和查询与当前 Foo 关联的 Bar 实体的 bars_foos 连接表,并将它们作为 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 控制器中向索引查询添加连接,以聚合连接表。这里的关键组件如下所示:

// 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']);

当没有任何关联的 Bars 时,您可以将 coalesce(..., '') 包裹在 string_agg(...) 周围以返回 "[]" 而不是 NULL,并在 string_agg 的第一个参数的开头抛出 distinct 如果多个连接在bar_id 列中返回重复项,但这是基本思想。

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

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

【问题讨论】:

    标签: cakephp has-and-belongs-to-many cakephp-3.0


    【解决方案1】:

    结果格式化程序

    我可能会使用containmentsresult 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;
                });
            }
        );      
    

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

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

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

    进一步自动化

    当然,您当然也可以进一步自动执行此操作,例如通过检查表关联并处理所有包含 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;
        });
    }
    

    另见

    【讨论】:

    • 我现在正在使用手机,所以稍后会更详细地测试它,但它看起来很棒,谢谢。我之前确实尝试过使用“包含”,但没有结果格式化程序,并且无法找到一种方法来不在响应中包含 Bar 对象的数组。 unset($row['bars']) 是一个合适的解决方案吗?
    • @Michael 当然,如果您不需要关联的实际结果,只需取消设置它们就可以了。在这种情况下,您可能还想仅选择关联的 id 字段 (book.cakephp.org/3.0/en/orm/…)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-07
    相关资源
    最近更新 更多