Product Cockpit beschleunigen

hybris: Product Cockpit beschleunigen

 

Das Problem

 

Bei vielen Hybris-Installationen mit einer großen Anzahl von Produkten, Varianten usw. dauert der Login in das Product Cockpit oder auch der Wechsel eines Catalogs recht lange.


Die einfachsten Lösungen sind

  • Abschalten der Initial Search durch Setzen von disabledInitialSearch auf true
  • Abschalten der Auto Search durch Setzen von disabledAutoSearch auf true


Das ist detailliert beschrieben im SAP KBA-Artikel 2435094.

 

Bei der Hybris-Installation eines Kunden hatten wir nach der entsprechenden Änderung der Konfiguration immer noch eine schlechte „User Experience“, da nach Auswählen eines Catalogs die Suche darin immer noch langsam war. Nach einer eingehenden Untersuchung des Problems stellte sich heraus, dass die langsame Suche durch die in der DB ausgeführte SQL-Query besonders durch die „ORDER BY„-Klausel verursacht wurde. Die Reihenfolge wird bestimmt durch den (ersten) Eintrag der „sort-property“ für beispielsweise Produkte wie es in der „Base_Product.xml“ Cockpit UI component configuration spezifiziert ist. (Das Weglassen/Entfernen aller Sort-Properties führt übrigens zu einer „XML violation“ und einem Rückfall auf „creationTime“ oder „PK“ bei der „sort-property“).

 

Per Vorgabe wird diese Query beispielsweise aufgebaut wie folgt:

SELECT {item:PK} FROM {Product AS item } WHERE  ( NOT EXISTS ({{ SELECT {CollElem:PK} FROM {ObjectCollectionItemReference AS CollElem } WHERE  ({CollElem:item} = {item:pk} AND {CollElem:collection} = ?gs.param.1 ) }}) AND  ({item:catalogVersion} IN (?gs.param.3) OR  EXISTS ({{ SELECT {CategoryProductRelation:target} FROM {CategoryProductRelation AS CategoryProductRelation  JOIN Category AS cat  ON {CategoryProductRelation:source} = {cat:PK} } WHERE  ({cat:catalogVersion} IN (?gs.param.4) AND {CategoryProductRelation:target} = {item:PK} ) }}) ) AND {item:itemtype} IN (?gs.param.5,?gs.param.6,?gs.param.7,?gs.param.8,?gs.param.9,?gs.param.10, ?gs.param.11,?gs.param.12,?gs.param.13,?gs.param.14,?gs.param.15)) ORDER BY {item:modifiedtime} DESC

 

(Das Verwenden von „JOIN“ zusammen mit „ORDER BY“ führt meist zum Anlegen von temporären DB-Tabellen, was man wegen schlechter Performance vermeiden sollte).

 

Der Kunde wünschte ein Abschalten der Sortierung. In der SAP KBA finden sich zur Optimierung des Product Cockpits und dem Entfernen von „ORDER BY“ die Artikel 2107507 und 2105270. Allerdings erstellten wir eine alternative und mehr „lightweight“ Lösung.

 

Die Lösung

Um wirklich „ORDER BY“ zu entfernen, mussten wir eine Methode in der Klasse SearchBrowser und eine Methode in der Klasse QueryProvider überschreiben.

 

In SearchBrowser.doSearchInternal() wird die SQL-Query erzeugt. Die Lösung hier ist, die Methode searchQuery.addSortCriterion() nur aufzurufen, wenn der ausgewählte Type nicht „Product“ und die Sort-Property nicht „Item.creationtime“ ist. Wenn die Query erzeugt wurde, wird sie durch Aufruf von „searchProvider.search(searchQuery)“ ausgeführt. Im SearchProvider wird die Query/Order geprüft mittels „genQuery.getOrderByList()„: Wenn sie leer ist, wird eine Order nach „PK“ hinzugefügt. Dieser Teil des Codes muss entfernt werden.

 

Damit unser Patch funktioniert, müssen wir sicherstellen, dass die erste „sort-property“ für den Typ „Product“ tatsächlich „creationTime“ ist. Dazu haben wir eine passende „Base_Product.xml“ erstellt.

 

Dieser einfache Lösungsansatz führt dazu, dass alle Produktsuchen sortiert nach „Erstellungszeit“ tatsächlich nicht sortiert sind. Abhängig von den Vorgaben des Kunden können hier beliebige Anpassungen vorgenommen werden.

 

