Jednym z najczęstszych zadań podczas implementacji Sitecore jest import danych i zapisanie ich w Sitecore w postaci itemów. Przeważnie robi się to jeśli mamy zewnętrzny system PIM, gdzie zarządzamy jakiegoś typu produktami i chcemy wyświetlić je na naszej stronie, bez utraty wydajności spowodowanej ciągłą komunikacją z tym systemem. Przeważnie produkty wyświetla się z dodatkowymi informacjami, zarządzanymi bezpośrednio w Sitecore, takimi jak opisy rich text i obrazki. Dodatkowo chcemy skorzystać z personalizacji Sitecore. Oczywiście chcemy aby dane produktów zapisane w Sitecore były aktualne.
Kluczowym komponentem implementacji jest Sitecore scheduled task, który jest kombinacją itemów i naszego kodu. W pierwszej części tego poradnika skupimy się na stworzeniu task’a, w kolejnym poprawimy nasz kod dodając item bucket.
Item’y Scheduled task
Repozytorium produktów
Zaczynamy tworząc item, który będzie pełnił rolę repozytorium produktów. W naszym wypadku, jest to prosty item, nie posiadający żadnych niestandardowych pól:
Task Command
Ok, stwórzmy teraz item definiujący task command. Dodajemy go pod /sitecore/system/Tasks/Commands. W polu “Type” definiujemy lokalizację naszego kodu task’a, w formacie “namespace.class, assembly name”. W polu “Method” informujemy Sitecore, którą metodę z naszej klasy powinien zawołać.
Task schedule
Tworzymy teraz item definiujący schedule naszego taska w /sitecore/system/Task/Schedules. Ten item jest trochę bardziej skomplikowany. W polu “Command” wybieramy command item stworzony w poprzednim kroku. W polu “Items”, możemy podać ID itemów, oddzielone znakiem |, które chcemy przekazać do naszgo kodu, w naszym przypadku wpisujemy tam ID itemu repozytorium produktów stworzonego w pierwszym korku. W polu “Schedule” definiujemy kiedy task powienien być uruchomiony po raz pierwszy, ostatni i z jaką cząstotliwością Sitecor powinien go uruchamiać. Format pola to oddzielone znakiem |:
- Pierwsza data uruchomienia w formacie yyyyMMdd
- Ostatnia data uruchomienia w formacie yyyyMMdd
- Dzień tygodnia, kiedy task powinien być uruchamiany, w formie mapy bitowej, gdzie:
- 1=Niedziela
- 2=Poniedziałek
- 4=Wtorek
- 8=Środa
- 16=Czwartek
- 32=Piątek
- 64=Sobota
- Czyli na przykład od poniedziałku do piątku to 62 a codziennie to 127
- Przedział czasu pomiędzy kolejnymi uruchomieniami w formacie HH:mm:ss
Więc 19990101|21000101|127|01.00:00 oznacza wystartuj task od razu, uruchamiaj co godzinę do roku 2100.
Pole “Last run” informuje Sitecore’a (i nas) kiedy task był uruchamiany ostatnim razem. Pole jest automatyczne uaktualniane przez Sitecore. Więc żeby wymusić uruchomienie taska, możemy ustawić w tym polu datę w przeszłości i zapisać item.
Zaznaczenie “Async” oznacza, że kolejna instancja taska może być uruchomiona, jeśli poprzednia nie została zakończona. Zaznaczenie “Auto remove” spowoduje usunięcie itemu definiującego schedule, co wymusi jednokrotne uruchomienie taska. W naszym przypadku, zostawiamy oba niezaznaczone.
Konfiguracja Sitecore Scheduler’a
Możemy teraz zapytać, dlaczego nasz kod jest cyklicznie odpalany jeśli zdefiniujemy te item’y w Sitecore? Odpowiedzią jest scheduling agent zdefiniowany w konfiguracji Sitecore, który cyklicznie sprawdza itemy z definicjami scheduled tasków i uruchamia je. Swoją drogą scheduling agents są kolejnym sposobem na cykliczne uruchamianie kodu w Sitecore. Jeśli jesteś zainteresowany jak się ich używa, zapraszam do wpisu John’a Westa z odnośnika poniżej.
Agent nazywa się “Master_Database_Agent” typu “Sitecore.Tasks.DatabaseAgent”. W Sitecore 8.x jest zdefiniowany w pliku Sitecore.Processing.config. Ważne:
- Plik powinien być wyłączony na środowiskach, na których nie chcemy uruchamiać tasków. Na przykład przeważnie wyłącza się ten plik na serwerach CD i Reporting.
- Częstotliwość zdefiniowana w itemie w polu “Schedule” nie może być częstsza niż, ta zdefiniowana w pliku konfiguracyjnym w “interval”.
Domyślna konfiguracja wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <scheduling> <!-- An agent that processes scheduled tasks embedded as items in the core database. --> <agent type="Sitecore.Tasks.DatabaseAgent" method="Run" interval="00:10:00" name="Core_Database_Agent"> <param desc="database">core</param> <param desc="schedule root">/sitecore/system/tasks/schedules</param> <LogActivity>true</LogActivity> </agent> <!-- An agent that processes scheduled tasks embedded as items in the master database. --> <agent type="Sitecore.Tasks.DatabaseAgent" method="Run" interval="00:10:00" name="Master_Database_Agent"> <param desc="database">master</param> <param desc="schedule root">/sitecore/system/tasks/schedules</param> <LogActivity>true</LogActivity> </agent> </scheduling> </sitecore> </configuration> |
Kod Sitecore Scheduled Task’a
Jesteśmy teraz gotowi, żeby napisać trochę kodu, zaczynając od stworzenia publicznej klasy “ProductImporter” w namespace i assembly zdefiniowanej w task command item (w polu “Type”) i metody, o nazwie, którą wpisaliśmy w polu “Method”. Metoda musi być publiczna i posiadać trzy parametry:
- items: lista itemów zdefiniowana w polu “Items” w itemie task schedule
- command: item task command
- schedule: item task schedule
Nasz kod jest uruchamiany na stronie zdefiniowanej jako “scheduler” w Sitecore.config w sekcji <sites>, bez context item’u, dlatego musimy przełączyć się na inną bazę (“master” w naszym wypadku). Aby uprościć dalszą implementację ustawiamy też context item na item naszego repozytorium produktu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public void Execute(Item[] items, CommandItem command, ScheduleItem schedule) { if (items == null || items.Length == 0) { Log.Warn($"{schedule.InnerItem.Paths.Path} tried to run without selected item", this); return; } using (new DatabaseSwitcher(Configuration.Factory.GetDatabase("master"))) { using (new ContextItemSwitcher(items[0])) { RunImporter(); } } } |
W metodzie RunImporter wołamy web service (productService.GetAll()), który zwraca produkty z zewnętrznego systemu, a następnie, w pętli dla każdego z nich, sprawdzamy czy produkt z tym samym, unikalnym kodem istnieje w repozytorium produktów w Sitecore. Jeśli nie, dodajemy nowy item, w przeciwnym wypadku uaktualniamy item, sprawdzając wcześniej czy timestamp (data utworzenia/ostatniej modyfikacji) zwrócony przez web service jest późniejszy od tego zapisanego w Sitecore.
Cały kod, powiązany z wołaniem web service’u jest w bloku try catch, ponieważ nie możemy w pełni ufać zewnętrznemu źródłu danych, które może nie być dostępne. Dodajemy również kod raportujący wydajność importera za pomocą klasy System.Diagnostics.StopWatch.
Sitecore IndexCustodian
Od razu wprowadzamy kod, poprawiający wydajność za pomocą Sitecore.ContentSearch.Maintenance.IndexCustodian, która jest wbudowaną w Sitecore pomocniczą klasą do zarządzania indeksami wyszukiwania.
To co zrobiliśmy to wstrzymanie indeksowania przed importem produktów (IndexCustodian.PauseIndexing) oraz wznowienie indeksowania po nim (IndexCustodian.ResumeIndexing).
Dodatkowo zapamiętujemy, które item’y były utworzone, lub zmienione i za jednym razem uaktualniamy je w indeksach wyszukiwania (IndexCustodian.IncrementalUpdate). W naszym wypadku tylko w indeksach powiązanych z bazą “Master”. To podejście może poprawić wydajność, jeśli zmieniamy dużą ilość itemów, używając domyślnej strategii uaktualniania indeksów, która synchronizuje je po każdej zmianie itemu.
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 |
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 = repositoryRoot.Axes.GetDescendants().Where(i => i.TemplateID == 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 { 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); } } |
Zapis itemu w Sitecore
Model danych użyty w naszym importerze jest bardzo prosty. Zakładamy, że z zewnętrznego systemu ładujemy unikalny kod i nazwę produktu. Dodatkowo każdy obiekt ma swój timestamp:
1 2 3 4 5 6 7 8 |
public class ImportedProduct { public string Code { get; set; } public string Name { get; set; } public DateTime Timestamp { get; set; } } |
Odpowiadający modelowi template Sitecore:
Ostatni fragment naszego kody mapuje obiekt zwrócony przez web service na item w Sitecore. Zabezpieczamy kod w bloku try catch, w razie zmiany formatu oczekiwanych przez nas danych. Zakładamy że kody produktów są unikalne i używamy ich jako nazwy itemów. Dodatkowo, aby ułatwić pracę Content Editorów, ustawiamy DisplayName na kod i nazwę 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 |
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); sitecoreProduct.Appearance.DisplayName = $"{newProduct.Code} - {newProduct.Name}"; sitecoreProduct.Editing.EndEdit(); }catch (Exception ex) { sitecoreProduct.Editing.CancelEdit(); throw ex; } } } |
Poprawienie bezpieczeństwa itemów produktów
Chcemy uniemożliwić manualną zmianę danych importowanych z zewnętrznego systemu przez edytorów. Aby to osiągnąć możemy użyć Sitecore security na itemach pól naszego template’u. Powtarzamy to dla pozostałych, importowanych pól:
Możemy to przetestować, logując się do Sitecore’a jako edytor (ustawienia Sitecore security nie mają wpływu na konto administratora). Po zrobieniu check-in itemu produktu widzimy, że importowane pola pozostają tylko do odczytu, a pozostałe pola można edytować: