问题
One thing with which I have long had problems, within the CakePHP framework, is defining simultaneous hasOne
and hasMany
relationships between two models. For example:
BlogEntry hasMany Comment
BlogEntry hasOne MostRecentComment
(where MostRecentComment
is the Comment
with the most recent created
field)
Defining these relationships in the BlogEntry model properties is problematic. CakePHP's ORM implements a has-one relationship as an INNER JOIN
, so as soon as there is more than one Comment, BlogEntry::find('all')
calls return multiple results per BlogEntry.
I've worked around these situations in the past in a few ways:
Using a model callback (or, sometimes, even in the controller or view!), I've simulated a MostRecentComment with:
$this->data['MostRecentComment'] = $this->data['Comment'][0];
This gets ugly fast if, say, I need to order the Comments any way other than byComment.created
. It also doesn't Cake's in-built pagination features to sort by MostRecentComment fields (e.g. sort BlogEntry results reverse-chronologically byMostRecentComment.created
.Maintaining an additional foreign key,
BlogEntry.most_recent_comment_id
. This is annoying to maintain, and breaks Cake's ORM: the implication isBlogEntry belongsTo MostRecentComment
. It works, but just looks...wrong.
These solutions left much to be desired, so I sat down with this problem the other day, and worked on a better solution. I've posted my eventual solution below, but I'd be thrilled (and maybe just a little mortified) to discover there is some mind-blowingly simple solution that has escaped me this whole time. Or any other solution that meets my criteria:
- it must be able to sort by MostRecentComment fields at the
Model::find
level (ie. not just a massage of the results); - it shouldn't require additional fields in the
comments
orblog_entries
tables; - it should respect the 'spirit' of the CakePHP ORM.
(I'm also not sure the title of this question is as concise/informative as it could be.)
回答1:
The solution I developed is the following:
class BlogEntry extends AppModel
{
var $hasMany = array( 'Comment' );
function beforeFind( $queryData )
{
$this->_bindMostRecentComment();
return $queryData;
}
function _bindMostRecentComment()
{
if ( isset($this->hasOne['MostRecentComment'])) { return; }
$dbo = $this->Comment->getDatasource();
$subQuery = String::insert("`MostRecentComment`.`id` = (:q)", array(
'q'=>$dbo->buildStatement(array(
'fields' => array( String::insert(':sqInnerComment:eq.:sqid:eq', array('sq'=>$dbo->startQuote, 'eq'=>$dbo->endQuote))),
'table' => $dbo->fullTableName($this->Comment),
'alias' => 'InnerComment',
'limit' => 1,
'order' => array('InnerComment.created'=>'DESC'),
'group' => null,
'conditions' => array(
'InnerComment.blog_entry_id = BlogEntry.id'
)
), $this->Comment)
));
$this->bindModel(array('hasOne'=>array(
'MostRecentComment'=>array(
'className' => 'Comment',
'conditions' => array( $subQuery )
)
)),false);
return;
}
// Other model stuff
}
The notion is simple. The _bindMostRecentComment
method defines a fairly standard has-one association, using a sub-query in the association conditions to ensure only the most-recent Comment is joined to BlogEntry queries. The method itself is invoked just before any Model::find()
calls, the MostRecentComment of each BlogEntry can be filtered or sorted against.
I realise it's possible to define this association in the hasOne
class member, but I'd have to write a bunch of raw SQL, which gives me pause.
I'd have preferred to call _bindMostRecentComment
from the BlogEntry's constructor, but the Model::bindModel() param that (per the documentation) makes a binding permanent doesn't appear to work, so the binding has to be done in the beforeFind callback.
来源:https://stackoverflow.com/questions/2510464/method-for-defining-simultaneous-has-many-and-has-one-associations-between-two-m