SOQL – Salesforce Object Query Language

SOQL – Salesforce Object Query Language

SOQL, czyli Salesforce Object Query Language jest językiem, który w bardzo dużym stopniu przypomina język SQL. W przypadku platformy Salesforce jest on wykorzystywany do pobierania danych dotyczących jednego obiektu (lub wielu obiektów, które są z nim powiązane). Zapytania napisane w języku SOQL możemy wykorzystywać bezpośrednio wewnątrz klas napisanych w języku Apex, za pośrednictwem narzędzia jakim jest Developer Console w oknie Anonymous Apex lub też za pomocą wtyczek przeglądarkowych takich jak choćby Salesforce Inspector. W przypadku, gdy tworzymy zapytanie wewnątrz klasy napisanej w języku Apex i chcemy przypisać jego wynik do listy obiektów, musimy pamiętać o umieszczeniu zapytania wewnątrz nawiasów kwadratowych. Jedno z najprostszych zapytań zapisujące znalezione rekordy standardowego obiektu Account do listy może więc wyglądać następująco.

List<Account> accounts = [SELECT Id FROM Account];

Oczywiście takie zapytanie może, podobnie jak w SQL, zawierać słowa kluczowe takie jak WHERE, LIMIT, czy ORDER BY, które pozwalają na filtrowanie rekordów, ograniczenie ich ilości, czy też sortowanie. Słów kluczowych jest oczywiście znacznie więcej, więc warto zapoznać się z oficjalną dokumentacją. Poniżej znajduje się przykład nieco bardziej rozbudowanego zapytania.

List<Account> accounts = [
    SELECT Id,
           Name,
           BillingCity,
           AnnualRevenue,
           CreatedDate
    FROM Account
    WHERE Name LIKE '%Adrian Kurek%'
    ORDER BY CreatedDate DESC
    LIMIT 10
];

Dość istotnym zagadnieniem w przypadku SOQL są zapytania relacyjne (relationship queries), które pozwalają na pozyskanie razem z rekordami powiązanych z nimi rodziców lub dzieci. Istnieją dwa rodzaje takich zapytań. Pierwszym z nich jest zapytanie rodzaju child to parent, w przypadku którego głównym obiektem (tym którego używamy zaraz po słowie kluczowym FROM) jest dziecko. Ten rodzaj zapytania wydaje się być znacznie prostszym i bardziej intuicyjnym i w zasadzie faktycznie tak jest.

List<Contact> contacts = [
    SELECT Id,
           Name,
           Account.Id,
           Account.Name,
           Account.BillingCity
    FROM Contact
    LIMIT 10
];

Jak widać w powyższym przykładzie (gdzie obiekt Account jest rodzicem obiektu Contact) pobieramy listę 10 rekordów typu Contact, a pola powiązanych z nimi rekordów Account pozyskujemy wykorzystując notację kropkową (dot notation). Nieco bardziej skomplikowane wydawać się mogą zapytania rodzaju parent to child, gdzie to rodzic jest głównym obiektem. W tym przypadku musimy użyć podzapytania (subquery) i pamiętać, by w podzapytaniu wykorzystać liczbę mnogą nazwy dziecka w przypadku obiektów standardowych (np. Contacts zamiast Contact, Opportunities zamiast Opportunity itd.) – jest to tak zwana nazwa relacji (relationship name).

List<Account> accounts = [
    SELECT Id,
           Name,
           (SELECT Id,
                   Name
            FROM Contacts)
    FROM Account
    LIMIT 10
];

W przypadku zapytań parent to child, rodzic może mieć zero lub wiele dzieci. Chcąc dla przykładu wypisać wszystkie rekordy Contact powiązane z danym rekordem Account, które pozyskaliśmy powyższym zapytaniem, możemy wykorzystać poniższy kod.

for (Account a : accounts) {
    System.debug('List of all Contacts of "' + a.Name + '" Account:');
    
    for (Contact c : a.Contacts) {
        System.debug(c.Name);
    }
}

Wynik wykonania powyższego kodu.

