Es gibt unterschiedliche Gründe, warum es passieren kann, dass in MM-Tabellen doppelte Datensätze enthalten sind. Oft stammen die Duplikate aus unglücklichen Import- oder Migrationsskripten, oft sind sie entstanden, weil die Tabellenstruktur zu spät strikt genug definiert wurde.
Das Kind ist nun aber schon in den Brunnen gefallen, und wir wollen es retten.
Mit ein wenig Schiebereien klappt das im MySQL recht fix.
Szenario
Nehmen wir an, wir hätten eine MM-Tabelle folgender Form:
# # Table structure for table 'tx_example_category_mm' # CREATE TABLE tx_example_category_mm ( uid_local int(11) DEFAULT '0' NOT NULL, uid_foreign int(11) DEFAULT '0' NOT NULL, sorting int(11) DEFAULT '0' NOT NULL, KEY uid_local (uid_local), KEY uid_foreign (uid_foreign) );
Jedes Pärchen aus uid_local
und uid_foreign
sollte eindeutig sein udn nur einmal vorkommen. Dies hätte erzwungen werden können durch einen UNIQUE KEY
– wurde aber wohl vergessen.
Im Nachhinein jetzt diesen Index hinzuzufügen, verwehrt MySQL, da es Duplikate gibt, die UNIQUE
-Bedingung also verletzt.
ALTER TABLE tx_example_category_mm ADD UNIQUE KEY uidx(uid_local, uid_foreign);
=> „Error in query (1062): Duplicate entry ’12-13′ for key ‚uidx‘„
Lösung
Vor MySQL 5.7 war eine einfache Lösung1, den Index zu setzen, und MySQL dabei die Duplikate verwerfen zu lassen. IGNORE
im ALTER TABLE
-Befehl wurde jedoch in MySQL v5.6 „deprecated“ und in v5.7 entfernt2.
# Not working anymore since MySQL 5.7 ALTER IGNORE TABLE tx_example_category_mm ADD UNIQUE KEY uidx(uid_local, uid_foreign);
Morgan hatte damals im Blogbeitrag „The future of ALTER IGNORE TABLE syntax“3 Überlegungen angestellt, wie man dennoch mit der Problematik umgehen kann, und dabei die Probleme des IGNORE
(z.B. bei Replikationen der Tabelle) mit in den Griff bekommt.
Ab MySQL 5.7 benötigt man den Zwischenschritt über eine Kopie der Tabelle:
# Working with MySQL 5.7 and newer CREATE TABLE tx_example_category_mm_new LIKE tx_example_category_mm; # !!! Attention !!! Check which field combination must be unique for you ALTER TABLE tx_example_category_mm_new ADD UNIQUE KEY uidx(uid_local, uid_foreign); INSERT IGNORE INTO tx_example_category_mm_new SELECT * FROM tx_example_category_mm; DROP TABLE tx_example_category_mm; RENAME TABLE tx_example_category_mm_new TO tx_example_category_mm;
Warnung
Prüft unbedingt zuvor, welche Spalten der mm-Tabelle bei Euch unique sind bzw. sein müss(t)en. In diesem einfachen Beispiel sind es nur uid_local und uid_foreign. Wird die mm-Tabelle für die Verknüpfung mehrerer Tabellen genutzt, sind tablesnames und fieldname mit zu berücksichtigen. Und es gibt auch Szenarien, da dürfen Einträge mehrfach verlinkt sein (multiple).
Danke für den Hinweis, Nicole (@ichhabrecht).
Es wird also zunächst die Tabellen-Struktur als Basis für eine temporäre Tabelle genutzt. In der neuen Tabelle wird dann der bislang fehlende UNIQUE
-Key gesetzt. Anschließend werden alle Datensätze aus der ursprünglichen Tabelle in die neue Tabelle kopiert, wobei Fehler aufgrund von doppelten Keys via IGNORE
im INSERT
-Befehl abgefangen werden. Als letztes wird die Quell-Tabelle durch die neu erstellte ersetzt (bzw. formal korrekt: die Quell-Tabelle wird gelöscht und die temporär-Tabelle erhält den Namen der Quell-Tabelle).
Herzlichen Dank an die +Pluswerk München AG (damals noch Marit AG) und Morgan Tocker für das Teilen ihrer Überlegungen.
Backup nicht vergessen!
Vor derartigen Änderungen an der Datenbank bitte sicherstellen, dass ein Backup vorhanden ist. Aber das ist ja klar ;)
Hey Julian, danke dir für den Beitrag. Ich habe diesen etwas zu spät entdeckt und nun eine eigene Lösung für das „Duplicate entry“-Problem in MM-Tabellen im Zuge meiner v12 Upgrade-Arbeiten auf Basis eines Symfony Commands gebaut.
Der Command geht durch alle mehrfach vorkommenden Records, löscht diese und erzeugt jeweils einen neuen Record, um die Records aller MM-Tabellen (ermittelt aus TCA) zu vereinzeln. Vielleicht ist dieser Ansatz ja auch für euch interessant:
https://gist.github.com/ErHaWeb/67c9b561e9a38084d9322e3e8c41f988
Da ich deinen Ansatz noch nicht getestet habe: Was passiert eigentlich bei dieser Methode mit den Feldern `sorting` und `sorting_foreign`? Ich vermute hier werden die Werte des verbliebenen Records erhalten? Mein Command setzt diese Felder einfach auf 0. Ich bin mir aktuell nicht ganz sicher, was in diesem Zusammenhang der geeignetere Weg wäre, da man ja kaum kontrollieren kann, ob die Werte des verbliebenen Records korrekt sind, oder?
Hihi, hatte Dich nach Deinem tweet/Tröt schon auf den Artikel hier hinweisen wollen ;-)
Das Vorgehen im Artikel ist sehr, sehr basic und liefert eher einen Ansatz. Nicole hatte vor einiger Zeit schon mal ein paar Kritikpunkte, die inzwischen als Warnung eingearbeitet sind. Man muss immer den eigenen Use-Case genauer angucken – und ggf. adaptieren, wie man es dann fixen kann.
(BTW: bei Dir ist „multiple“ unberücksichtigt, d.h. das *gewollte* Mehrfachvorkommen von Relationen zwischen denselben zwei Datensätzen)
Oh ja, guter Punkt. Danke dir für den Hinweis zu `multiple`, das hatte ich bisher nicht so auf dem Schirm und werde mir das nochmal genauer ansehen/den Command anpassen. Immerhin hatte ich Nicoles Anmerkung zum möglichen Vorhandensein von `tablenames`/`fieldname` bereits berücksichtigt.