I'm working on showing my node's taxonomy in a table view. To do so, I need to define a field for each vocabulary my node type uses. But simply joining the term_node and term_data tables in my hook_views_tables() is not going to work. The reason being that those multiple joins cause the query to return the same nodes over and over again, once for each taxonomy term the node has.
So I've been poking around and I discovered the 'notafield' setting for a field. And the 'query_handler' setting. Together these make it possible, but I still have not found the ideal solution.
My first attempt was to define a query handler which itself made a SQL query finding all the nodes terms for a given vocabulary. This displayed the information, but it's inefficient because it forces a query for each row of the table being displayed.
Below is some code from my second approach. Again I define a custom query_handler. In this case I define a subquery and use group_concat. Both are MySQL 4.1 features so I doubt this code will work with other databases. But I share it here in case anyone else is trying the same thing, and maybe someone can tell me how to make it better.
In particular, I'd like to sort my table based on the value I'm adding to the query, but the table sort stuff insists on using TABLE_NAME.FIELD_NAME as the order by field. Is there some way I can override that? In particular what I'd like is if I define 'notafield', then it should use my FIELD_NAME without trying to fully qualify it (I can name it in a way that will be unique.) I tried for a while to make that work but couldn't find the code that controls it.
Anyway, here's my rough and undocumented code:
// define tables for a term in the $vid vocabulary.
// ignore $multiple -- not used for now
function views_term_table2($vid, $name, $multiple = false) {
// add tables necessary to join the term_node and term_data tables.
// term_data contains the human-readable term name.
$term_data_table_name = 'term_data_'.$vid;
$tables[$term_data_table_name] =
array(
'fields' =>
array("term_$vid" =>
array('name' => $name,
'notafield' => true,
'query_handler' => 'views_query_handler_field_term',
'handler' => 'views_handler_field_term',
'multiple' => $multiple,
'sortable' => false,
'vid' => $vid,
),
),
);
return $tables;
}
function views_query_handler_field_term($field, $fieldinfo, &$query) {
$vid = $fieldinfo['vid']; // vocabulary ID (not view id)
// Use GROUP_CONCAT to find all terms. Requires MySQL 4.1. Nested SELECT also requires 4.1
// Could use MAX instead of CONCAT to find just one
$query->add_field("(SELECT GROUP_CONCAT(td.name ORDER BY td.weight, td.name) FROM {term_data} td LEFT JOIN {term_node} tn ON tn.tid = td.tid WHERE td.vid = $vid AND tn.nid = node.nid)", null, "term_$vid");
}
// we define this handler to display the term. Necessary because we used 'notafield' in defining the field.
function views_handler_field_term($fieldinfo, $fielddata, $value, $data) {
$vid = $fieldinfo['vid']; // vocabulary ID (not view id)
$field = "term_$vid";
return $data->$field;
}
Finally to make the code work, you have to call it from your hook_views_tables():
function ocp_project_views_tables() {
$tables = array();
// define your normal $tables here.
// add a field for a term from vocabulary ID 5
$tables = array_merge($tables,
views_term_table2(5,'My Module: Field From Vocab 5'));
$tables = array_merge($tables,
views_term_table2(10,'My Module: Field From Vocab 20'));
return $tables;
}
Remember to empty your cache before you will see the new fields in the Views form.
Perhaps this is leading towards something that could be added to the Views module. But if not, it's here for people to try to improve upon.
-Dave
Comments
Comment #1
merlinofchaos commentedHmmm. I'm not entirely sure you're on the right track.
The easiest way to do it is your initial solution; do a query to get all of your terms for each row of the node table returned. Yes, this is a bunch of extra queries, but you have a variable number of terms, realistically, and SQL doesn't like variable numbers of columns.
What you really want the query to look like, in the end, is something like this:
SELECT ... FROM node node LEFT JOIN term_node term_node ON term_node.nid = node.nid LEFT JOIN term_node term_node2 ON term_node2.nid = node.nid AND term_node2.tid != term_node.tid LEFT JOIN term_node term_node3 ON term_node3.nid = node.nid AND term_node3.tid != term_node2.tid AND term_node3.tid != term_node.tid
... and so on, until you have a field for every term you want.
You can actually build this construct in views, but you have to cheat a little. The 'extra' info expects it to be "key" => "value" which translates to "key = value". But you can cheat and do "key != value" => NULL.
If you know that you have a nice 1 term : 1 vocabulary relationship, you can make the join a little easier.
LEFT JOIN term_node on term_node.nid = node.nid LEFT JOIN term_data on term_data.tid = term_node.tid AND term_data.vid = MYVOCABULARYID
Repeat those joins for each vocabulary ID you want. Because it always uses LEFT JOINs, if you don't have a term, it'll always return NULL, which I consider desirable.
Let me know if you like any of the above solutions; I can turn it into more specific code if you like.
Comment #2
Dave Cohen commentedActually, I started with something close to your suggestion
Unfortunately it works only if the node has one and only one term (or no terms). Let's say the node has one term in MYVOCABULARYID and another in ANOTHERVID. You'll get two rows back with that nid. One with term_data.tid set, and another with NULL. Add more terms to your node, and the query returns even more nodes. I believe both your suggestions have this problem.
With your other suggestion, you may be on to something:
This tells me that I could first do a seperate query for all the TIDs in my vocabulary. Then I could construct the 'extra' field like so:
'extra' => 'tid IN (2, 3, 4, 7, 9)' => NULL
Where the numbers are the TIDs returned from my query. The drawbacks here are that it would only work if the node has one (or zero) terms in the vocab, and more importantly if terms are added to the vocab my query will not update because the Views module uses caching.
However since I am using mysql 4.1 I could use a nested query:
'extra' => "tid IN (SELECT tid FROM {term_data} WHERE vid=$vid)" => NULL
That still works correctly only when there is one or zero terms from the vocab (otherwise it will return multiple rows per node). But it should allow me to sort by the column, which would be excellent. And I can still use my original scheme when I know there could be multiple terms.
It's late here. I'll wait till tomorrow to try it out.
Comment #3
merlinofchaos commentedYou can call views_invalidate_cache() from your hook_taxonomy(), which will force the rebuild when your vocabulary is changed, if you need.
Comment #4
merlinofchaos commentedThe CVS version now supports a 'distinct' filter, which may help the problem of returning multiple nodes.
Is there anything else I need to do here? I'm going to set this 'fixed'.
Comment #5
Dave Cohen commentedThe distinct idea sounds good. I have not tried it yet. I'm OK with you closing this issue. But just to share, here's the code I'm currently using. It takes advantage of the extra trick you mentioned earler.
It also handles to cases. If the caller knows the vocabulary allows single select, it uses an approach that allows me to sort the table by terms in that vocab. If multiple select it uses a group concat and does not allow the sorting.
Comment #6
(not verified) commented