22:42:54:024 USER_DEBUG [10]|DEBUG|List of all Contacts of "Edge Communications" Account:
22:42:54:031 USER_DEBUG [13]|DEBUG|Rose Gonzalez
22:42:54:031 USER_DEBUG [13]|DEBUG|Sean Forbes
22:42:54:033 USER_DEBUG [10]|DEBUG|List of all Contacts of "Burlington Textiles Corp of America" Account:
22:42:54:034 USER_DEBUG [13]|DEBUG|Jack Rogers
22:42:54:035 USER_DEBUG [10]|DEBUG|List of all Contacts of "Pyramid Construction Inc." Account:
22:42:54:039 USER_DEBUG [13]|DEBUG|Pat Stumuller
22:42:54:039 USER_DEBUG [10]|DEBUG|List of all Contacts of "Dickenson plc" Account:
22:42:54:041 USER_DEBUG [13]|DEBUG|Andy Young
22:42:54:041 USER_DEBUG [10]|DEBUG|List of all Contacts of "Grand Hotels & Resorts Ltd" Account:
22:42:54:044 USER_DEBUG [13]|DEBUG|Tim Barr
22:42:54:045 USER_DEBUG [13]|DEBUG|John Bond
22:42:54:045 USER_DEBUG [10]|DEBUG|List of all Contacts of "United Oil & Gas Corp." Account:
22:42:54:047 USER_DEBUG [13]|DEBUG|Stella Pavlova
22:42:54:049 USER_DEBUG [13]|DEBUG|Lauren Boyle
22:42:54:051 USER_DEBUG [13]|DEBUG|Avi Green
22:42:54:052 USER_DEBUG [13]|DEBUG|Arthur Song
22:42:54:052 USER_DEBUG [10]|DEBUG|List of all Contacts of "Express Logistics and Transport" Account:
22:42:54:054 USER_DEBUG [13]|DEBUG|Babara Levy
22:42:54:056 USER_DEBUG [13]|DEBUG|Josh Davis
22:42:54:056 USER_DEBUG [10]|DEBUG|List of all Contacts of "University of Arizona" Account:
22:42:54:058 USER_DEBUG [13]|DEBUG|Jane Grey
22:42:54:058 USER_DEBUG [10]|DEBUG|List of all Contacts of "United Oil & Gas, UK" Account:
22:42:54:061 USER_DEBUG [13]|DEBUG|Ashley James
22:42:54:061 USER_DEBUG [10]|DEBUG|List of all Contacts of "United Oil & Gas, Singapore" Account:
22:42:54:064 USER_DEBUG [13]|DEBUG|Tom Ripley
22:42:54:064 USER_DEBUG [13]|DEBUG|Liz D'Cruz

Sprawa wygląda minimalnie inaczej w przypadku zapytań związanych z obiektami niestandardowymi, w przypadku których należy pamiętać o używaniu przyrostków __c (obiekty i pola niestandardowe) oraz __r (relacje niestandardowe). Poniżej znajdują się dwa zapytania związane z obiektami niestandardowymi. Pierwsze jest zapytaniem child to parent, a drugie parent to child. W przykładzie wykorzystane są niestandardowe obiekty Driver__c (dziecko) i Vehicle__c (rodzic).

// Zapytanie child to parent.
List<Driver__c> drivers = [
    SELECT Name,
           Nationality__c,
           Vehicle__r.Id,
           Vehicle__r.Manufacturer__c
    FROM Driver__c
    WHERE Vehicle__r.Id = 'a000900000FThKjAAL'
];

// Zapytanie parent to child.
List<Vehicle__c> vehicles = [
    SELECT Id,
           Name,
           Manufacturer__c,
           (SELECT Name,
                   Nationality__c
            FROM Drivers__r)
    FROM Vehicle__c
    WHERE Id = 'a000900000FThKjAAL'
];

Wskazówka #1

Wynikiem zapytań napisanych w języku SOQL może być liczba (w przypadku funkcji agregacyjnych takich jak COUNT), pojedynczy rekord lub lista rekordów. Jeśli celem naszego zapytania ma być pozyskanie wyłącznie jednego rekordu (np. przy wykorzystaniu słowa kluczowego LIMIT 1), to nie najgorszym nawykiem jest jednak zapisywanie jego wyniku w liście. Weźmy pod uwagę dwa poniżej przedstawione zapytania.

