Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to get a selector without sharing #62

Open
wimvelzeboer opened this issue Oct 21, 2021 · 4 comments
Open

How to get a selector without sharing #62

wimvelzeboer opened this issue Oct 21, 2021 · 4 comments

Comments

@wimvelzeboer
Copy link

wimvelzeboer commented Oct 21, 2021

What is the best way to have one selector implementation and be able to choose to have a with sharing and a without sharing selector, with the minimal amount of code duplication.

I currently have something like this, but how do I implement this in AT4DX:

public interface IContactsSelector extend fflib_ISObjectSelector
{
    List<Contact> selectById(Set<Id> ids);
 }
pubic virtual inherited sharing class ContactsSelector extends fflib_SObjectSelector implements IContactsSelector
{
   public static IContactsSelector newInstance()
   {
       return (IContactsSelector) Application.Selector.newInstance(Schema.Contact.SObjectType);
   }
   public static IContactsSelector newWithoutSharingInstance()
   {
       return (IContactsSelector) Application.WithoutSharingSelector.newInstance(Schema.Contact.SObjectType);
   }
   public static IContactsSelector newWithSharingInstance()
   {
       return (IContactsSelector) Application.WithSharingSelector.newInstance(Schema.Contact.SObjectType);
   }
   public ContactsSelector()
   {
       super();
   }
   public ContactsSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS)
   {
       super(includeFieldSetFields, enforceCRUD, enforceFLS);
   }
   public List<Contact> selectById(Set<Id> ids);
   {
      ...
   }
   
   public with sharing class WithSharing extends ContactsSelector
   {
       public WithSharing()
       {
           super(true, true, true);
       }
       public override List<Contact> selectById(Set<Id> idSet)
       {
           return super.selectById(idSet);
       }
   }
   public with sharing class WithoutSharing extends ContactsSelector
   {
       public WithSharing()
       {
           super(true, false, false);
       }
       public override List<Contact> selectById(Set<Id> idSet)
       {
           return super.selectById(idSet);
       }
   }
}
@wimvelzeboer wimvelzeboer changed the title How to get an selector without sharing How to get a selector without sharing Oct 21, 2021
@stohn777
Copy link
Collaborator

Hi @wimvelzeboer

When might a Selector be the top-level Apex code that would thus define the sharing model? Would the calling code -- having "with" or "without sharing" or implied in some way -- define the sharing model, which an "inherited sharing" Selector would implicitly enforce?

@chazwatkins
Copy link
Contributor

Typically, selectors should default to with sharing and have an inner class that allows elevation to without sharing. This forces devs to call the elevated query explicitly instead of depending on the caller class sharing. I prefer to start with the lowest level of access and only elevate if necessary.

I don't think Application.SelectorFactory should be updated to include With, Without, or Inherited Sharing variants. These can be handled by exposing additional methods like the one below.

If you are set on using different instances of the selector for each variant, I suggest extending Application and implementing them yourself.

public class with sharing ContactSelector
    extends ApplicationSObjectSelector
    implements IContactSelector
{

  public Contact[] selectById(Set<Id> ids) {
      return (Contact[])selectSObjectById(ids);
  }


  public Contact[] selectByIdWithoutSharing(Set<Id> ids) {
      return new WithoutSharing().selectById(this, ids);
  }

  public class without sharing WithoutSharing {

      private WithoutSharing() {}

      public Contact[] selectById(IContactSelector selector, Set<Id> ids) {
          return selector.selectById(ids);
      }
  }

}

@stohn777
Copy link
Collaborator

stohn777 commented Dec 6, 2022

Hi @chazwatkins

Thank you for clarifying.

This code has been thoroughly vetted with my favorite text editor, so take it for what it's worth. 😇 However I think, based on my understanding, this is the first step that I'd take down the path that's been charted.

