Was tun bei einer “Integrity constraint violation”?

Veröffentlicht am 20.09.2011 von jkuensebeck in der Schublade Anderes | Tags: , , | Comments Off

Ihr habt Mist gebaut und die Indexe werden nicht mehr richtig aufgebaut: Beim reindexieren der Indexe “catalog_product_attribute” oder “cataloginventory_stock” landet nur der Fehler
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '18263-1-1' for key 1'
im Log.
Dann habt ihr wahrscheinlich doppelte Attributewerte in den Produkten, also hat z.B. das Produkt mit der ID 16 für das Attribut “Status” (im Beispiel mit der ID 272) gleich zweimal im selben Store (mit der ID 0) einen Wert gespeichert. In der Datenbank sähe das z.B so aus:

SELECT * FROM catalog_product_entity_int LIMIT 2;
+----------+----------------+--------------+----------+-----------+-------+
| value_id | entity_type_id | attribute_id | store_id | entity_id | value |
+----------+----------------+--------------+----------+-----------+-------+
|        1 |             10 |          272 |        0 |        16 |     0 |
|        2 |             10 |          272 |        0 |        16 |     1 |
+----------+----------------+--------------+----------+-----------+-------+

Das Attribut mit der ID 272 ist also für das Produkt doppelt belegt, einmal mit dem Wert 0 und einmal mit dem Wert 1 (man hat aber auch das selbe Problem, wenn die Werte gleich sind).

Um die problematischen Werte zu finden, helfen folgende SQL Zeilen um Duplikate in den catalog_product_entity_… Tabellen zu finden:

SELECT eav_table.* FROM catalog_product_entity_int AS eav_table GROUP BY entity_id, attribute_id, store_id HAVING COUNT(*) > 1;
SELECT eav_table.* FROM catalog_product_entity_varchar AS eav_table GROUP BY entity_id, attribute_id, store_id HAVING COUNT(*) > 1;
SELECT eav_table.* FROM catalog_product_entity_text AS eav_table GROUP BY entity_id, attribute_id, store_id HAVING COUNT(*) > 1;
SELECT eav_table.* FROM catalog_product_entity_datetime AS eav_table GROUP BY entity_id, attribute_id, store_id HAVING COUNT(*) > 1;
SELECT eav_table.* FROM catalog_product_entity_decimal AS eav_table GROUP BY entity_id, attribute_id, store_id HAVING COUNT(*) > 1;

Um dann die doppelten Werte zu löschen (vorher bitte unbedingt anschauen, was doppelt ist und ob es Widerspüchlichkeiten gibt):

DELETE FROM catalog_product_entity_int WHERE value_id IN (SELECT value_id FROM (SELECT value_id FROM catalog_product_entity_int GROUP BY entity_id, attribute_id, store_id HAVING COUNT(*) > 1) AS temp);
DELETE FROM catalog_product_entity_varchar WHERE value_id IN (SELECT value_id FROM (SELECT value_id FROM catalog_product_entity_varchar GROUP BY entity_id, attribute_id, store_id HAVING COUNT(*) > 1) AS temp);
DELETE FROM catalog_product_entity_text WHERE value_id IN (SELECT value_id FROM (SELECT value_id FROM catalog_product_entity_text GROUP BY entity_id, attribute_id, store_id HAVING COUNT(*) > 1) AS temp);
DELETE FROM catalog_product_entity_datetime WHERE value_id IN (SELECT value_id FROM (SELECT value_id FROM catalog_product_entity_datetime GROUP BY entity_id, attribute_id, store_id HAVING COUNT(*) > 1) AS temp);
DELETE FROM catalog_product_entity_decimal WHERE value_id IN (SELECT value_id FROM (SELECT value_id FROM catalog_product_entity_decimal GROUP BY entity_id, attribute_id, store_id HAVING COUNT(*) > 1) AS temp);

Die eigentliche Frage ist, wie man überhaupt zu so einer EAV-Struktur kommen konnte. Das kann eigentlich nicht passieren, denn es gibt einen Unique-Index über die Felder (attribute_id, entity_id, store_id). Per SQL kann man diese aber über mit “SET  UNIQUE_CHECKS=0″ umgehen, was zum Beispiel am Anfang eines phpMyAdminDumps immer gemacht wird. Der “Trick” wird auch von eingen Extensions benutzt, um schneller in die Datenbank zu schreiben.


