W poprzedniej części poradnika stworzyliśmy podstawowy importer produktów z zewnętrznego systemu Product Information Management (PIM), który zapisuje produkty jako item’y w Sitecore. Implementacja może być ulepszona poprzez użycie Sitecore item bucket i indeksów wyszukiwania, co pozwoli poprawić wydajność w razie większej ilości danych.
Aby lepiej odzwierciedlić strukturę produktów w systemie PIM zastosujemy niestandardową strukturę bucket’u w Sitecore. Repozytorium produktów podzielimy na:
- Active (aktywne produkty)
- A (wszystkie produkty, których nazwa zaczyna się na A)
- B (wszystkie produkty, których nazwa zaczyna się na B)
- …
- Inactive (nieaktywne produkty)
- A (wszystkie produkty, których nazwa zaczyna się na A)
- B (wszystkie produkty, których nazwa zaczyna się na B)
- …
Rozszerzenie modelu danych
Zakładamy że status produktu (active vs inactive) jest flagą importowaną z systemu PIM. Rozszerzmy nasz model, stworzony w poprzedniej części, dodając nowe pole typu Checkbox:
Dodajemy odpowiadającą właściwość w klasie naszego modelu:
1 2 3 4 5 6 7 8 9 10 |
public class ImportedProduct { public string Code { get; set; } public string Name { get; set; } public DateTime Timestamp { get; set; } public bool Active { get; set; } } |
Zmiana repozytorium produktów na Item Bucket
Po wybraniu item’u “standard values” template’u naszego produktu (“Product”), zaznaczamy w nim pola:
Ustawienie “Bucketable” oznacza że wszystkie produkty oparte o template “Product” mogą być umieszczone w item bucket. Zaznaczamy również “Lock child relationship”, które zastosujemy później.
Domyślnie Sitecore tworzy itemy w strukturze bucket’u, używając daty utworzenia item’u. Chcemy to zmienić, żeby zbliżyć naszą strukturę do tej użytej w systemie PIM. Możemy to zrobić tworząc nową akcję reguły tworzącej bucket poprzez dodanie itemu pod /sitecore/system/Settings/Rules/Definitions/Elements/Bucketing. Uzupełniamy pole “Text”, gdzie wpisujemy przyjazny, dla edytujących treść komunikat oraz pole “Type”, gdzie wskazujemy na klasę w naszym projekcie.
Stwórzmy teraz naszą klasę RuleAction we wskazanym wcześniej miejscu. Klasa musi posiadać metodę “Apply”, która definiuje każdy poziom struktury item bucket’u. Dla pierwszego poziomu, użyjemy właściwości “Active” (status produktu). Drugi poziom stworzymy na podstawie pierwszej litery nazwy produktu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
namespace Sitecore.Feature.Products.Rules { public class ProductBucketRule<T> : RuleAction<T> where T : BucketingRuleContext { public const string ActiveFolderName = "Active"; public const string InactiveFolderName = "Inactive"; public override void Apply(T ruleContext) { string[] path = new string[2]; var item = ruleContext.Database.GetItem(ruleContext.NewItemId); if (item != null && item.TemplateID == Templates.Product.ID) { var isActive = ((CheckboxField)item.Fields[Templates.Product.Fields.Active]).Checked; path[0] = isActive ? ActiveFolderName : InactiveFolderName; var productName = item.Fields[Templates.Product.Fields.InternalName].Value; path[1] = string.IsNullOrEmpty(productName) ? "#" : productName[0].ToString(); ruleContext.ResolvedPath = string.Join(Buckets.Util.Constants.ContentPathSeperator, path); } } } } |
Aby połączyć akcję z itemami naszych produktów edytujemy systemowy item /sitecore/system/Settings/Buckets/Item Bucket Settings. Wybieramy regułę “where the new bucketable item is based on the … template”, wybieramy template “Product” i przypisujemy akcję stworzoną w poprzednich krokach:
Teraz możemy zmienić nasze repozytorium produktów w item bucket. Robimy to klikając na przycisk “Bucket” w zakładce “Configure”, mając wybrany item repozytorium produktów w drzewku content editor’a. Jeśli mamy już jakieś produkty w repozytorium, możemy nacisnąć teraz przycisk “Sync”, który ustawi je w odpowiedniej strukturze.
Item bucket w importerze produktów
Ponieważ mamy dużo produktów do zaimportowania, chcemy poprawić wydajność mechanizmu importu. Możemy to osiągnąć podmieniając wyszukiwanie produktów bezpośrednio w bazie danych Sitecore, wyszukiwaniem za pomocą indeksu. Bierzemy indeks z item’u bucket’u, budujemy predykat i wykonujemy wyszukiwanie:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
private List<Item> SearchBucket(Item bucketRootItem, ID templateID) { if (bucketRootItem == null || !BucketManager.IsBucket(bucketRootItem)) return null; var indexableItem = new SitecoreIndexableItem(bucketRootItem); using (var searchContext = ContentSearchManager.GetIndex(indexableItem).CreateSearchContext()) { var predicate = PredicateBuilder.Create<SearchResultItem>( i => i.TemplateId == templateID && i.Path.StartsWith(bucketRootItem.Paths.Path)); var queryable = searchContext.GetQueryable<SearchResultItem>().Where(predicate); return queryable.Select(i => i.GetItem()).ToList(); } } |
Potrzebujemy także metody, która przebuduje domyślną strukturę bucket’u, tak żeby każdy stworzony item był umieszczony w odpowiednim miejscu, według początkowych założeń:
1 2 3 4 5 6 7 |
private void SyncBucket(Item item) { if (item != null && BucketManager.IsBucket(item)) { BucketManager.Sync(item); } } |
Użyjmy teraz nowych metod w kodzie importera. Zamieniamy wyszukiwanie istniejących produktów z metodą “SearchBucket” i dodajemy wywołanie metody “SyncBucket” pod koniec importu. Robimy to tylko w wypadku jeśli są jakieś zmienione produkty:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
protected void RunImporter() { var watch = System.Diagnostics.Stopwatch.StartNew(); var repositoryRoot = Context.Item; Log.Info($"Product importer started in {repositoryRoot.Name}", this); var changedItems = new List<Item>(); try { var existingProducts = SearchBucket(repositoryRoot, Templates.Product.ID); var productService = new ProductSerice(); var importedProducts = productService.GetAll(); IndexCustodian.PauseIndexing(); var counter = 0; foreach (var newProduct in importedProducts) { var sitecoreProduct = existingProducts.FirstOrDefault(x => x.Fields[Templates.Product.Fields.Code].Value == newProduct.Code); if (sitecoreProduct == null || newProduct.Timestamp > ((DateField)sitecoreProduct.Fields[Templates.Product.Fields.Timestamp]).DateTime) { Map(newProduct, ref sitecoreProduct, repositoryRoot); changedItems.Add(sitecoreProduct); } Log.Info($"Product importer processed {++counter} of {importedProducts.Count()} products", this); } } catch (Exception ex) { Log.Error("Product importer failed", ex, this); } finally { if (changedItems.Any()) { SyncBucket(repositoryRoot); } IndexCustodian.ResumeIndexing(); if (changedItems.Any()) { foreach (var index in ContentSearchManager.Indexes.Where(x => x.Name.Contains("master"))) { var changes = changedItems.Select(change => new SitecoreItemUniqueId(change.Uri)); IndexCustodian.IncrementalUpdate(index, changes); } } watch.Stop(); Log.Info($"Product importer completed, took {watch.ElapsedMilliseconds} ms", this); } } |
Musimy także zmienić trochę metodę “Map”, tak aby obsłużyć dodatkową flagę statusu produktu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
protected void Map(ImportedProduct newProduct, ref Item sitecoreProduct, Item repositoryRoot) { using (new SecurityDisabler()) { if (sitecoreProduct == null) { var name = ItemUtil.ProposeValidItemName(newProduct.Code); sitecoreProduct = repositoryRoot.Add(name, new TemplateID(Templates.Product.ID)); Log.Info($"--Product importer add new {name}", this); } try { sitecoreProduct.Editing.BeginEdit(); sitecoreProduct[Templates.Product.Fields.Code] = newProduct.Code; sitecoreProduct[Templates.Product.Fields.InternalName] = newProduct.Name; sitecoreProduct[Templates.Product.Fields.Timestamp] = DateUtil.ToIsoDate(newProduct.Timestamp); ((CheckboxField)sitecoreProduct.Fields[Templates.Product.Fields.Active]).Checked = newProduct.Active; sitecoreProduct.Appearance.DisplayName = $"{newProduct.Code} - {newProduct.Name}"; sitecoreProduct.Editing.EndEdit(); }catch (Exception ex) { sitecoreProduct.Editing.CancelEdit(); throw ex; } } } |
Po zakończeniu działania scheduled task’a powinniśmy widzieć nasze produkty w odpowiedniej strukturze item bucket’u:
W jednym z poprzednich kroków zaznaczyliśmy w standard values template’u naszego produktu pole “Lock child relationship”. Pozwala to nam na stworzenie itemów poniżej itemu, który jest “bucketable”, przykładowo możemy dodać tam warianty naszych produktów:
Dla itemów poniżej nie musimy zaznaczać opcji “Bucketable” i “Lock child relationship”.