(Um anzuzeigen, dass eine Produktsuche tatsächlich unsortiert erfolgt, haben wir eine spezielle Übersetzung für „creationTime“ hinzugefügt.)

Die vorzunehmenden Schritt

In der Cockpit Extension:

  1. Fügen Sie die Java-Klasse CustomerProductCockpitProductSearchBrowserModel.java hinzu.
  2. Fügen Sie die Java-Klasse CustomerProductCockpitProductPerspectiveQueryProvider.java hinzu.
  3. Erweitern Sie die passende Web-Spring-Konfiguration wie in CustomerProductCockpit-web-spring.xml gezeigt.
  4. Erweitern Sie die passende Services-Spring-Konfiguration wie in CustomerProductCockpit-spring-services.xml gezeigt.
  5. Setzen Sie die Sort-Properties wie in Base_Product.xml gezeigt.
  6. Fügen Sie eine Translation hinzu wie sie z.B. in CustomerProductCockpit-locales_de.properties gezeigt wird.

 

Die Quellcodes

CustomerProductCockpitProductSearchBrowserModel.java

package de.customer.cockpit.session.impl;

import de.hybris.platform.catalog.model.CatalogVersionModel;
import de.hybris.platform.cockpit.model.meta.ObjectTemplate;
import de.hybris.platform.cockpit.model.meta.PropertyDescriptor;
import de.hybris.platform.cockpit.model.search.ExtendedSearchResult;
import de.hybris.platform.cockpit.model.search.Query;
import de.hybris.platform.cockpit.model.search.SearchType;
import de.hybris.platform.cockpit.services.search.SearchProvider;
import de.hybris.platform.productcockpit.session.impl.DefaultProductSearchBrowserModel;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;

import org.apache.log4j.Logger;


public class CustomerProductCockpitProductSearchBrowserModel extends DefaultProductSearchBrowserModel
{

  private static final Logger LOG = Logger.getLogger(CustomerProductCockpitProductSearchBrowserModel.class);

