简体   繁体   English

Yii活动记录与一个记录的关系限制

[英]Yii active record relation limit to one record

I am using PHP Yii framework's Active Records to model a relation between two tables. 我正在使用PHP Yii框架的Active Records建模两个表之间的关系。 The join involves a column and a literal, and could match 2+ rows but must be limited to only ever return 1 row. 联接包含一列和一个文字,并且可以匹配2行以上,但必须限制为只能返回1行。

I'm using Yii version 1.1.13, and MySQL 5.1.something. 我正在使用Yii版本1.1.13和MySQL 5.1.something。

My problem isn't the SQL, but how to configure the Yii model classes to work in all cases. 我的问题不是SQL,而是如何配置Yii模型类以在所有情况下都能正常工作。 I can get the classes to work sometimes (simple eager loading) but not always (never for lazy loading). 我可以使类有时(简单的渴望加载)工作,但不总是(永远不要延迟加载)工作。

First I will describe the database. 首先,我将描述数据库。 Then the goal. 然后是目标。 Then I will include examples of code I've tried and why it failed. 然后,我将提供我尝试过的代码示例以及失败的原因。

Sorry for the length, this is complex and examples are necessary. 很抱歉,长度太长,很复杂,需要举一些例子。

The database: 数据库:

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

So sites has an informal a reference to field_options where: 因此, sites有一个非正式的对field_options的引用,其中:

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

The goal: 目标:

The goal is for sites to look up the relevant field_options.option_label to go with its type value. sites的目标是查找相关的field_options.option_label及其type值。 If there happens to be more than one matching row, pick only one (any one, doesn't matter which). 如果碰巧有不止一个匹配行,则仅选择一个(无论哪一个都无关紧要)。

Using SQL this is easy, I can do it 2 ways: 使用SQL很容易,我可以通过两种方式做到这一点:

  1. I can join using a subquery: 我可以使用子查询加入:
    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. Or I can use a subquery as a column reference in the select clause: 或者,我可以在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

Either way works great. 两种方法都很棒。 So how do I model this in Yii?? 那么如何在Yii中对此建模?

What I've tried so far: 到目前为止,我已经尝试过:

1. Use "on" array key in relation 1.使用“ on”数组键

I can get a simple eager lookup to work with this code: 我可以通过简单的热切查找来使用此代码:

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

This works when I load a set of Sites objects and force it to eager load type_label , eg Sites::model()->with('type_label')->findByPk(1) . 当我加载一组Sites对象并强制其渴望加载type_label ,此方法type_label ,例如Sites::model()->with('type_label')->findByPk(1)

It does not work if type_label is lazy-loaded. 如果它工作type_label是懒加载。

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

2. Force eager loading always 2.永远渴望加载

Building on #1 above, I tried forcing Yii to always to eager loading, never lazy loading: 在以上#1的基础上,我尝试迫使Yii 始终渴望加载,而不是懒加载:

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

Now everything always works when I load Sites , but it's no good because there are other models (not pictured here) that have relations that point to Sites , and those result in errors: 现在,当我加载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. Make the reference to the base table somehow relative 3.以某种方式使对基表的引用相对

If there was a way that I could refer to the base table, other than "t", that was guaranteed to point to the correct alias, that would work, eg 如果有一种方法可以引用基表,而不是“ t”,那可以保证指向正确的别名,那将是可行的,例如

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

where %%BASE_TABLE%% always refers to the correct alias for table sites . 其中%%BASE_TABLE%%始终引用表sites的正确别名。 But I know of no such token. 但是我不知道有这样的标记。

4. Add a true virtual database column 4.添加一个真实的虚拟数据库列

This way would be the best, if I could convince Yii that the table has an extra column, which should be loaded just like every other column, except the SQL is a subquery -- that would be awesome. 如果我可以说服Yii该表有一个额外的列,则这种方法将是最好的,除了SQL是一个子查询之外,应该像其他所有列一样加载该列,这真是太棒了。 But again, I don't see any way to mess with the column list, it's all done automatically. 但是再说一次,我看不出有什么办法弄乱列列表,它们都是自动完成的。

So, after all that... does anyone have any ideas? 那么,毕竟……有人有什么想法吗?

EDIT Mar 21/15: I just spent a long time investigating the possibility of subclassing parts of Yii to get the job done. 编辑3月21/15日:我只是花了很长时间研究Yii的部分子类化以完成工作的可能性。 No luck. 没运气。

I tried creating a new type of relation based on BELONGS_TO (class CBelongsToRelation ), to see if I could somehow add in context sensitivity so it could react differently depending on whether it was being lazy-loaded or not. 我尝试基于BELONGS_TO (类CBelongsToRelation )创建一种新型的关系,以查看是否可以以某种方式添加上下文相关性,以便根据是否被延迟加载而做出不同的反应。 But Yii isn't built that way. 但是Yii不是那样构建的。 There is no place where I can hook in code during query buiding from inside a relation object. 在建立关系对象的查询过程中,没有地方可以挂钩代码。 And there is also no way I can tell even what the base class is, relation objects have no link back to the parent model. 而且,即使是基类,我也无法分辨,关系对象没有链接回父模型的链接。

All of the code that assembles these queries for active records and their relations is locked up in a separate set of classes (CActiveFinder, CJoinQuery, etc.) that cannot be extended or replaced without replacing the entire AR system pretty much. 组装这些查询以获取活动记录及其关系的所有代码都锁定在一组单独的类(CActiveFinder,CJoinQuery等)中,这些类无法扩展或替换,而无需大量替换整个AR系统。 So that's out. 这样就可以了。

I then tried to see if I can create "fake" database column entries that would actually be a subquery. 然后,我尝试查看是否可以创建实际上是子查询的“伪”数据库列条目。 Answer: no. 答:不可以。 I figured out how I could add additional columns to Yii's automatically generated schema data. 我想出了如何向Yii自动生成的架构数据中添加其他列的方法。 But, 但,
a) there's no way to define a column in such a way that it can be a derived value, Yii assumes it's a column name in way too many places for that; a)无法以可以作为派生值的方式来定义列,Yii认为在太多地方它都是列名; and
b) there also doesn't appear to be any way to avoid having it try to insert/update to those columns on save. b)似乎也没有办法避免在保存时尝试插入/更新到这些列。

