我正在使用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
所以sites
对field_options
有一个非正式的引用,其中:
field_options.field = 'sites.type'
和field_options.option_value = sites.type
目标是让sites
查找相关field_options.option_label
以获取其type
值。如果恰好有多个匹配的行,则只选择一个(任何一个,无关紧要)。
使用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
无论哪种方式都很棒。 那么如何在Yii中对此进行建模?
我可以通过简单的急切查找来使用此代码:
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
在上面#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
如果有一种方法可以引用基表而不是“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%%
的正确别名。但我知道没有这样的象征。
这种方式将是最好的,如果我可以说服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(必须使用基表的类,没有额外的列)。也就是说,对于我的特定案例来说,它是一个可行的解决方案,甚至可能是唯一的解决方案 - 虽然不是这个问题的答案,但我不打算把它作为答案。
答案 0 :(得分:0)
好的,经过很多的尝试,我找到了解决方案。感谢@eggyal让我考虑数据库视图。
快速回顾一下,我的目标是:
LIMIT 1
)我得到了它的工作:
field_options
从GROUP BY
基表创建视图以消除重复行即便如此,也有一些皱纹(也许是Yii虫子?)我不得不解决。
以下是所有细节:
CREATE VIEW field_options_distinct AS
SELECT
field,
option_value,
option_label
FROM
field_options
GROUP BY
field,
option_value
;
此视图仅包含我关注的列,每个字段/ 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'",
),
);
}
}
所有这一切都可以解决问题。容易,对吧?!?