Keeping Application and the core ContactSelector pure for the most part, I've kept the two selector separated -- S in SOLID -- but one could put a static factory method in ContactSelector if they felt strongly about it. With this, the consuming business logic would be free of extraneous code, as typical, and the unit test classes would still simply provide a mock selector to the Application, as typical when needed.

Please give this a whirl in a project, where you probably have some robust testing setup. I'm genuinely interested if this is going in the right direction and not broken in some way.

Regards!

public interface IContactSelector {
	Contact[] selectById(Set<Id> ids);
}

public virtual with sharing ContactSelector 
	extends ApplicationSObjectSelector
	implements IContactSelector
{
	public static IContactSelector newInstance() {
		return (IContactSelector) Application.Selector.newInstance(Contact.SObjectType);
	}
	
	public virtual Contact[] selectById(Set<Id> ids) {
		return (Contact[]) Database.query(...);
	}
}

public without sharing ContactSelectorWithoutSharing
	extends ContactSelector
{
	public static IContactSelector newInstance() {
		return new ContactSelectorWithoutSharing();
	}
	
	private IContactSelector baseSelector = Application.Selector.newInstance(Contact.SObjectType);
	
	public override Contact[] selectById(Set<Id> ids) {
		return baseSelector.selectById(ids);
	}
}

@chazwatkins
Copy link
Contributor

chazwatkins commented Dec 6, 2022

Hi @stohn777 ,

Thanks for the response. I agree that the inner-class approach is not the cleanest solution. I believe most folks, including myself, discover this approach in the blog post Apex Sharing and applying to Apex Enterprise Patterns.

The only problem with the approach you outlined is that you're forced to call ContactSelector.newInstance() or ContactSelectorWithoutSharing.newInstance(). The calling class must know about the selector implementation class instead of using the application selector factory to return the implementation. Which defeats the purpose of having the selector factory.

I believe this is why @wimvelzeboer went the way of extending the selector factory to include sharing variants.

Another approach could be to allow the caller to specify the sharing rule within which the query would be executed.

IContactSelector selector = (IContactSelector)Application.Selector.newInstance(Contact.SObjectType);
// default sharing rule is inherited sharing

selector.getSharingRule();
// INHERITED

// Executed inherited sharing
selector.selectInjection(...);
selector.selectById(...);

selector.withSharing();  // Sets the sharing rule to with sharing

selector.getSharingRule();
// WITH

// Executed with sharing
selector.selectInjection(...);
selector.selectById(...);

selector.withoutSharing();  // Sets the sharing rule to without sharing

selector.getSharingRule();
// WITHOUT

// Executed without sharing
selector.selectInjection(...);
selector.selectById(...);

The selector query methods would no longer directly call Database.query(...) or Database.getQueryLocator(...). Rather, they would pass in a query factory or SOQL string to something like:

  • ApplicationSObjectSelector.query(fflib_QueryFactory qf)
  • ApplicationSObjectSelector.query(String query)
  • ApplicationSObjectSelector.getQueryLocator(fflib_QueryFactory qf)
  • ApplicationSObjectSelector.getQueryLocator(String query)

These methods would look at the current sharing rule and call the queries within the corresponding class:

  • ApplicationSObjectSelector.WithSharing
  • ApplicationSObjectSelector.WithoutSharing
  • ApplicationSObjectSelector.InheritedSharing

Doing so will allow the same query method to be reused for all the sharing rules, so duplication of method definitions is no longer necessary.

With the upcoming changes in fflib_QueryFactory to add USER_MODE support, apex-enterprise-patterns/fflib-apex-common#420, apex-enterprise-patterns/fflib-apex-common#435, fflib_SObjectSelector might be a better home for setting the selector sharing rule. Then again, maybe being able to use USER_MODE eliminates the need to worry about the calling classes sharing rule keyword.

I'm happy to implement setting the sharing rule on the selector if the team is interested in adding this.

What do you think?

BTW - You left me in suspense. What is your favorite text editor?

This code has been thoroughly vetted with my favorite text editor, so take it for what it's worth. 😇 ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants