SOQL Lib

repository·main·Indexed 41 days ago

https://github.com/beyond-the-cloud-dev/soql-lib

A functional Apex library providing a fluent, type-safe API for constructing SOQL queries. It supports standard queries, optimized cached queries via SOQLCache, and patterns for implementing Selectors to encapsulate data access logic. Features include dynamic condition building with FilterGroups, cache refresh configuration via maxHoursWithoutRefresh, and a mocking framework for test execution.

Tokens
82.8K
Snippets
298
Records
319
Agent score
97%

What's inside SOQL Lib

  1. Overview of SOQL Lib capabilities

    main

    SOQL Lib is more than a query builder; it is a lightweight alternative to FFLib Selectors. Key capabilities include:

    • Mocking: Simulate query results for unit tests.
    • Caching: Cache query results to improve performance.
    • Selectors: Build lightweight, custom selectors.
    • Security: Control Field-Level Security (FLS) and sharing rules.
    • Result Functions: Clean up code with optimized data transformations.
    • Dynamic Building: Avoid string concatenation for dynamic SOQL.
  2. What is SOQL Lib and how does it differ from FFLib?

    main

    SOQL Lib is a modern Apex library designed as an intuitive, lightweight alternative to the FFLib Selectors framework. Unlike FFLib, which often leads to large, complex, and hard-to-maintain selector classes, SOQL Lib promotes Inline Query Construction using a fluent API. This allows developers to build business-specific queries exactly where they are needed, reducing the need for specialized selector methods and minimizing merge conflicts in team environments.

    Key differences include:

    • Lightweight Selectors: Focus on essential configurations (default fields, sharing) and generic methods (byId(), byRecordType()) rather than storing every possible query.
    • Fluent API: Uses a builder pattern for dynamic query construction instead of complex method naming conventions.
    • Modern Security: Built-in, intuitive support for Field-Level Security and sharing settings.
  3. Automatic variable binding in SOQL Lib

    main

    SOQL Lib automatically handles variable binding for all variables used in WHERE conditions. When you use a variable in a filter, the library generates a binding map (e.g., {"v1" : "value"}) and executes the query using Apex bind variables (e.g., :v1), which prevents SOQL injection and improves performance.

    // The variable 'Test' is automatically bound to v1
    // Resulting SOQL: SELECT Id, Name FROM Account WHERE Name LIKE '%Test%'
    SOQL.of(Account.SObjectType)
        .with(Account.Id, Account.Name)
        .whereAre(SOQL.Filter.with(Account.Name).contains('Test'))
        .toList();
  4. Field behavior in mocked results

    main

    When mocking results, SOQL Lib simulates standard Salesforce SOQL behavior regarding fields:

    1. Automatic Id Inclusion: The Id field is always included in mocked results, even if it was not explicitly requested in the .with() clause.
    2. Field Stripping: To simulate real SOQL, SOQL Lib strips all fields from the mocked data that were not explicitly included in the query's .with() clause.
      • Note: This currently applies to simple fields and subqueries.
      • Limitation: Relationship fields and queries using functions (like COUNT, AVG) are not yet subject to stripping and will return the exact mock provided.
    // Setup data with extra fields
    List<Account> accounts = new List<Account>{ 
        new Account(Name = 'Test 1', Description = 'Should be stripped') 
    };
    SOQL.mock('mockingQuery').thenReturn(accounts);
    
    // Query only Name
    List<Account> result = SOQL.of(Account.SObjectType)
        .with(Account.Name)
        .mockId('mockingQuery')
        .toList();
    
    // result[0].Name is 'Test 1'
    // result[0].Description is null (stripped)
  5. How the mocking stack works for sequential mocks

    main

    SOQL Lib supports a FIFO (First In, First Out) stack for multiple mocks assigned to the same ID. This allows you to simulate a query returning different results on consecutive calls.

    • Single Mock: If you only define one mock for an ID, it is reused for every execution of that query.
    • Multiple Mocks: Each call to SOQL.mock(id).thenReturn(data) adds a new entry to the stack. Each time the query is executed, the first mock in the stack is consumed and removed. The last mock in the stack is not removed and will be returned for all subsequent calls.
    // Setup multiple sequential mocks
    SOQL.mock('testQuery').thenReturn(new Account(Name = 'First Call'));
    SOQL.mock('testQuery').thenReturn(new Account(Name = 'Second Call'));
    SOQL.mock('testQuery').thenReturn(new Account(Name = 'Third Call'));
    
    // First execution returns "First Call", then removes that mock
    Account result1 = SOQL.of(Account.SObjectType).mockId('testQuery').toObject();
    
    // Second execution returns "Second Call", then removes that mock
    Account result2 = SOQL.of(Account.SObjectType).mockId('testQuery').toObject();
    
    // Third execution returns "Third Call", but does not remove it
    Account result3 = SOQL.of(Account.SObjectType).mockId('testQuery').toObject();
    
    // Fourth execution also returns "Third Call"
    Account result4 = SOQL.of(Account.SObjectType).mockId('testQuery').toObject();
  6. Automatic Id field behavior in mocks

    main

    When mocking results with SOQLCache, the Id field is automatically included in the returned SObject, even if it was not explicitly requested in the .with() clause. This mirrors standard Salesforce SOQL behavior where the Id is always present in query results.

    // Even if Id is not in .with(), it is included in the output
    SOQLCache.mock('mockingQuery').thenReturn(new List<Account>{ 
        new Account(Name = 'Test 1') 
    });
    
    List<Account> accounts = (List<Account>) SOQLCache.of(Account.SObjectType)
        .with(Account.Name)
        .mockId('mockingQuery')
        .toObject();
    
    // Output: Account:{Name=Test 1, Id=001J5000008AvzkIAC}
  7. How SOQL Cached Selectors work

    main

    SOQL Cached Selectors are the recommended way to implement caching. Instead of building queries ad-hoc, you create a class per SObjectType that extends SOQLCache and implements SOQLCache.Selector.

    This approach allows you to:

    1. Set default caching configurations (fields, cache scope, TTL) for a specific object in the constructor.
    2. Centralize reusable cached queries in one place.
    3. Provide a clean, domain-specific API (e.g., .byName()) for querying that object.
    public inherited sharing class SOQL_CachedProfile extends SOQLCache implements SOQLCache.Selector {
        public static SOQL_CachedProfile query() {
            return new SOQL_CachedProfile();
        }
    
        private SOQL_CachedProfile() {
            super(Profile.SObjectType);
            // default settings
            with(Profile.Id, Profile.Name, Profile.UserType)
                .cacheInOrgCache()
                .maxHoursWithoutRefresh(36);
        }
    
        public override SOQL.Queryable initialQuery() {
            return SOQL.of(Profile.SObjectType);
        }
    
        public SOQL_CachedProfile byName(String profileName) {
            whereEqual(Profile.Name, profileName);
            return this;
        }
    }
  8. Returning concrete SOQL instances for query customization

    main

    Instead of returning a raw List<SObject> or Object immediately, you can design custom query classes that extend SOQL and implement SOQL.Selector. By having domain-specific methods return this (the concrete class instance), you allow developers to chain additional SOQL clauses or domain-specific filters without duplicating code.

    This pattern enables a highly readable and reusable query builder for specific SObjects.

    // 1. Define a custom query class
    public inherited sharing class SOQL_Account extends SOQL implements SOQL.Selector {
        public static SOQL_Account query() {
            return new SOQL_Account();
        }
    
        private SOQL_Account() {
            super(Account.SObjectType);
            // default settings
            with(Account.Name, Account.AccountNumber)
                .systemMode();
        }
    
        public SOQL_Account byIndustry(String industry) {
            with(Account.Industry)
                .whereAre(Filter.with(Account.Industry).equal(industry));
            return this; // Returns the concrete class to allow further chaining
        }
    }
    
    // 2. Use the custom query class with chaining
    SOQL_Account.query()
        .byIndustry('IT')
        .orderBy(Account.Name)
        .toList();
  9. Use SOQL Filter to specify conditions

    main

    The SOQL.Filter class allows you to specify and adjust single conditions within a SOQL query. You typically use it within a .whereAre() call after defining the SObject type. Filters are composed of a field selection (using methods like id(), name(), or with()) followed by a comparator (like equal(), isNull(), or isIn()).

    SOQL.of(Account.SObjectType)
        .whereAre(SOQL.Filter.name().equal('My Account'))
        .toList();
  10. Understand the Cached SOQL Selector interface

    main

    The Cacheable interface (used by SOQLCache) provides a limited set of operations specifically for retrieving and managing cached records. Unlike the standard SOQL Library, methods related to sharing rules (e.g., withSharing(), withoutSharing()) or query scoping (e.g., forView()) are not available because cached records are retrieved directly from storage, bypassing standard database access controls.

    Key categories of methods available in the Cacheable interface include:

    • Configuration: cacheInApexTransaction(), cacheInOrgCache(), cacheInSessionCache(), and maxHoursWithoutRefresh(Integer hours).
    • Selection: with(...) to specify fields, whereEqual(...) for filtering, and byId(...) for direct ID lookups.
    • Security: stripInaccessible() to enforce field-level security.
    • Results: toId(), doExist(), toObject(), and toValueOf(SObjectField fieldToExtract) to extract the data.
    public interface Cacheable {
        // CONFIG
        Cacheable cacheInApexTransaction();
        Cacheable cacheInOrgCache();
        Cacheable cacheInSessionCache();
        Cacheable maxHoursWithoutRefresh(Integer hours);
        // SELECT
        Cacheable with(SObjectField field);
        Cacheable with(List<SObjectField> fields);
        Cacheable with(String fields);
        // WHERE
        Cacheable whereEqual(SObjectField field, Object value);
        Cacheable whereEqual(String field, Object value);
        // FIELD-LEVEL SECURITY
        Cacheable stripInaccessible();
        Cacheable stripInaccessible(AccessType accessType);
        // MOCKING
        Cacheable mockId(String queryIdentifier);
        // DEBUGGING
        Cacheable preview();
        // PREDEFINED
        Cacheable byId(SObject record);
        Cacheable byId(Id recordId);
        // RESULT
        Id toId();
        Boolean doExist();
        SObject toObject();
        Object toValueOf(SObjectField fieldToExtract);
    }
  11. Implement minimalistic Selectors

    main

    A Selector is a specialized class extending SOQL that implements SOQL.Selector. It is designed to encapsulate core query logic for a specific object while remaining lightweight.

    Best Practices:

    1. Minimal Default Fields: Only include essential identifiers (like Id) and commonly used fields in the constructor to improve performance.
    2. Method Chaining: Implement domain-specific methods (e.g., byIndustry(String industry)) that return this to allow for fluent query construction.
    3. Dynamic Extension: Allow callers to add additional fields via .with() or complex clauses via .whereAre() at the point of use, rather than bloating the selector with every possible permutation.
    public inherited sharing class SOQL_Account extends SOQL implements SOQL.Selector {
        public static SOQL_Account query() {
            return new SOQL_Account();
        }
    
        private SOQL_Account() {
            super(Account.SObjectType);
            with(Account.Id, Account.Name);
            systemMode();
            withoutSharing();
        }
    
        public SOQL_Account byType(String type) {
            whereAre(Filter.with(Account.Type).equal(type));
            return this;
        }
    }
    
    // Usage
    List<Account> accounts = SOQL_Account.query()
        .byType('Partner')
        .with(Account.AnnualRevenue)
        .toList();
  12. Build WHERE clauses with SOQL.Filter and SOQL.FilterGroup

    main

    To construct a WHERE clause in SOQL Lib, use SOQL.Filter for individual conditions and SOQL.FilterGroup to group multiple conditions together.

    If you are using the inline Query Builder, use SOQL.of(...). If you are using a Selector, replace SOQL.of(...) with YourSelectorName.query().

    Key Abstractions:

    • SOQL.Filter: Represents a single condition (e.g., field = value).
    • SOQL.FilterGroup: A collection of filters that can be combined using conditionLogic (e.g., AND, OR).
    SOQL.of(Account.SObject)
        .with(Account.Id, Account.Name)
        .whereAre(SOQL.FilterGroup
            .add(SOQL.Filter.with(Account.Id).equal(account.Id))
            .add(SOQL.Filter.with(Account.Name).contains(accountName))
        )
        .toList();