Controller Action mit Events überschreiben

Veröffentlicht am 27.07.2011 von datenbrille in der Schublade Anderes | Ein Kommentar »

In meinem Projekt verwenden wir momentan einen Rewrite des Checkout Controllers um unsere Modifikation vorzunehmen. Irgendwie gefiel mir das nicht und ich habe nach einer anderen Methode gesucht. Nach einigen Hinweisen von den Webguys habe ich eine Event-Observer Idee entwickelt.

Das ganze ist möglich weil Magento in Mage_Core_Controller_Varien_Action in der Methode dispatch ein Flag abfragt ob es überhaupt einen Dispatch geben soll.

//Mage_Core_Controller_Varien_Action->dispatch()
...
if ($this->getRequest()->isDispatched()) {
    /**
     * preDispatch() didn't change the action, so we can continue
     */
    if (!$this->getFlag('', self::FLAG_NO_DISPATCH)) {
        $_profilerKey = self::PROFILER_KEY.'::'.$this->getFullActionName();
        Varien_Profiler::start($_profilerKey);
        $this->$actionMethodName();
        Varien_Profiler::stop($_profilerKey);
        Varien_Profiler::start(self::PROFILER_KEY.'::postdispatch');
        $this->postDispatch();
        Varien_Profiler::stop(self::PROFILER_KEY.'::postdispatch');
    }
}
...

Setzt man nun diesen Flag in Controller oder sonst wo, dann wird Magento nicht die Action des Controller ausführen. Damit war ich meinem Ziel schon etwas näher.
Aber wie kann ich mich in eine beliebige Controller Action einhängen? Auch hier bietet Magento einen Hook. Dieser befindet sich ebenfalls in Mage_Core_Controller_Varien_Action, aber dieses mal in der Methode preDispatch. Dort werden verschiede Events geworfen und das zu einem sehr frühen Zeitpunkt in Request-Response Zyklus von Magento. Erst dadurch ist es möglich eine Action vollständig zu überschreiben.

Mage::dispatchEvent('controller_action_predispatch', array('controller_action'=>$this));
Mage::dispatchEvent(
    'controller_action_predispatch_'.$this->getRequest()->getRouteName(),
    array('controller_action'=>$this)
);
Varien_Autoload::registerScope($this->getRequest()->getRouteName());
Mage::dispatchEvent(
    'controller_action_predispatch_'.$this->getFullActionName(),
    array('controller_action'=>$this)
);

Wie ihr seht werden hier viele verschiedene Events gefeuert. Ich kann einfach auf alle Controller preDispatches hören oder auch nur auf bestimmte. Zur Demonstration habe ich einfach mal alle Actions überschrieben.

//XML in aus der Config XML eines Moduls
<controller_action_predispatch>
  <observers>
    <updateflags>
      <type>singleton</type>
      <class>magelog_teaser/observer</class>
      <method>stopAction</method>
    </updateflags>
  </observers>
</controller_action_predispatch>

Die Methode im Oberserver macht nichts anderes als ein Hello in die Response zu schreiben. Will man nun eine Methode im Checkout überschreiben, ersetzt man das Hello durch eine JSON Rückgabe und wählt den Event weniger allgemein.

public function stopAction(Varien_Event_Observer $observer) {
        //controller aus dem event laden
        /** @var $controller Mage_Core_Controller_Varien_Action */
        $controller = $observer->getData('controller_action');
        //hier passiert die Magie, und wird der Dispatch unterbrochen
        $controller->setFlag(
             $controller->getRequest()->getActionName()
             ,Mage_Core_Controller_Varien_Action::FLAG_NO_DISPATCH
             ,true
         );
        //jetzt kann man beliebigen Output erzeugen
        $controller->getResponse()->setBody("Hello");
}

Viel Spaß damit und ich werde berichten wie weit wir dieses Konzept in unserem neuen Checkout getrieben haben. Ach ja, das ganze hat bei uns auch schon einen Namen: “Das schöne Magento die()”.