Yii 活动记录关系限制为一条记录

Yii active record relation limit to one record

我正在使用 PHP Yii 框架的活动记录来模拟两个 table 之间的关系。连接涉及一个列和一个文字,可以匹配 2+ 行,但必须仅限于 return 1 行。

我使用的是 Yii 版本 1.1.13,并且 MySQL 5.1.something.

我的问题不是 SQL,而是如何配置 Yii 模型 classes 以在所有情况下工作。我有时可以让 classes 工作(简单的预加载)但不是总是(从不用于延迟加载)。

首先我将描述数据库。然后是进球。然后我将包括我尝试过的代码示例以及失败的原因。

抱歉,篇幅太长了,这很复杂,需要示例。

数据库:

TABLE sites
columns:
    id INT
    name VARCHAR
    type VARCHAR
rows:
    id  name     type
    --  -------  -----
    1   Site A   foo
    2   Site B   bar
    3   Site C   bar

TABLE field_options
columns:
    id INT
    field VARCHAR
    option_value VARCHAR
    option_label VARCHAR
rows:
    id  field        option_value   option_label
    --  -----------  -------------  -------------
    1   sites.type   foo            Foo Style Site
    2   sites.type   bar            Bar-Like Site
    3   sites.type   bar            Bar Site

所以 sites 有一个对 field_options 的非正式引用,其中:

  1. field_options.field = 'sites.type'
  2. field_options.option_value = sites.type

目标:

目标是 sites 查找相关的 field_options.option_label 及其 type 值。如果恰好有多个匹配行,则只选择一个(任意一个,哪个无关紧要)。

使用SQL这很简单,我可以通过两种方式做到:

  1. 我可以使用子查询加入:
    SELECT
        sites.id,
        f1.option_label AS type_label
    FROM sites
    LEFT JOIN field_options AS f1 ON f1.id = (
        SELECT id FROM field_options
        WHERE
            field_options.field = 'sites.type'
            AND field_options.option_value = sites.type
        LIMIT 1
    )
  1. 或者我可以在 select 子句中使用子查询作为列引用:
    SELECT
        sites.id,
        (
            SELECT id FROM field_options
            WHERE
                field_options.field = 'sites.type'
                AND field_options.option_value = sites.type
            LIMIT 1
        ) AS type_label
    FROM sites

两种方式都很好。 那么我该如何在 Yii 中建模呢??

到目前为止我尝试过的:

1。在关系

中使用"on"数组键

我可以获得一个简单的急切查找来使用此代码:

class Sites extends CActiveRecord
{
    ...
    public function relations()
    {
        return array(
            'type_option' => array(
                self::BELONGS_TO,
                'FieldOptions', // that's the class for field_options
                '', // no normal foreign key
                'on' => "type_option.id = (SELECT id FROM field_options WHERE field = 'sites.type' AND option_value = t.type LIMIT 1)",
            ),
        );
    }
}

这在我加载一组 Sites 对象并强制它预先加载 type_label 时起作用,例如Sites::model()->with('type_label')->findByPk(1).

如果 type_label 是延迟加载的,它 不会 工作。

$site = Sites::model()->findByPk(1);
$label = $site->type_option->option_label; // ERROR: column t.type doesn't exist

2。始终强制预先加载

基于上面的#1,我尝试强制 Yii 总是 预先加载,从不延迟加载:

class Sites extends CActiveRecord
{
    public function relations()
    {
        ....
    }
    public function defaultScope()
    {
        return array(
            'with' => array( 'type_option' ),
        );
    }
}

现在,当我加载 Sites 时一切正常,但这并不好,因为还有其他模型(此处未显示)具有指向 Sites 的关系,并且会导致错误:

$site = Sites::model()->findByPk(1);
$label = $site->type_option->option_label; // works now

$other = OtherModel::model()->with('site_relation')->findByPk(1); // ERROR: column t.type doesn't exist, because 't' refers to OtherModel now

3。以某种方式引用基数 table 相对

如果有一种方法可以让我引用基础 table,而不是 "t",那可以保证指向正确的别名,那是可行的,例如

'on' => "type_option.id = (SELECT id FROM field_options WHERE field = 'sites.type' AND option_value = %%BASE_TABLE%%.type LIMIT 1)",

其中 %%BASE_TABLE%% 始终指代 table sites 的正确别名。但我知道没有这样的标记。