  @Override
  protected ExtendedSearchResult doSearchInternal(final Query query)
  {
    if (query == null)
    {
      throw new IllegalArgumentException("Query can not be null.");
    }

    ExtendedSearchResult result = null;

    final SearchProvider searchProvider = getSearchProvider();
    if (searchProvider != null)
    {
      Query searchQuery = null;

      final int pageSize = query.getCount() > 0 ? query.getCount() : getPageSize();

      SearchType selectedType = null;
      if (query.getSelectedTypes().size() == 1)
      {
        selectedType = query.getSelectedTypes().iterator().next();
      }
      else if (!query.getSelectedTypes().isEmpty())
      {
        selectedType = query.getSelectedTypes().iterator().next();
        LOG.warn("Query has ambigious search types. Using '" + selectedType.getCode() + "' for searching.");
      }

      if (selectedType == null)
      {
        selectedType = getSearchType();
      }

      searchQuery = new Query(Collections.singletonList(selectedType), query.getSimpleText(), query.getStart(), pageSize);
      searchQuery.setNeedTotalCount(!isSimplePaging());
      searchQuery.setParameterValues(query.getParameterValues());
      searchQuery.setParameterOrValues(query.getParameterOrValues());
      searchQuery.setExcludeSubTypes(query.isExcludeSubTypes());

      final ObjectTemplate selTemplate = (ObjectTemplate) query.getContextParameter("objectTemplate");
      if (selTemplate != null)
      {
        searchQuery.setContextParameter("objectTemplate", selTemplate);
      }


      if (query.getContextParameter("selectedCatalogVersions") != null)
      {
        try
        {
          setSelectedCatalogVersions((Collection) query.getContextParameter("selectedCatalogVersions"));
        }
        catch (final Exception e)
        {
          LOG.warn("Could not select catalog versions", e);
        }
      }
      if (query.getContextParameter("selectedCategories") != null)
      {
        try
        {
          setSelectedCategories((Collection) query.getContextParameter("selectedCategories"));
        }
        catch (final Exception e)
        {
          LOG.warn("Could not set selected categories", e);
        }
      }


      Collection<CatalogVersionModel> catver = getSelectedCatalogVersions();
      if ((catver.isEmpty()) && (getSelectedCategories().isEmpty()))
      {
        catver = getProductCockpitCatalogService().getAvailableCatalogVersions();
      }
      searchQuery.setContextParameter("selectedCatalogVersions", catver);
      if (!getSelectedCategories().isEmpty())
      {
        searchQuery.setContextParameter("selectedCategories", getSelectedCategories());
      }



      final Map<PropertyDescriptor, Boolean> sortCriterion = getSortCriterion(query);

      PropertyDescriptor sortProp = null;
      boolean asc = false;

      if ((sortCriterion != null) && (!sortCriterion.isEmpty()))
      {
        sortProp = sortCriterion.keySet().iterator().next();
        if (sortProp == null)
        {
          LOG.warn("Could not add sort criterion (Reason: Specified sort property is null).");
        }
        else
        {
          /**
           * Check for selected type "Product" and only "creationtime" as sort property. As this is expected to be
           * the initial search we remove the sortCriterion to avoid SQL "order by" clause and speed up initial
           * search in DB.
           *
           * To get "creationtime" as only sort property we remove all sort properties for Product. After that we
           * get creationtime as fallback. To set the sort property we use a cockpit UI config for "Base" and
           * "Product". Example Base_Product.xml:
           *
           * <pre>
           *
           * <?xml version="1.0" encoding="UTF-8"?>
           * <base>
           *     <search>
           *         <search-properties>
           *             <property qualifier="Product.code" />
           *             <property qualifier="Product.name" />
           *         </search-properties>
           *         <sort-properties>
           *             <property qualifier="Product.creationTime"/>
           *         </sort-properties>
           *     </search>
           *     <label>
           *         <property qualifier="Product.name" />
           *     </label>
           * </base>
           *
           * </pre>
           *
           * Additionally we must remove...
           *
           * <pre>
           * genQuery.addOrderBy(new GenericSearchOrderBy(new GenericSearchField("item", "pk"), true));
           * </pre>
           *
           * ... from SearchProvider.search().
           */
          if ((selectedType.getCode().compareTo("Product") == 0)
              && (sortProp.getQualifier().compareTo("Item.creationtime") == 0))
          {
            LOG.info("Disabled sorting for 'Product' and 'Item.creationtime' to remove 'ORDER BY'.");
            //sortProp = null; // optionally, for advanced search
          }
          else
          {
            if (sortCriterion.get(sortProp) != null)
            {
              asc = sortCriterion.get(sortProp).booleanValue();
            }
            searchQuery.addSortCriterion(sortProp, asc);
          }
        }
      }

      updateAdvancedSearchModel(searchQuery, sortProp, asc);

      try
      {
        final Query clonedQuery = (Query) searchQuery.clone();
        setLastQuery(clonedQuery);
      }
      catch (final CloneNotSupportedException localCloneNotSupportedException)
      {
        LOG.error("Cloning the query is not supported");
      }

      if (getBrowserFilter() != null)
      {
        getBrowserFilter().filterQuery(searchQuery);
      }

      result = searchProvider.search(searchQuery);

      updateLabels();
    }

    return result;
  }
}

CustomerProductCockpitProductPerspectiveQueryProvider.java

package de.customer.cockpit.session.impl;

import de.hybris.platform.cockpit.model.search.ExtendedSearchResult;
import de.hybris.platform.cockpit.model.search.Query;
import de.hybris.platform.cockpit.model.search.SearchType;
import de.hybris.platform.cockpit.model.search.impl.DefaultExtendedSearchResult;
import de.hybris.platform.cockpit.services.search.impl.GenericSearchParameterDescriptor;
import de.hybris.platform.core.GenericCondition;
import de.hybris.platform.core.GenericQuery;
import de.hybris.platform.core.GenericSearchField;
import de.hybris.platform.core.GenericSearchOrderBy;
import de.hybris.platform.core.model.type.ComposedTypeModel;
import de.hybris.platform.productcockpit.services.search.impl.ProductPerspectiveQueryProvider;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;


public class CustomerProductCockpitProductPerspectiveQueryProvider extends ProductPerspectiveQueryProvider
{