List<Account> accounts = [
    SELECT Id
    FROM Account
    WHERE Name LIKE '%Pyramid%'
    LIMIT 1
];

Account acc = [
    SELECT Id
    FROM Account
    WHERE Name LIKE '%Pyramid%'
    LIMIT 1
];

Oba są jak najbardziej w porządku i wszystko będzie okej, o ile ten jeden rekord zostanie w bazie danych znaleziony. Co jednak stanie się, gdy nie znajdziemy rekordu? W takiej sytuacji w pierwszym zapytaniu nie stanie się w zasadzie nic. Lista będzie po prostu pusta. W tym drugim przypadku rzucony zostanie wyjątek wskazujący, że lista nie zawiera wierszy, które mogłyby zostać przypisane do obiektu Salesforce.

System.QueryException: List has no rows for assignment to SObject

Z błędem w przypadku zmiennej wskazującej na jeden rekord spotkamy się również, gdy z jakiegoś powodu pominiemy w zapytaniu LIMIT 1.

List<Account> accounts = [
    SELECT Id
    FROM Account
    WHERE Name LIKE '%United%'
];

Account acc = [
    SELECT Id
    FROM Account
    WHERE Name LIKE '%United%'
];

Tak jak poprzednio również i w tym przykładzie w przypadku chęci przypisania wyniku zapytania SOQL do listy wszystko będzie w porządku bez względu na to, czy znajdziemy 0, 1, czy więcej rekordów. Problem ponownie pojawi się jednak, gdy będziemy chcieli przypisać wynik do zmiennej wskazującej na pojedynczy rekord, a znalezionych ich zostanie więcej niż 1. Wtedy zetkniemy się z poniższym wyjątkiem.

System.QueryException: List has more than 1 row for assignment to SObject

Korzystanie w takiej sytuacji z listy wydaje się więc być bezpieczniejszym rozwiązaniem, pozwalającym na uniknięcie pojawienia się wyjątku.

Wskazówka #2

Jeśli pobieramy jakieś rekordy w celu zaktualizowania któregoś z ich pól, nie musimy wyszczególniać go w zapytaniu. W poniższym przykładzie przypisujemy do pola Name pięciu obiektów nową wartość, mimo że pola Name nie pobraliśmy bezpośrednio w zapytaniu i jest to jak najbardziej w porządku.

List<Account> accounts = [SELECT Id FROM Account LIMIT 5];

for (Integer i = 0; i < accounts.size(); i++) {
    accounts[i].Name = 'New Account Name ' + i;
}

update accounts;

Sytuacja wygląda inaczej, gdy chcemy wartość pola Name w jakiś sposób wykorzystać (na przykład wypisać).

List<Account> accounts = [SELECT Id FROM Account LIMIT 5];

for (Account acc : accounts) {
    System.debug('Account Name: ' + acc.Name);
}

Po uruchomieniu powyższego kod rzucony zostanie wyjątek, ponieważ nie pobraliśmy pola Name obiektu Account, a mimo to chcemy jego wartość wykorzystać. Jest to chyba jeden z najprostszych do rozwiązania, choć najczęściej spotykanych błędów.

System.SObjectException: SObject row was retrieved via SOQL without querying the requested field: Account.Name

Poniższy kod będzie już więc jak najbardziej okej, ponieważ uwzględniamy pole Name w zapytaniu SOQL.

List<Account> accounts = [SELECT Name FROM Account LIMIT 5];

for (Account acc : accounts) {
    System.debug('Account Name: ' + acc.Name);
}

Trzeba więc pamiętać o tym, jak istotne jest branie pod uwagę pól, których wartości próbujemy w swoim kodzie w jakiś sposób użyć.

Wskazówka #3

Jedną z najważniejszych rzeczy, o których należy pamiętać w przypadku platformy Salesforce, są różnego rodzaju limity, które dotyczą również zapytań SOQL. Jednym z najłatwiejszych do przekroczenia limitów jest limit związany z ilością zapytań SOQL (Too many SOQL queries: 101). Taki limit przekroczyć można stosując zapytanie do bazy danych w pętli.

for (Integer i = 0; i < 125; i++) {
    Account c = [SELECT Id FROM Account LIMIT 1];
}