4。添加一个真正的虚拟数据库列

这种方式最好,如果我能让 Yii 相信 table 有一个额外的列,应该像其他列一样加载,除了 SQL 是一个子查询 - - 那将是真棒。但同样,我看不出有什么方法可以扰乱列列表,这一切都是自动完成的。

所以,毕竟……有人有什么想法吗?

编辑 2015 年 3 月 21 日: 我只是花了很长时间研究子 class 部分 Yii 来完成工作的可能性。运气不好。

我尝试创建一种基于 BELONGS_TO (class CBelongsToRelation) 的新型关系,看看我是否可以以某种方式添加上下文敏感性,以便它可以根据不同的情况做出不同的反应它是否被延迟加载。但是 Yii 不是那样构建的。在从关系对象内部进行查询构建期间,没有地方可以挂钩代码。而且我也无法分辨基础 class 是什么,关系对象没有 link 返回父模型。

为活动记录及其关系组合这些查询的所有代码都被锁定在一组单独的 classes(CActiveFinder、CJoinQuery 等)中,如果不替换整个AR系统差不多。这样就结束了。

然后我尝试查看是否可以创建 "fake" 实际上是子查询的数据库列条目。答:没有。我想出了如何向 Yii 自动生成的模式数据添加额外的列。但是,
a) 无法以派生值的方式定义列,Yii 假设它是一个列名,在太多地方;和
b) 似乎也没有任何方法可以避免在保存时尝试 insert/update 那些列。

所以它真的看起来像 Yii (1.x) 只是没有任何方法可以实现这一点。

@eggyal 在评论中提供的有限解决方案: @eggyal 有一个建议可以满足我的需要。他建议创建一个 MySQL 视图 table 来为每个标签添加额外的列,使用子查询来查找值。为了允许编辑,视图必须绑定到一个单独的 Yii class,所以缺点在我的代码中无处不在我需要知道我是否正在加载一条只读记录(必须使用视图的class) 或 read/write(必须使用基础 table 的 class,没有多余的列)。也就是说,对于我的特殊情况,这是一个可行的解决方案,甚至可能是唯一的解决方案——尽管不是对这个问题的回答,所以我不打算把它作为答案。

好的,经过很多的尝试,我找到了解决办法。感谢@eggyal 让我思考数据库视图。

快速回顾一下,我的目标是:

  • link 一个 Yii 模型 (CActiveRecord) 到另一个使用 relation()
  • table 连接很复杂,可以匹配多行
  • 该关系绝不能连接超过一行(即 LIMIT 1

我通过以下方式让它工作:

  • field_options 基础 table 创建视图,使用 SQL GROUP BY 消除重复行
  • 为视图创建一个单独的 Yii 模型 (CActiveRecord class)
  • 为 relation() 使用新的 model/view,而不是原来的 table

即使那样也有一些问题(也许是 Yii 的错误?)我不得不变通。

以下是所有详细信息:

SQL 视图:

CREATE VIEW field_options_distinct AS
    SELECT
        field,
        option_value,
        option_label
    FROM
        field_options
    GROUP BY
        field,
        option_value
;

此视图仅包含我关心的列,并且每 field/option_value 对仅包含一行。

Yii 模型class:

class FieldOptionsDistinct extends CActiveRecord
{
    public function tableName()
    {
        return 'field_options_distinct'; // the view
    }

    /*
        I found I needed the following to override Yii's default table data.
        The view doesn't have a primary key, and that confused Yii's AR finding system
        and resulted in a PHP "invalid foreach()" error.

        So the code below works around it by diving into the Yii table metadata object
        and manually setting the primary key column list.
    */
    private $bMetaDataSet = FALSE;
    public function getMetaData()
    {
        $oMetaData = parent::getMetaData();
        if (!$this->bMetaDataSet) {
            $oMetaData->tableSchema->primaryKey = array( 'field', 'option_value' );
            $this->bMetaDataSet = TRUE;
        }
        return $oMetaData;
    }
}

Yii 关系():

class Sites extends CActiveRecord
{
    // ...

    public function relations()
    {
        return (
            'type_option' => array(
                self::BELONGS_TO,
                'FieldOptionsDistinct',
                array(
                    'type' => 'option_value',
                ),
                'on' => "type_option.field = 'sites.type'",
            ),
        );
    }
}

所有这一切都奏效了。简单吧?!?