I've got a problem with sorting on a many-to-many relationship. What I'm trying to achieve seems like it should be fairly straightforward, but after a lot of banging my head against Fuel, I still can't get it to work.
(Incidentally, I realise more-or-less this question has been asked before, but since (a) at that time it wasn't possible to sort on a lazy-loading relationship, and (b) I've got a lot more detail of the exact problem I'm having, I thought it would be worth asking as a separate question...)
Here's the problem I'm having:
I have an "Item" model. Items can have children (which are also items), joined via a many-to-many relationship through an "items_items" table with a 'parent_id' and 'child_id' column. The "items_items" table also has a "sortorder" column, so that the order of the child items can be set.
Creating a separate model for this relationship seemed like overkill to me, so the order is updated by an observer that updates child items' sortorder when the parent is saved (via an "_event_after_save" method on Model_Item). This seems to work fine.
However, the problem I'm having is that I can get sort order working either with lazy loading or with eager loading, but not both. Whichever one works, the other throws Fuel errors, so the only way currently of getting both eager and lazy loading to work is to scrap the 'order_by' clause when eager-loading. Here are the three ways I've tried defining the 'children' relationship:
Approach #1
This is the approach that the relevant page in FuelPHP's documentation suggests should work.
No errors on eager or lazy load, but either way, order_by is not respected (child items are ordered by id, not by sortorder)
'children' => array(
'table_through' => 'items_items',
'key_through_from' => 'parent_id',
'key_through_to' => 'child_id',
'model_to' => 'Model_Item',
'order_by' => array(
'items_items.sortorder' => 'ASC'
),
)
Approach #2
Works as desired with eager loading
Causes Fuel error with lazy loading ("Column not found: 1054 Unknown column 'items_items.sortorder' in 'order clause'")
This seems to be because items_items is aliased to 't0_through' in the JOIN clause, but not in the ORDER BY clause.
'children' => array(
'table_through' => 'items_items',
'key_through_from' => 'parent_id',
'key_through_to' => 'child_id',
'model_to' => 'Model_Item',
'conditions' => array(
'order_by' => array(
'items_items.sortorder' => 'ASC'
),
)
)
Approach #3
Works as desired with lazy loading
Causes Fuel error with eager loading (effectively the reverse of take #2, above)
'children' => array(
'table_through' => 'items_items',
'key_through_from' => 'parent_id',
'key_through_to' => 'child_id',
'model_to' => 'Model_Item',
'conditions' => array(
'order_by' => array(
't0_through.sortorder' => 'ASC'
),
)
)
In a slightly desperate hack, I tried combining approaches 2 and 3 above by defining two separate relationships ('children' and 'child') - one for eager loading, the other for lazy loading, but it still breaks my app, because it makes the delete process throw similar errors. I could attempt to fix this, but I feel like that would just be piling hack upon hack. What I'd like instead is a solid way of ordering child items that works whether they were eager or lazy loaded. Fuel's docs suggest this should be doable (approach #1, above), but I just can't get that to work...
Any help would be very much appreciated!
From the ORM's point of view, the "through_table" does not exist. It is only defined so it can construct the SQL needed to make the relation work. That also means that additional fields in the "through_table" are not supported, it can only contain key values.
If you want attributes (additional columns) in the through table, it becomes a standard table in your database, for which a model is required. Your many-2-many relation then breaks down into two one-2-many relations.
Note that both can exist at the same time, so you can still use the many-to-many if you don't need to query or use columns in the through table.
I've finally managed to fix this issue. It turned out to be a problem with the current release of Fuel's ORM (version 1.7).
What worked for me was to update the ORM to a more recent development version (60cc4eb576 on the ORM's 1.8/develop branch), and then to set the 'order_by' on the many-to-many relation like this:
...
'conditions' => array(
'order_by' => array(
'items_items.sortorder' => 'ASC'
),
)
...
This is a combination of approaches 2 and 3 above, and not quite the syntax that the documentation currently suggests, but seems to work as expected / desired for both eager- and lazy-loading.
Related
I am not sure why I can't get the columns from my other tables via my relations. I was thinking is it because of my scope? After i had a default scope in my models, everything seems to be out of place, even if i use resetscope() at some places. Some sections I can't get to my relation columns; when that happens, I'd have to use Model::model->findbypk(n)->name.. that doesn't look pretty.
the id shows if i don't have the relations, but the name is blank when i put the relation name.
CHtml::listData(Model::model()->findAll(),'product_id','main.product_name'),
my model defaultscope is pretty basic:
return array(
'condition'=>'store_id1=:store_id OR store_id2=:store_id' ,
'params' => array(':store_id' => $store_id)
);
You can change the way you use your model like below:
Model::model()->with('main')->findAll();
I'm using giix to extend model (and crud) behavior. In this I would like to handle columns of type timestamp (that already exist in my model) specifically, rather like autoincrement fields are handled. (Ignored and not shown, that is.) However, there is no property $column->isTimestamp. I would like to add this somewhere, but I'm rather at loss what the best place for this would be. Do I put it in giix somewhere, or do I have to extend the column-baseclass?
Edit: I want to ignore them from every view, for every table. Since this is a lot of work, and it's something I always want, I'd like to automate it. Adapting the generators seems to make most sense, but I'm not sure what the best way to do it would be.
Here is the process:
Extend your database schema, if you are on MySQL it is CMysqlSchema.
Extend CMysqlColumnSchema and add a "isTimestamp" attribute.
In your CMysqlSchema sub-class extend createColumn and test for a timestamp, you'll see that Yii makes simple string comparisons here to set it's own flags. Set "isTimestamp" in your CMysqlColumnSchema here.
Tell Yii to use your schema driver like this in your components section in the config:
'db' => array(
'connectionString' => 'mysql:host=localhost;dbname=database',
'username' => '',
'password' => '',
'driverMap' => array('mysql' => 'CustomMysqlSchema'),
),
You will need to query the column schema, I've not used giix but find where it is generating the views, it should be looping through either the model attributes or the underlying table schema.
If it is looping through the schema:
//you can also ask Yii for the table schema with Yii::app()->db->schema->getTable[$tableName];
if ('timestamp' === $tableSchema->columns[$columnName]->dbType)
continue; //skip this loop iteration
If it loops over the attributes:
$dbType = Yii::app()->db->schema->getTable[$model->tableName]->columns[$modelAttribute]->dbType;
if ('timestamp' === $dbType)
continue; //skip this loop iteration
I've got a pretty complex object graph that I want to load in one fell
swoop.
Samples have Daylogs which have Daylog Tests which have Daylog
Results
Daylog Tests have Testkeys, Daylog Results have Resultkeys, and
TestKeys have Resultkeys.
I'm using the QueryOver API and Future to run these all as one query,
and all the data that NHibernate should need to instantiate the entire
graph IS being returned, verfied by NHProf.
public static IList<Daylog> DatablockLoad(Isession sess,
ICollection<int> ids)
{
var daylogQuery = sess.QueryOver<Daylog>()
.WhereRestrictionOn(dl => dl.DaylogID).IsIn(ids.ToArray())
.Fetch(dl => dl.Tests).Eager
.TransformUsing(Transformers.DistinctRootEntity)
.Future<Daylog>();
sess.QueryOver<DaylogTest>()
.WhereRestrictionOn(dlt =>
dlt.Daylog.DaylogID).IsIn(ids.ToArray())
.Fetch(dlt => dlt.Results).Eager
.Inner.JoinQueryOver<TestKey>(dlt => dlt.TestKey)
.Fetch(dlt => dlt.TestKey).Eager
.Inner.JoinQueryOver<ResultKey>(tk => tk.Results)
.Fetch(dlt => dlt.TestKey.Results).Eager
.Future<DaylogTest>();
sess.QueryOver<DaylogResult>()
.Inner.JoinQueryOver(dlr => dlr.DaylogTest)
.WhereRestrictionOn(dlt =>
dlt.Daylog.DaylogID).IsIn(ids.ToArray())
.Fetch(dlr => dlr.ResultKey).Eager
.Fetch(dlr => dlr.History).Eager
.Future<DaylogResult>();
var daylogs = daylogQuery.ToList();
return daylogs;
}
However, I still end up with proxies to represent the relationship
between Testkey and ResultKey, even though I'm specifically loading
that relationship.
I think this entire query is probably representative of a poor
understanding of the QueryOver API, so I would like any and all advice
on it, but primarily, I'd like to understand why I get a proxy and not
a list of results when later I try to get
daylogresult.resultkey.testkey.results.
Any help?
The answer was to call NHibernateUtil.Initialize on the various objects. Simply pulling the data down does not mean that NHibernate will hydrate all the proxies.
You have to load all your entities in one QueryOver clause to get rid of proxies. But in this case you will have a lot of joins in your query, so I recommend to use lazy loading with batching.
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 by Comment.created. It also doesn't Cake's in-built pagination features to sort by MostRecentComment fields (e.g. sort BlogEntry results reverse-chronologically by MostRecentComment.created.
Maintaining an additional foreign key, BlogEntry.most_recent_comment_id. This is annoying to maintain, and breaks Cake's ORM: the implication is BlogEntry 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 or blog_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.)
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.
I get the following SQL error:
SQL Error: 1066: Not unique table/alias: 'I18n__name'
when doing a simple find query.
Any ideas on possible situations that may have caused this??
I'm using a bindModel method to retrieve the data is that related?
This is my code:
$this->Project->bindModel(array(
'hasOne' => array(
'ProjectsCategories',
'FilterCategory' => array(
'className' => 'Category',
'foreignKey' => false,
'conditions' => array('FilterCategory.id = ProjectsCategories.category_id')
))));
$prlist = $this->Project->find('all', array(
'fields' => array('DISTINCT slug','name'),
'conditions' => array('FilterCategory.slug !='=>'uncategorised')
))
You have not used or initialized the required table/model you are using in your controller. Use var $uses = array('your_table_name');
I do not have a direct answer to my problem. However after some research I came to the following conclusion.
I've decided to change my methodology and stop using the translate behavior in cakephp.
I've found another behavior called i18n which works much better when dealing with associated models. You can read about it http://www.palivoda.eu/2008/04/i18n-in-cakephp-12-database-content-translation-part-2/#comment-1380
In cakephp book it says :
"Note that only fields of the model
you are directly doing find on will
be translated. Models attached via
associations won't be translated
because triggering callbacks on
associated models is currently not
supported."
I'm not sure if I made the right choice however I have been struggling to get the translate behavior to work in cakephp and this solution at least makes the project I'm working functional.