Jako że maksymalna liczba zapytań SOQL w przypadku kodu synchronicznego wynosi 100 (200 w przypadku kodu asynchronicznego), to uruchamiając powyższy kod otrzymamy wyjątek i to bez względu na to, czy rekordów Account w bazie danych będzie 5, 90, czy 500.

System.LimitException: Too many SOQL queries: 101

Warto więc pamiętać o tym, by wszystkie rekordy, na których chcemy wykonać jakieś operacje, pobierać przed wszelkimi pętlami i unikać stosowania zapytań wewnątrz nich.

Wskazówka #4

Kolejnym limitem, w uniknięciu którego pomóc może umiejętne stosowanie zapytań SOQL jest Heap Size Limit, który w Salesforce wynosi dla transakcji synchronicznych 6MB (12MB dla transakcji asynchronicznych). Przekroczenie go jest już nieco trudniejsze, jednak w przypadku bardziej zaawansowanych projektów, gdzie w grę wchodzi konieczność przetwarzania ogromnych ilości rekordów, trzeba o nim pamiętać i każdą szansę pozwalającą na ograniczenie ilości przechowywanych w pamięci danych starać się wykorzystać. Jako że zapytania SOQL mogą zwracać listę rekordów, możemy wykorzystywać je w formie wyrażeń na przykład w pętli for. Jeśli więc potrzebujemy pobrać z rekordów Account wyłącznie ich nazwy, zapisać je wewnątrz listy, by później jakoś je wykorzystać (i jednocześnie jest to jedyny powód dla którego przechowujemy rekordy Account w taki sposób), nie ma większego sensu przechowywać rekordów Account w osobnej liście tak jak w przypadku poniższego przykładu.

List<Account> accounts = [SELECT Name FROM Account LIMIT 10];
List<String> accountNames = new List<String>();

for (Account acc : accounts) {
    accountNames.add(acc.Name);
}

// 08:42:52:013 USER_DEBUG [8]|DEBUG|Heap Size: 1948
System.debug('Heap Size: ' + System.Limits.getHeapSize());

Zamiast powyższego kodu wykorzystać możemy ten zaprezentowany poniżej. Efekt będzie ten sam, lecz Heap Size będzie mniejszy.

List<String> accountNames = new List<String>();

for (Account acc : [SELECT Name FROM Account LIMIT 10]) {
    accountNames.add(acc.Name);
}

// 08:45:57:146 USER_DEBUG [8]|DEBUG|Heap Size: 1395
System.debug('Heap Size: ' + System.Limits.getHeapSize());

Już na pierwszy rzut oka widać różnicę w Heap Size i mimo że w tym konkretnym przypadku nie jest ona ogromna, to nietrudno wyobrazić sobie, jak wyglądałaby ona w przypadku nieporównywalnie większej liczby rekordów. Stosując więc takie podejście nie dość, że zmniejszamy Heap Size, to ograniczamy liczbę linii kodu.

Wskazówka #5

Podczas pracy nad jakimś rozwiązaniem niejednokrotnie będziemy potrzebowali, by kolekcja, jaką jest mapa, przechowywała na przykład Id rekordów Account w postaci klucza oraz związanych z tymi Id rekordów Account w postaci wartości. Jednym z pierwszych i jak najbardziej poprawnych (choć nie najlepszych) rozwiązań jakie przyjść może do głowy, jest pobranie listy rekordów Account za pomocą zapytania SOQL i dodanie ich do mapy przy użyciu pętli for, tak jak zostało to przedstawione w poniższym przykładzie.

List<Account> accounts = [SELECT Id FROM Account LIMIT 10];
Map<Id, Account> accountIdsToAccounts = new Map<Id, Account>();

for (Account acc : accounts) {
    accountIdsToAccounts.put(acc.Id, acc);
}

Dokładnie ten sam efekt, choć w znacznie bardziej elegancki i krótszy sposób, osiągnąć można po prostu przekazując listę rekordów Account jako argument w konstruktorze mapy.

Map<Id, Account> accountIdsToAccounts2 = new Map<Id, Account>([SELECT Id FROM Account LIMIT 10]);

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *