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

时间:2015-03-20 23:17:42

标签: php mysql sql yii orm

我正在使用PHP Yii框架的Active Records来模拟两个表之间的关系。连接涉及一个列和一个文字,可以匹配2行以上,但必须限制为只返回1行。

我正在使用Yii版本1.1.13和MySQL 5.1.something。

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

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

对不起,这很复杂,需要举例说明。

数据库:

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

所以sitesfield_options有一个非正式的引用,其中:

  1. field_options.field = 'sites.type'
  2. field_options.option_value = sites.type
  3. 目标:

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

    使用SQL很容易,我可以通过两种方式实现:

    1. 我可以使用子查询加入:
    2.     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子句中使用子查询作为列引用:
      2.     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个对象并将其强制加载到{1}}时,这是有效的。 type_label

        如果Sites::model()->with('type_label')->findByPk(1)延迟加载,工作。

        type_label

        2。始终强制加载

        在上面#1的基础上,我试图强迫Yii 总是以急切加载,从不延迟加载:

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

        现在一切都在我加载class Sites extends CActiveRecord { public function relations() { .... } public function defaultScope() { return array( 'with' => array( 'type_option' ), ); } } 时总是有效,但这并不好,因为有其他模型(这里没有图示)的关系指向Sites,这些导致错误:

        Sites

        3。以某种方式对基表进行引用

        如果有一种方法可以引用基表而不是“t”,那么可以保证指向正确的别名,这样就可以了,例如。

        $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
        

        其中'on' => "type_option.id = (SELECT id FROM field_options WHERE field = 'sites.type' AND option_value = %%BASE_TABLE%%.type LIMIT 1)", 始终引用表%%BASE_TABLE%%的正确别名。但我知道没有这样的象征。

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

        这种方式将是最好的,如果我可以说服Yii该表有一个额外的列,应该像其他每一列一样加载,除了SQL是一个子查询 - 这将是非常棒的。但同样,我没有看到任何混淆列列表的方法,它都是自动完成的。

        所以,毕竟......有没有人有任何想法?

        编辑Mar 21/15:我花了很长时间调查Yii的部分子类化以完成工作的可能性。没有运气。

        我尝试基于sites(类BELONGS_TO)创建一种新类型的关系,看看我是否可以某种方式添加上下文敏感度,以便它可以根据它是否是懒惰而做出不同的反应是否加载。但Yii不是那样构建的。在查询从关系对象内部建立时,我无法在代码中挂钩。而且我也无法分辨基类是什么,关系对象没有链接回父模型。

        所有为活动记录及其关系组装这些查询的代码都被锁定在一组单独的类(CActiveFinder,CJoinQuery等)中,这些类无法在不替换整个AR系统的情况下进行扩展或替换。那就好了。

        然后我试着看看我是否可以创建实际上是子查询的“假”数据库列条目。答:没有。我想出了如何在Yii自动生成的架构数据中添加其他列。但是,
        a)没有办法以这样的方式定义一个列,它可以是一个派生值,Yii假设它的列名太多了;和
        b)似乎没有任何方法可以避免它尝试在保存时插入/更新这些列。

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

        @eggyal在评论中提供的有限解决方案: @eggyal有一个符合我需求的建议。他建议创建一个MySQL视图表,为每个标签添加额外的列,使用子查询来查找值。为了允许编辑,视图必须绑定到一个单独的Yii类,因此我的代码中的缺点是我需要知道我是否正在加载只读的记录(必须使用视图的类)或读取/ write(必须使用基表的类,没有额外的列)。也就是说,对于我的特定案例来说,它是一个可行的解决方案,甚至可能是唯一的解决方案 - 虽然不是这个问题的答案,但我不打算把它作为答案。

1 个答案:

答案 0 :(得分:0)

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

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

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

我得到了它的工作:

  • 使用SQL field_optionsGROUP BY基表创建视图以消除重复行
  • 为视图创建单独的Yii模型(CActiveRecord类)
  • 使用关系()的新模型/视图,而不是原始表

即便如此,也有一些皱纹(也许是Yii虫子?)我不得不解决。

以下是所有细节:

SQL视图:

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

此视图仅包含我关注的列,每个字段/ option_value对只有一行。

Yii模型类:

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'",
            ),
        );
    }
}

所有这一切都可以解决问题。容易,对吧?!?