We ran into this problem: An editor edits a menu to change out which stories on the site are being linked to from the menu in question. The links are edited in place as opposed to deleting and re-adding a whole new menu item. This causes duplicate menu_node rows to be created, because menu_node_exists() is checking for the combo of nid/mlid, and is assuming that, if it doesn't exist, a new row needs to be inserted. Later down the road, we're using menu_node_views to filter based on the menu name, and the view is returning more entries than it should, because there are extra rows in the table.
I think this is actually a semi-fundamental flaw in how the menu_node module works. menu_node should really be considered an extension of the menu_links table. It has a much more intimate relationship with menu_links than node. In fact, it could theoretically be re-implemented with a schema_alter on the menu_links table to just add the nid column, but I don't necessarily think that's the correct course of action. In any case, when thinking this way, it makes sense to have the mlid be the sole field for the primary key. This allows us to check to see if a row with a certain mlid already exists, and, if so, update it (with drupal_write_record) instead of adding a new row (and thereby creating duplicate-mlid rows). It is obvious that a node can be referenced by multiple menu items, so multiple nids in the table is correct, but there can't be a case that I can see where multiple mlids should exist, so reworking this piece allows us to make sure that's the case and allow for row updates where it makes sense.
Attached is the patch that I worked up for us that makes these changes.
| Comment | File | Size | Author |
|---|---|---|---|
| #12 | 783442-mlid.patch | 3.28 KB | agentrickard |
| menu_node-mlid_pkey.patch | 2.65 KB | mgriego |
Comments
Comment #1
agentrickardI believe it is theoretically possible for one node to have two menu entries, in two separate menus. Which is why this was written the way it is.
What does "edited in place" mean in this context?
Comment #2
agentrickardI checked. It is possible. So the debate is over the design of Drupal's menu system.
Comment #3
agentrickardEssentially, then, this is By Design, due to the behavior of Drupal core.
An acceptable patch would be a submodule or settings that _prevents_ nodes from being added to more than one menu, and enforces the single nid rule without altering the database schema.
Otherwise, we'll have people complaining about the reverse problem.
Comment #4
agentrickardFor your Views, you should add a menu filter to weed out the duplicates.
Comment #5
agentrickardNot a bug.
Comment #6
mgriego commentedThis *is* actually a bug. I think you're misunderstanding what I'm saying. Of course a node can be referenced by multiple menu items. BUT, multiple menu items with the same mlid can *never* exist. If you have multiple rows in menu_node with the same *mlid* (not nid), then there is a problem. This is the duplicate problem I was referring to.
Try this. This is the "edit in place" I was referring to. Create a custom menu, and add a link to a node on the site. Now go back and edit the same menu item. Only change the link to be a different node. What *should* happen is that the menu_node row should either be deleted before a new one is inserted or (as my patch does) updates the row in place to reference the new node id. Instead, what happens, is that the existing row is left behind, basically an orphan, and a new row is inserted to join the node and menu_links together. The fact that the old row is left behind is not a problem of removing duplicate rows in Views (which I'm very aware of how to do), it's a problem of stale and incorrect data.
This gets back to my fundamental point that there really should never be an instance where there are duplicate mlid's in the menu_node table. It should never happen. *That's* how the Drupal Menu system is designed.
Comment #7
agentrickardThat makes sense. It's been a while since I touched this code. Thanks.
This is the part I glossed over:
I read the key part of the patch backwards.
Comment #8
agentrickardWondering where my head was at when I coded this originally.
Two questions:
1) Do we need an update hook to remove the deadwood?
2) How can we write that efficiently?
Comment #9
mgriego commentedHah, I've often looked back at old code and wondered what I was thinking at the time...
In any case, you're probably right. Knowing the condition things can be in, it would probably be best to code something into the update hook before the indexes are recreated. In our environment, I caught it early enough that I was just going to remove the deadwood manually before running this update hook in production. In the wild, though, it's a whole different ball game. Though, I could see it working something like this:
Comment #10
agentrickardLucky me, I have a site with 6000 menu nodes I can test this on....
Comment #11
derjochenmeyer commentedsubscribing
Comment #12
agentrickardUpdated patch for testing.
Comment #13
derjochenmeyer commentedThanks, I tested the patch. It solves the problem mentioned by mgriego in #6
Comment #14
agentrickardDid you happen to test the update function?
Comment #15
derjochenmeyer commentedNo, I only tested the problem mentioned by mgriego in #6
Comment #16
agentrickardOK, thanks. I'd love for someone to validate the update function, but it should be ready to go.
Comment #17
agentrickardCommitted. Shame no reviews.
Comment #18
bstoppel commentedI just applied the patch from menu_node 1.3 to 1.4. Before I applied the update I looked at the menu_node table. There are 459 rows. 458 are distinct.
The update didn't apply fully because of the duplicate. (This is obviously easy for me to fix.)
Here's the the relevant part of the output...
Update #6002
* ALTER TABLE {menu_node} DROP PRIMARY KEY
* Failed: ALTER TABLE {menu_node} DROP INDEX nid
* Failed: ALTER TABLE {menu_node} ADD PRIMARY KEY (mlid)
* ALTER TABLE {menu_node} ADD INDEX nid (nid)
Comment #19
bstoppel commentedBTW if anyone is trying to find the dups in their menu_node table, use this query (assumes menu_node has not been prefaced.)
select mlid, count(mlid) from menu_node group by mlid having count(mlid) > 1;
Comment #20
agentrickardYou need to open a new issue, please, with regard to the update failure. I cannot tell if the update failed because the code is wrong or because you edited your database in some way.
Comment #21
bstoppel commentedI started a new issue. http://drupal.org/node/1018692