So it really is looking like Yii (1.x) just does not have any way to make this happen. 因此,看起来Yii(1.x)确实没有任何办法可以实现这一目标。

Limited solution provided by @eggyal in comments: @eggyal has a suggestion that will meet my needs. @eggyal在评论中提供的有限解决方案: @eggyal的建议可以满足我的需求。 He suggests creating a MySQL view table to add extra columns for each label, using a subquery to look up the value. 他建议创建一个MySQL视图表,以便为每个标签添加额外的列,并使用子查询来查找值。 To allow editing, the view would have to be tied to a separate Yii class, so the downside is everywhere in my code I need to be aware of whether I'm loading a record for reading only (must use the view's class) or read/write (must use the base table's class, does not have the extra columns). 为了进行编辑,视图必须绑定到单独的Yii类,因此代码的缺点是到处都是,我需要知道我是加载记录还是只读(必须使用视图的类)还是读取/ write(必须使用基表的类,没有多余的列)。 That said, it is a workable solution for my particular case, maybe even the only solution -- although not an answer to this question as written, so I'm not going to put it in as an answer. 也就是说,对于我的特殊情况,这是一个可行的解决方案,甚至可能是唯一的解决方案-尽管这不是书面问题的答案,所以我不会将其作为答案。

OK, after a lot of attempts, I have found a solution. 好的,经过很多尝试,我找到了解决方案。 Thanks to @eggyal for making me think about database views. 感谢@eggyal使我考虑数据库视图。

As a quick recap, my goal was: 快速回顾一下,我的目标是:

  • link one Yii model (CActiveRecord) to another using a relation() 使用Relation()将一个Yii模型(CActiveRecord)链接到另一个模型
  • the table join is complex and could match more than one row 表联接很复杂,可以匹配多个行
  • the relation must never join more than one row (ie LIMIT 1 ) 关系不得连接超过一行(即LIMIT 1

I got it to work by: 我得到它的工作方式是:

  • creating a view from the field_options base table, using SQL GROUP BY to eliminate duplicate rows 使用SQL GROUP BYfield_options基表创建视图,以消除重复的行
  • creating a separate Yii model (CActiveRecord class) for the view 为视图创建一个单独的Yii模型(CActiveRecord类)
  • using the new model/view for the relation(), not the original table 使用新的模型/视图作为Relation(),而不是原始表

Even then there were some wrinkles (maybe a Yii bug?) I had to work around. 即便如此,我还是不得不解决一些皱纹(也许是Yii虫?)。

Here are all the details: 以下是所有详细信息:

The SQL view: SQL视图:

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

This view contains only the columns I care about, and only ever one row per field/option_value pair. 该视图仅包含我关心的列,每个字段/ option_value对仅包含一行。

The Yii model class: 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;
    }
}

The Yii relation(): 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'",
            ),
        );
    }
}

And all that does the trick. 而所有的技巧。 Easy, right?!? 容易吧?!?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM