[英]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. 很抱歉,长度太长,很复杂,需要举一些例子。
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
的引用,其中:
field_options.field = 'sites.type'
and field_options.field = 'sites.type'
和 field_options.option_value = sites.type
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很容易,我可以通过两种方式做到这一点:
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
)
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中对此建模?
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
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
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. 但是我不知道有这样的标记。
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: 快速回顾一下,我的目标是:
LIMIT 1
) 关系不得连接超过一行(即LIMIT 1
) I got it to work by: 我得到它的工作方式是:
field_options
base table, using SQL GROUP BY
to eliminate duplicate rows 使用SQL GROUP BY
从field_options
基表创建视图,以消除重复的行 Even then there were some wrinkles (maybe a Yii bug?) I had to work around. 即便如此,我还是不得不解决一些皱纹(也许是Yii虫?)。
Here are all the details: 以下是所有详细信息:
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对仅包含一行。
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;
}
}
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.