fflib selector - configure Read and FLS - fflib_SObjectSelector vs fflib_QueryFactory

You should always call a selector class and the methods within that class ... eg selectXxxByYyy(...) then exploit the queryfactory class to construct queries , especially composite queries. The selector class is referenced in Application.cls and can be mocked in test methods. Andrew Fawcett's book goes over all of this and is well worth studying.

Let's say you have a Selector class on Account and invoke it by:

AccountsSelector.newInstance().selectByXXX(args)

This invokes a factory method

public static IAccountsSelector newInstance()    {
    return (IAccountsSelector) Application.Selector.newInstance(Account.SObjectType);
}

This in turn executes this method in fflib_Application

public fflib_ISObjectSelector newInstance(SObjectType sObjectType)
    {
        // Mock implementation?
        if(m_sObjectByMockSelector.containsKey(sObjectType))
            return m_sObjectByMockSelector.get(sObjectType);

        // Determine Apex class for Selector class          
        Type selectorClass = m_sObjectBySelectorType.get(sObjectType);
        if(selectorClass==null)
            throw new DeveloperException('Selector class not found for SObjectType ' + sObjectType);

        // Construct Selector class and query by Id for the records
        return (fflib_ISObjectSelector) selectorClass.newInstance();            
    }

The last line is the relevant one here:

return (fflib_ISObjectSelector) selectorClass.newInstance();

which returns an object of your Selector class (e.g. AccountsSelector.cls). Selector classes inherit from fflib_SObjectSelector and the no arg constructor of this super class defaults these fields that control CRUD and FLS:

m_includeFieldSetFields = false
m_enforceCRUD = true
m_enforceFLS = false
m_sortSelectFelds = true

Now, you want to preserve the use of the factory because it allows you to set up mock Selectors and have them be dependency injected in unit tests.

But fflib_Application has no provision for invoking the other constructors in fflib_Selector that allow for dynamic setting of enforceCRUD and/or enforceFLS

That is, you can't do this:

Boolean enforceCRUD = false;
AccountsSelector.newInstance(enforceCRUD).selectByXXX(args);

so even if you added another factory invocation method to AccountsSelector that looked like this:

public static IAccountsSelector newInstance(Boolean enforceCRUD)    {
    return (IAccountsSelector) Application.Selector.newInstance(Account.SObjectType,enforceCRUD);
}

this wouldn't work because fflib_Application.Selector.newInstance() only supports a single argument sObjectType.

So, what to do?

Option 1: Some selectors should probably never enforce CRUD or FLS; particularly selectors on 'system-y' objects. For example, in my org, I have an Async_Request__c Sobject.

When fflib_Application.Selector executes its last line:

return (fflib_ISObjectSelector) selectorClass.newInstance();

The no arg constructor in AsyncRequestsSelector looks like this:

static final Boolean INCLUDE_FIELDSETS  = false;    // false is default, if true, class must include getSobjectFieldSetList
static final Boolean ENFORCE_CRUD       = false;    // true is default; 
static final Boolean ENFORCE_FLS        = false;    // this is default
static final Boolean SORT_SELECT_FIELDS = true;     // this is default

public AsyncRequestsSelector() {
    super(INCLUDE_FIELDSETS,ENFORCE_CRUD,ENFORCE_FLS,SORT_SELECT_FIELDS);
}

Thus, every call to AsyncRequestsSelector.newInstance().selectByXXX(args), regardless of how many methods in AsyncRequestsSelector will not enforce CRUD

Option 2: Individualize each selector method with fine-grained control over whether you want to enforceCRUD or FLS

Consider a method in AccountsSelector called selectByWebsite where you want FLS enforced

public Account[] selectByWebsite(Set<String> websites) {
  Boolean enforceCRUD = true;
  Boolean enforceFLS = true;
  Boolean includeSelectorFields = true; 
  fflib_QueryFactory acctQF = newQueryFactory(enforceCRUD,enforceFLS,includeSelectorFields)
    .setCondition('Website IN : websites');
  return Database.query(acctQF.toSOQL());
}

So, here, you are overriding the super class defaults for enforcing CRUD and FLS by constructing a queryFactory with selector-specific needs.

Option 2.1 - If you prefer the fluent style for queryFactories, you can accomplish the same in Option 2 with:

public Account[] selectByWebsite(Set<String> websites) {
  Boolean enforceCRUD = true;
  Boolean enforceFLS = true;
  Boolean includeSelectorFields = true; 
  fflib_QueryFactory acctQF = newQueryFactory(includeSelectorFields)
    .assertIsAccessible(enforceCRUD)
    .setEnforceFLS(enforceFLS)
    .setCondition('Website IN : websites');
  return Database.query(acctQF.toSOQL());
}   

Option 2.2 - If you need to have the selector method sometimes enforce CRUD or FLS, overload the method:

public Account[] selectByWebsite(Set<String> websites) {      
  return selectByWebsite(websites,super.isEnforcingCRUD,super.isEnforcingFLS);
}

public Account[] selectByWebSite(Set<String> websites, Boolean enforceCrud, Boolean enforceFLS) {
   Boolean includeSelectorFields = true;  
   fflib_QueryFactory acctQF = newQueryFactory(includeSelectorFields)
    .assertIsAccessible(enforceCRUD)
    .setEnforceFLS(enforceFLS)
    .setCondition('Website IN : websites');
  return Database.query(acctQF.toSOQL());
}

The fflib selector design pattern and its query factory has been an old-fashion and cumbersome design. Say if you wrote a function selectById with only a list of Id as the parameter, and has been put into use. But then some new features need to be added, e.g. orders, limits, or even more conditions. Now you have two options, write a completely new function in the selector, or refactor your existing function with more parameters. Either option looks sustainable. Moreover, fflib query factory only allows us to build the WHERE string manually.

An alternative of the fflib selector structure, is using the Query.apex, and get rid of the whole select layer. Starting with replacing the traditional selectById, you can do

List<Account> accounts = new Query('Account')
    .selectAllFields()
    .byId(yourIdList)
    .run();

If you want, you can reuse the Query instance to add more sorting, limits, condition, etc.

Query q = new Query('Account')
    .selectAllFields()
    .byId(yourIdList);

List<Account> accounts = q.run();

List<Account> anotherAccounts = q
    .addConditionLike('Name', 'Another%')
    .setLimit(10)
    .orderBy('CreatedDate', 'DESC')
    .run();

Enforce security check (throw exceptions when read permission is missing):

List<Account> accounts = new Query('Account').enforceSecurity().run();

You can even do aggregate functions with Query.apex. You may go to the repository to find out more.

Tags:

Apex

Isv

Fflib