  @Override
  public ExtendedSearchResult search(final Query query)
  {
    final SearchType oldRootType = getDefaultRootType();

    final Set<SearchType> searchTypes = query.getSelectedTypes();
    if ((searchTypes != null) && (searchTypes.size() == 1))
    {
      setDefaultRootType(searchTypes.iterator().next());
    }

    final List<GenericCondition> conditions = new ArrayList<GenericCondition>();
    final Set<GenericSearchParameterDescriptor> activeParameters = new LinkedHashSet<GenericSearchParameterDescriptor>();

    final ComposedTypeModel rootTypeModel = extractRootTypeModel(activeParameters);
    final Set<ComposedTypeModel> permittedTypeModels = rootTypeModel == null ? Collections.EMPTY_SET
        : getPermittedTypes(rootTypeModel, query.getSelectedTypes(), query.isExcludeSubTypes());
    final ExtendedSearchResult ret;
    if ((rootTypeModel == null) || (permittedTypeModels.isEmpty()))
    {
      ret = new DefaultExtendedSearchResult(query, Collections.EMPTY_LIST, 0);
    }
    else
    {
      final GenericQuery genQuery = new GenericQuery(rootTypeModel.getCode());
      genQuery.setInitialTypeAlias("item");

      conditions.addAll(createConditions(query, genQuery));

      final GenericSearchOrderBy orderBy = createOrderBy(query);
      if (orderBy != null)
      {
        genQuery.addOrderBy(orderBy);
      }

      //			final Collection<GenericSearchOrderBy> orderByList = genQuery.getOrderByList();
      //			if (orderByList.isEmpty())
      //			{
      //				genQuery.addOrderBy(new GenericSearchOrderBy(new GenericSearchField("item", "pk"), true));
      //			}

      conditions.add(GenericCondition.createConditionForValueComparison(new GenericSearchField("itemtype"),
          de.hybris.platform.core.Operator.IN, permittedTypeModels));

      genQuery.addCondition(GenericCondition.and(conditions));

      beforeSearch();
      try
      {
        ret = performQuery(query, genQuery);
      }
      finally
      {
        afterSearch();
      }
    }

    setDefaultRootType(oldRootType);
    return ret;
  }
}

CustomerProductCockpit-web-spring.xml

<!-- add at least alias and bean and defaultBrowserClass CustomerProductCockpitProductSearchBrowserModel to your web-spring: -->
  <alias name="CustomerProductCockpitProductPerspective" alias="ProductPerspective" />
  <bean id="CustomerProductCockpitProductPerspective" scope="session" parent="defaultProductPerspective">
    <property name="browserArea">
      <bean class="de.hybris.platform.productcockpit.session.impl.BrowserArea">
        <property name="defaultBrowserClass" value="de.customer.cockpit.session.impl.CustomerProductCockpitProductSearchBrowserModel" />
        <property name="viewURI" value="/productcockpit/productBrowser.zul" />
        <property name="multiSelectActions">
          <ref bean="ContentBrowserMultiSelectActionColumn" />
        </property>
        <property name="multiSelectContextActionsRegistry">
          <ref bean="ContextAreaActionColumnConfigurationRegistry" />
        </property>
        <property name="additionalToolbarActions">
          <ref bean="ProductBrowserAdditionalToolbarActionColumn" />
        </property>
        <property name="defaultBrowserViewMapping">
          <map>
            <entry key="cockpitgroup" value="GRID" />
            <entry key="productmanagergroup" value="GRID" />
          </map>
        </property>
        <property name="inspectorRenderer" ref="coverageInspectorRenderer" />
        <property name="openInspectorOnSelect" value="true" />
      </bean>
    </property>
  </bean>

 CustomerProductCockpit-spring-services.xml

<!-- add this to spring-services: -->
  <alias alias="productSearchProvider" name="CustomerProductCockpitProductPerspectiveQueryProvider" />
  <bean id="CustomerProductCockpitProductPerspectiveQueryProvider" class="de.customer.cockpit.session.impl.CustomerProductCockpitProductPerspectiveQueryProvider"
    scope="tenant">
    <property name="maxCategoryCount" value="900" />
  </bean>

 Base_Product.xml

<?xml version="1.0" encoding="UTF-8"?>
<base>
  <search>
    <search-properties>
      <property qualifier="Product.code"/>
      <property qualifier="Product.name"/>
      <property qualifier="Product.description"/>
    </search-properties>
    <sort-properties>
      <property qualifier="Product.creationTime"/> <!-- actually Item.creationtime will be used for this on setting up SQL Query but the translation will give "no sorting" when displaying in cockpit -->
      <property qualifier="Item.modifiedtime"/>
      <property qualifier="Product.code"/>
      <property qualifier="Product.name"/>
      <property qualifier="Product.description"/>
      <property qualifier="Item.pk"/>
    </sort-properties>
  </search>
  <label>
    <property qualifier="Product.name" />
  </label>
</base>

CustomerProductCockpit-locales_de.properties

type.item.creationtime.name=Erstellungszeit

# This is actually some kind of hack to display that there is no sorting for this type in product cockpit (see Base_Product.xml). The sort-property Product.creationtime will be taken as Item.creationtime and SearchBrowser/QueryProvider will remove this sorting ("ORDER BY")

type.product.creationtime.name=Erstellungszeit (no sorting)