Generalnie Apex jest fajny, składniowo podobny do Javy, łatwy w nauce ale posiada w swojej specyfice pewne pułapki, które nieświadomemu developerowi mogą przysporzyć nie lada problemów.
Ostatnio miałem przypadek, że musiałem podnieść pokrycie kodu do funkcjonalności, którą sam wcześniej pisałem. Ponieważ wymaganie miało pójść do testów w bardzo krótkim terminie, więc nie miałem za wiele czasu aby dokładnie napisać testy. Złe podejście, wiem.
Funkcjonalność ta działała w taki sposób, że użytkownik tworzył rekord a następnie na jego podstawie maksymalnie 100 podobnych, które automatycznie wysyłał do Approval Processu lub wybierając kilka rekordów będących już w Approvalu mógł je zatwierdzić lub odrzucić. Ponieważ przy około 50 rekordach wpadaliśmy już w limity CPU postanowiłem napisać do tego batch, który bardzo fajnie działał i spełniał wymaganie. Jednak podczas testów coś się psuło i test wyrzucał exception Template Rendering Exception: List has no rows for assignment to SObject: []
W sumie jasna sprawa, query nie zwraca rezultatów i stąd ten błąd... jednak w którym miejscu? Tego już debug logi nie pokazywały. Po żmudnym sprawdzaniu po kolei kolejnych elementów doszedłem do miejsca, gdzie ten template był renderowany. Posiadał w sobie komponent z kontrolerem. W konstruktorze inicializował pola, ok, nic szczególnego. Jednak w testach brakowało danych testowych, bo jak wiemy środowisko jest wtedy odizolowane od danych. Po sprawdzeniu sporej ilości pól doszedłem do metody z klasy Utilsowej, w której znalazłem poniższy twór :
Account account = [SELECT Id FROM Account WHERE Id = 'xyz' LIMIT 1];
return account;
W sumie składniowo jest to poprawne, ale w tym wypadku powodowało to wystąpienie powyższego exceptiona. Dlaczego? Ponieważ do obiektu nie możemy przypisać wyniku zapytania, które nic nie zwraca. Ot tyle lub aż tyle.
Jest to jedna z tych trudnych do zdebugowania pułapek, której można łatwo uniknąć konstruując coś takiego :
List<Account> accounts = [SELECT Id FROM Account WHERE Id = 'xyz' LIMIT 1];
Account account = (accounts.size() == 1) ? accounts.get(0) : null;
return account;
Powyższa konstrukcja przypisuje wynik zapytania do listy, która może otrzymać wynik pustego zapytania i nie rzuci nam wyjątkiem. Czyli albo otrzymamy wynik do listy, w tym wypadku będzie to tylko jeden indeks, albo zwrócimy null. Możemy pójść jeszcze dalej i nie zwracać nulla tylko np. nowy obiekt. Jest to przydatne w niektórych momentach, aby nie sprawdzać dodatkowo czy metoda nie zwraca nulla, np :
List<Account> accounts = [SELECT Id FROM Account WHERE Id = 'xyz' LIMIT 1];
Account account = (accounts.size() == 1) ? accounts.get(0) : new Account(....);
return account;
Pro tip : Używając wtyczki Illuminated Cloud w IntelliJ mamy nawet dostępny live template z powyższą konstrukcją. Wystarczy wpisać sqv1 i wcisnąć enter.
Oczywiście można zamiast return account; od razu zwrócić wynik czyli zrobić:
return (accounts.size() == 1) ? accounts.get(0) : new Account(....);
Trzeba zapamiętać, że zapytań SOQL nie wykonuje się w pętli. Koniec i kropka. Salesforce chcąc dostarczyć maksymalnie wydajny system narzuca na developera masę limitów. Między innymi Query limit, który można bardzo łatwo wywołać wykonując zapytanie w pętli.
Idealnym anty przykładem jest umieszczenie zapytania w triggerze lub batchu. Zazwyczaj w triggerze przetwarzamy jeden rekord i zapytanie w pętli Trigger.new (lub podobnej listy) nie spowoduje wywołania wyjątku, co jednak w sytuacji, kiedy w Trigger.new znajdzie się więcej rekordów? A co jeżeli potrzebujemy danych na jakich musimy operować?
Zazwyczaj robi się wtedy pobieranie danych do listy przed przystąpieniem do pętli. Jeżeli w liście obiektów przechowywanej w Trigger.new mamy jakieś relacje, musimy je jakoś wydostać z tej listy. Jak? Trzeba np. pobrać do seta wszystkie ID i wykonać zapytanie które pobierze nam wszystkie rekordy relacji. Następnie w pętli głównej mamy już obiekty na których możemy działać.
Bardzo ciekawą opcją jest pobieranie danych zapytaniem do mapy np.:
Map<Id, Account> accountMap = new Map<Id, Account>([SELECT Id, Name FROM Account]);
Taka konstrukcja daje nam potężne możliwości. Pozwala w łatwy sposób dostać się do obiektu Account znając jego ID, wystarczy użyć accountMap.get(myId);
Proste i skuteczne.