Releases: beyond-the-cloud-dev/soql-lib
v4.2.0
03-February-2025
Scope
SOQL
Queryable toLabel(SObjectField field, String alias);Queryable toLabel(String field, String alias);Queryable sort(String direction);Map<String, SObject> toMap(String relationshipName, SObjectField targetKeyField);Map<String, List<SObject>> toAggregatedMap(String relationshipName, SObjectField targetKeyField);- Issued queries counter
- Code refactoring
SOQL.SubQuery
SubQuery with(String fields);SubQuery orderBy(String field);SubQuery sort(String direction);
SOQL.FilterGroup
FilterGroup add(List<Filter> filters)FilterGroup add(List<String> dynamicConditions)
SOQLCache
removeFromCache(List<SObject> records)
SOQL
toLabel(SObjectField field, String alias)
Enhancement: #147
You can specify the toLabel(field) alias to avoid the error: “duplicate field selected: field”.
Signature
Queryable toLabel(SObjectField field, String alias);
Queryable toLabel(String field, String alias);Example
SELECT Status, toLabel(Status) leadStatus FROM LeadSOQL.of(Lead.SObjectType)
.with(Lead.Status)
.toLabel(Lead.Status, 'leadStatus')
.toList();SELECT Company, toLabel(Recordtype.Name) recordTypeName FROM LeadSOQL.of(Lead.SObjectType)
.with(Lead.Company)
.toLabel('Recordtype.Name', 'recordTypeName')
.toList();Map<String, SObject> toMap(String relationshipName, SObjectField targetKeyField)
Enhancement: #144
Signature
Map<String, SObject> toMap(String relationshipName, SObjectField targetKeyField);Example
❌
Map<String, Account> parentCreatedByEmailToAccount = new Map<String, Account>();
for (Account account : [SELECT Id, Parent.CreatedBy.Email FROM Account]) {
parentCreatedByEmailToAccount.put(account.Parent.CreatedBy.Email, account);
}✅
Map<String, Account> parentCreatedByEmailToAccount =
(Map<String, Account>) SOQL.of(Account.SObjectType).toMap('Parent.CreatedBy', User.Email);Map<String, List<SObject>> toAggregatedMap(String relationshipName, SObjectField targetKeyField);
Enhancement: #144
Signature
Map<String, List<SObject>> toAggregatedMap(String relationshipName, SObjectField targetKeyField);Example
❌
Map<String, List<Account>> parentCreatedByEmailToAccount = new Map<String, List<Account>>();
for (Account account : [SELECT Id, Parent.CreatedBy.Email FROM Account]) {
if (!parentCreatedByEmailToAccount.containsKey(account.Parent.CreatedBy.Email)) {
parentCreatedByEmailToAccount.put(account.Parent.CreatedBy.Email, new List<Account>());
}
parentCreatedByEmailToAccount.get(account.Parent.CreatedBy.Email).add(account);
}✅
Map<String, List<Account>> parentCreatedByEmailToAccounts =
(Map<String, List<Account>>) SOQL.of(Account.SObjectType).toAggregatedMap('Parent.CreatedBy', User.Email);Issued queries counter
Mocked queries are now counted by SOQL Lib, and if the number of issued queries exceeds 100, a new QueryException('Too many SOQL queries.') will be thrown. This allows you to catch the issue in your unit tests.
SOQL.SubQuery
Thanks, @salberski for the collaboration!
SubQuery with(String fields);
Signature
SubQuery with(String fields)Example
SELECT Id, (
SELECT Id, Name, Phone, RecordTypeId, Title, Salutation
FROM Contacts
) FROM AccountSOQL.of(Account.SObjectType)
.with(SOQL.SubQuery.of('Contacts')
.with('Id, Name, Phone, RecordTypeId, Title, Salutation')
)
.toList();SubQuery orderBy(String field);
Signature
SubQuery orderBy(String field)Example
SELECT Id, (
SELECT Id
FROM Contacts
ORDER BY Name
) FROM AccountSOQL.of(Account.SObjectType)
.with(SOQL.SubQuery.of('Contacts')
.orderBy('Name')
)
.toList();SubQuery sort(String direction);
Signature
SubQuery sort(String direction)Example
SELECT Id, (
SELECT Id
FROM Contacts
ORDER BY Name DESC
) FROM AccountSOQL.of(Account.SObjectType)
.with(SOQL.SubQuery.of('Contacts')
.orderBy('Name')
.sort('DESC')
)
.toList();SOQL.FilterGroup
Thanks, @salberski for the collaboration!
Signature
FilterGroup add(List<Filter> filters);
FilterGroup add(List<String> dynamicConditions);Example
// SELECT Id FROM Account WHERE (Name = 'Test' AND BillingCity = 'Krakow')
SOQL.of(Account.SObjectType)
.whereAre(SOQL.FilterGroup
.add(new List<SOQL.Filter> {
SOQL.Filter.with(Account.Name).equal('Test'),
SOQL.Filter.with(Account.BillingCity).equal('Krakow')
})
).toList();// SELECT Id FROM Account WHERE (Name = 'Test' AND BillingCity = 'Krakow')
SOQL.of(Account.SObjectType)
.whereAre(SOQL.FilterGroup
.add(new List<String> {
'Name = \'Test\'',
'BillingCity = \'Krakow\''
})
).toList();SOQLCache
removeFromCache(List<SObject> records)
The removeFromCache method clears records from the cache, triggering an automatic refresh the next time the query is executed.
Thanks, @patrykacc for the collaboration!
Signature
Cacheable removeFromCache(List<SObject> records);Example
trigger SomeObjectTrigger on SomeObject (after update, after delete) {
SOQLCache.removeFromCache(Trigger.new);
}New Contributors
- @patrykacc in #143
- @salberski in #153
v4.0.0
07-January-2025
SOQL Caching
API: https://soql.beyondthecloud.dev/api/cached-soql/soql-cache
Examples: https://soql.beyondthecloud.dev/examples/cached-soql/select
Cached Selector: https://soql.beyondthecloud.dev/docs/build-cached-selector
SOQL caching is more complex than it seems. From our perspective, it is much more important to make the code predictable, bug-proof, and intuitive rather than adding numerous features that could confuse developers.
To achieve our goals, we made the following assumptions:
Limited Interface
The Cached SOQL Selector should have a limited interface that represents only the operations that can be performed on cached records. Methods that exist in the SOQL Library are not relevant when retrieving cached records. For example, it is impossible to check sharing rules for cached records, so methods like withSharing() or withoutSharing() are not applicable.
For instance, the following query:
SOQL.of(Profile.SObjectType)
.with(Profile.Id, Profile.Name, Profile.UserType)
.myTerritoryScope()
.forView()
.withSharing()
.toList();doesn’t make sense when working with cached records. We cannot apply scope, forView, or withSharing to cached records.
From the very beginning, we envisioned the cache SOQL as a standalone module, not logic tightly coupled with the SOQL Library. Each company could potentially have its own caching system. Therefore, we decided to introduce a new module called SOQLCache.cls.
public interface Cacheable {
// CONFIG
Cacheable cacheInApexTransaction();
Cacheable cacheInOrgCache();
Cacheable cacheInSessionCache();
// SELECT
Cacheable with(SObjectField field);
Cacheable with(SObjectField field1, SObjectField field2);
Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3);
Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4);
Cacheable with(SObjectField field1, SObjectField field2, SObjectField field3, SObjectField field4, SObjectField field5);
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);
}Cached Selectors
To avoid interfering with existing selectors and to keep the logic clean and simple, the best approach we identified is using Cached Selectors.
This approach provides immediate feedback to developers reading the code, indicating that the following query is cached. For instance:
SOQL_Profile.query().byName('System Administrator').toObject(); retrieves records from the database, whereas
SOQL_ProfileCache.query().byName('System Administrator').toObject(); retrieves records from the cache.
This clear distinction gives architects full control over which records should be cached and which should not, addressing potential issues that might arise with a .saveInCache() method.
Additionally, Cached Selectors look slightly different and include cache-specific methods:
public with sharing class SOQL_ProfileCache extends SOQLCache implements SOQLCache.Selector {
public static SOQL_ProfileCache query() {
return new SOQL_ProfileCache();
}
private SOQL_ProfileCache() {
super(Profile.SObjectType);
cacheInOrgCache();
with(Profile.Id, Profile.Name, Profile.UserType);
}
public override SOQL.Queryable initialQuery() {
return SOQL.of(Profile.SObjectType).systemMode().withoutSharing();
}
public SOQL_ProfileCache byName(String profileName) {
whereEqual(Profile.Name, profileName);
return thisl
}
}Developers can specify the type of storage using the cacheIn...() method. The available storage types are: Apex Transaction (cacheInApexTransaction()), Org Cache (cacheInOrgCache()), or Session Cache (cacheInSessionCache()).
Additionally, the with(...) method provides information about which fields are cached in the specified storage.
Last but not least, there is the initialQuery() method, which allows records to be prepopulated in the storage. For example, SOQL_ProfileCache will prepopulate all Profiles in the storage, so calling byName(String profileName) will not execute a query but will retrieve the records directly from the cache.
Cache Manager
Inspired by the Iteratively Building a Flexible Caching System for Apex post, we decided to build our own Cache Manager. This can be used not only for the SOQL Lib but also for caching other crucial items in your Salesforce org.
CacheManager.cls is required for using cached selectors and it's part of this repository. Already have your own cache manager?
No worries! You can use it by simply replacing SOQLCache.CacheStorageProxy with your own cache manager.
Records are stored as a List
Records are stored in the cache as a List, with the cache key being the SObjectType. This approach helps avoid record duplication, which is crucial given the limited storage capacity.
Key: Profile
Records:
| Id | Name | UserType |
|---|---|---|
| 00e3V000000DhteQAC | Standard Guest | Guest |
| 00e3V000000DhtfQAC | Community Profile | Guest |
| 00e3V000000NmefQAC | Standard User | Standard |
| 00e3V000000Nme3QAC | System Administrator | Standard |
| 00e3V000000NmeAQAS | Standard Platform User | Standard |
| 00e3V000000NmeHQAS | Customer Community Plus User | PowerCustomerSuccess |
| 00e3V000000NmeNQAS | Customer Community Plus Login User | PowerCustomerSuccess |
Why not just cache by SOQL String?
SELECT Id, Name FROM Profile => List of Profiles.
There are several reasons why caching by SOQL string is not the best approach:
- Identical Queries Treated Differently
Even the same SOQL query can be treated as different due to variations in the string format.
For instance, SELECT Id, Name FROM Profile and SELECT Name, Id FROM Profile are identical in intent but differ in query string format. This issue becomes even more complex when dealing with WHERE conditions.
- Duplicate Entries and Inefficient Record Retrieval
A simple query like SELECT Id, Name FROM Profile is a subset of a more complex query like SELECT Id, Name, UserType FROM Profile.
If the results of SELECT Id, Name, UserType FROM Profile are already cached, the simpler query should ideally retrieve the relevant data from the cache. However, if the cache uses the query string as the key, this won’t work, leading to duplication and inefficiency.
- Cache Storage Limitations
Cache storage is limited. If different variations of queries continually add new entries to the cache, it can quickly fill up with redundant or overlapping records, reducing overall effectiveness.
- Condition binding
The SOQL library uses variable binding (queryWithBinds). Using the query as a key becomes highly complex when there are many binding variables. Developers aim to use cached records to boost performance, but relying on the query as a key can be performance-intensive and counterproductive.
Cached query required single condition
A query requires a single condition, and that condition must filter by a unique field.
To ensure that cached records are aligned with the database, a single condition is required.
A query without a condition cannot guarantee that the number of records in the cache matches the database.
For example, let’s assume a developer makes the query: SELECT Id, Name FROM Profile. Cached records will be returned, but they may differ from the records in the database.
The filter field should be unique. Consistency issues can arise when the field is not unique. For instance, the query:
SELECT Id, Name FROM Profile WHERE UserType = 'Standard'
may return some records, but the number of records in the cache may differ from those in the database.
Using a unique field ensures that if a record is not found in the cache, the SOQL library can look it up in the database.
Example
Cached Records:
| Id | Name | UserType |
|---|---|---|
| 00e3V000000Nme3QAC | System Administrator | Standard |
| 00e3V000000NmeAQAS | Standard Platform User | Standard |
| 00e3V000000NmeHQAS | Customer Community Plus User | PowerCustomerSuccess |
Database Records:
| Id | Name | UserType |
|---|---|---|
| 00e3V000000Nme3QAC | System Administrator | Standard |
| 00e3V000000NmeAQAS | Standard Platform User | Standard |
| 00e3V000000NmeZQAS | Read Only | Standard |
| 00e3V000000NmeYQAS | Solution Manager | Standard |
| 00e3V000000NmeHQAS | Customer Community Plus User | PowerCustomerSuccess |
Let’s...
v3.4.0
04-January-2025
New
toId()- API 62.0 update
toId()
A new predefined method that simplifies your code.
Signature
Id toId();Example
new User (
// ...
ProfileId = SOQL_Profile.query().byName('System Administrator').toId()
);More details:
API 62.0 Update
Resolved the issue System.FinalException: Cannot modify a collection while it is being iterated.
The SOQL library now supports API version 62.0.
v3.3.0
06-October-2024
New
HAVINGsupport
HAVING support
HAVING is an optional clause that can be used in a SOQL query to filter results that aggregate functions return.
You can use a HAVING clause with a GROUP BY clause to filter the results returned by aggregate functions, such as SUM(). The HAVING clause is similar to a WHERE clause. The difference is that you can include aggregate functions in a HAVING clause, but not in a WHERE clause.
Signature
Queryable have(HavingFilterGroup filterGroup);
Queryable have(HavingFilter filter);
Queryable have(String dynamicCondition);
Queryable havingConditionLogic(String order);
Queryable anyHavingConditionMatching();Example
SOQL.of(Lead.SObjectType)
.with(Lead.LeadSource)
.count(Lead.Name)
.groupBy(Lead.City)
.groupBy(Lead.LeadSource)
.have(SOQL.HavingFilterGroup
.add(SOQL.HavingFilter.count(Lead.Name).greaterThan(100))
.add(SOQL.HavingFilter.with(Lead.City).startsWith('San'))
)
.toAggregated();SOQL.of(Lead.SObjectType)
.sum(Lead.AnnualRevenue)
.groupBy(Lead.LeadSource)
.have(SOQL.HavingFilter.sum(Lead.AnnualRevenue).greaterThan(1000000))
.toAggregated();More details:
release/v3.2.4
01-July-2024
New
userMode()- API Update 61.0
userMode
Execution mode in which the object permissions, field-level security, and sharing rules of the current user are enforced.
By default, all queries are executed WITH USER_MODE. However, developers can override this. For more details, check Field-Level Security and Sharing Rules.
The userMode() method can be useful to override the systemMode() provided by the selector as in the example below.
Signature
Queryable userMode()Example
public inherited sharing class SOQL_Account extends SOQL implements SOQL.Selector {
public static final String MOCK_ID = 'SOQL_Account';
public static SOQL_Account query() {
return new SOQL_Account();
}
private SOQL_Account() {
super(Account.SObjectType);
// default settings
with(Account.Id, Account.Name, Account.Type);
systemMode();
withoutSharing();
mockId(MOCK_ID);
}
}
SOQL_Account.query()
.userMode() // systemMode() from SOQL_Account.query will be overridden by userMode() method
.toList();v3.2.3
25-April-2024
New
groupBy(String relationshipName, SObjectField field)groupByRollup(String relationshipName, SObjectField field)groupByCube(String relationshipName, SObjectField field)
Refactoring
- Iterable for
SubQuery with(Iterable<SObjectField> fields) - Iterable for
Filter containsSome(Iterable<String> values) - Code refactoring
groupBy related
Thanks to @jessewheeler 's help, we have a groupBy related.
Signature
Queryable groupBy(String relationshipName, SObjectField field)Example
SELECT COUNT(Name) count
FROM OpportunityLineItem
GROUP BY OpportunityLineItem.Opportunity.Account.IdSOQL.of(OpportunityLineItem.SObjectType)
.count(OpportunityLineItem.Name, 'count')
.groupBy('OpportunityLineItem.Opportunity.Account', Account.Id)
.toAggregated();groupByRollup related
Signature
Queryable groupByRollup(String relationshipName, SObjectField field)Example
SELECT COUNT(Name) cnt
FROM Lead
GROUP BY ROLLUP(ConvertedOpportunity.StageName)SOQL.of(Lead.SObjectType)
.count(Lead.Name, 'cnt')
.groupByRollup('ConvertedOpportunity', Opportunity.StageName)
.toAggregated();groupByCube related
Signature
Queryable groupByCube(String relationshipName, SObjectField field)Example
SELECT COUNT(Name) cnt
FROM Lead
GROUP BY CUBE(ConvertedOpportunity.StageName)SOQL.of(Lead.SObjectType)
.count(Lead.Name, 'cnt')
.groupByCube('ConvertedOpportunity', Opportunity.StageName)
.toAggregated();Code refactoring
Applied the Iterable interface where possible for a small code refactoring to simplify the code.
v3.2.2
27-March-2024
New
withFieldSet(String fieldSetName)byRecordType(String recordTypeDeveloperName)
Refactoring
- API 60 Update
- Iterable for
with(String relationshipName, Iterable<SObjectField> fields) - Code refactoring
withFieldSet
Pass FieldSet name to get dynamic fields. It can be very useful to have a dynamic UI.
Signature
Queryable withFieldSet(String fieldSetName)Example
SELECT
Id,
Name,
Industry
FROM AccountSOQL.of(Account.SObjectType)
.withFieldSet('AccountFieldSet')
.toList();byRecordType
Query record by RecordType.DeveloperName. To do that, you can use the byRecordType method.
🚨 If you already have a method called byRecordType in your selector, you may encounter the following issues:
Non-virtual, non-abstract methods cannot be overridden: YourSelector YourSelector.byRecordType(String)
You have two options:
- You can remove the
byRecordTypefrom your selector and use the predefined method from the SOQL library. - You can skip that update and still use
byRecordTypefrom your selector.
Signature
Queryable byRecordType(String recordTypeDeveloperName)Example
SELECT Id
FROM Account
WHERE RecordType.DeveloperName = 'Partner'SOQL.of(Account.SObjectType)
.byRecordType('Partner')
.toList();API 60 Update
🚨Do not forget to update ALL selectors to API 60.
Selectors with API < 60 that are using the isIn method will encounter the following error:
Method does not exist or incorrect signature: void isIn(List<String>) from the type SOQL.Filter
Salesforce has made some changes, and this update is necessary!
Iterable for with(String relationshipName, Iterable<SObjectField> fields)
Now you can pass List<SObjectField> as well as Set<SObjectField> to with(String relationshipName, Iterable<SObjectField> fields).
Code refactoring
Unused lines of code were removed. Null Coalescing Operator was applied.
v3.2.1
16-February-2024
Update of SOQL_Test
We replaced the Standard User Profile with Minimum Access - Salesforce, ensuring that the stripInaccessibleToObject and stripInaccessibleToList tests will execute successfully regardless of the CRUD settings in the Salesforce org.
From:
static User standardUser() {
return new User(
Alias = 'newUser',
Email = '[email protected]',
EmailEncodingKey = 'UTF-8',
LastName = 'Testing',
LanguageLocaleKey = 'en_US',
LocaleSidKey = 'en_US',
ProfileId = [SELECT Id FROM Profile WHERE Name = 'Standard User'].Id,
TimeZoneSidKey = 'America/Los_Angeles',
UserName = '[email protected]'
);
}To:
static User minimumAccessUser() {
return new User(
Alias = 'newUser',
Email = '[email protected]',
EmailEncodingKey = 'UTF-8',
LastName = 'Testing',
LanguageLocaleKey = 'en_US',
LocaleSidKey = 'en_US',
ProfileId = [SELECT Id FROM Profile WHERE Name = 'Minimum Access - Salesforce'].Id,
TimeZoneSidKey = 'America/Los_Angeles',
UserName = '[email protected]'
);
}v3.2.0
19-December-2023
- Changes
- COUNT related
- AVG related
- COUNT_DISTINCT related
- MIN related
- MAX related
- SUM related
toLabelformat- Code refactoring
COUNT
Introducing the count method for related fields.
Queryable count(String relationshipName, SObjectField field);
Queryable count(String relationshipName, SObjectField field, String alias);// SELECT COUNT(Account.Name)
// FROM Contact
SOQL.of(Contact.SObjectType)
.count('Account', Account.Name)
.toAggregated();// SELECT COUNT(Account.Name) names
// FROM Contact
SOQL.of(Contact.SObjectType)
.count('Account', Account.Name, 'names')
.toAggregated();AVG
Introducing the avg method for related fields.
Queryable avg(String relationshipName, SObjectField field);
Queryable avg(String relationshipName, SObjectField field, String alias);// SELECT AVG(Opportunity.Amount)
// FROM OpportunityLineItem
SOQL.of(OpportunityLineItem.SObjectType)
.avg('Opportunity', Opportunity.Amount)
.toAggregate();// SELECT AVG(Opportunity.Amount) amount
// FROM OpportunityLineItem
SOQL.of(OpportunityLineItem.SObjectType)
.avg('Opportunity', Opportunity.Amount, 'amount')
.toAggregate();COUNT_DISTINCT
Introducing the countDistinct method for related fields.
Queryable countDistinct(String relationshipName, SObjectField field);
Queryable countDistinct(String relationshipName, SObjectField field, String alias);// SELECT COUNT_DISTINCT(Lead.Company)
// FROM CampaignMember
SOQL.of(CampaignMember.SObjectType)
.countDistinct('Lead', Lead.Company)
.toAggregate();// SELECT COUNT_DISTINCT(Lead.Company) company
// FROM CampaignMember
SOQL.of(CampaignMember.SObjectType)
.countDistinct('Lead', Lead.Company, 'company')
.toAggregate();MIN
Introducing the min method for related fields.
Queryable min(String relationshipName, SObjectField field);
Queryable min(String relationshipName, SObjectField field, String alias);// SELECT MIN(Account.CreatedDate)
// FROM Contact
SOQL.of(Contact.SObjectType)
.min('Account', Account.CreatedDate)
.toAggregate();// SELECT MIN(Account.CreatedDate) createdDate
// FROM Contact
SOQL.of(Contact.SObjectType)
.min('Account', Account.CreatedDate, 'createdDate')
.toAggregate();MAX
Introducing the max method for related fields.
Queryable max(String relationshipName, SObjectField field);
Queryable max(String relationshipName, SObjectField field, String alias);// SELECT MAX(Campaign.BudgetedCost)
// FROM CampaignMember
SOQL.of(CampaignMember.SObjectType)
.max('Campaign', Campaign.BudgetedCost)
.toAggregate();// SELECT MAX(Campaign.BudgetedCost) budgetedCost
// FROM CampaignMember
SOQL.of(CampaignMember.SObjectType)
.max('Campaign', Campaign.BudgetedCost, 'budgetedCost')
.toAggregate();SUM
Introducing the sum method for related fields.
Queryable sum(String relationshipName, SObjectField field);
Queryable sum(String relationshipName, SObjectField field, String alias);// SELECT SUM(Opportunity.Amount)
// FROM OpportunityLineItem
SOQL.of(OpportunityLineItem.SObjectType)
.sum('Opportunity', Opportunity.Amount)
.toAggregate();// SELECT SUM(Opportunity.Amount) amount
// FROM OpportunityLineItem
SOQL.of(OpportunityLineItem.SObjectType)
.sum('Opportunity', Opportunity.Amount, 'amount')
.toAggregate();toLabel
To translate SOQL query results into the language of the user who submits the query, use the toLabel method.
Queryable toLabel(SObjectField field);
Queryable toLabel(String field);// SELECT Company, toLabel(Status)
// FROM Lead
SOQL.of(Lead.SObjectType)
.with(Lead.Company)
.toLabel(Lead.Status)
.toList();// SELECT Company, toLabel(Recordtype.Name)
// FROM Lead
SOQL.of(Lead.SObjectType)
.with(Lead.Company)
.toLabel('Recordtype.Name')
.toList();format
Use FORMAT with the SELECT clause to apply localized formatting to standard and custom number, date, time, and currency fields.
Queryable format(SObjectField field);
Queryable format(SObjectField field, String alias);// SELECT FORMAT(Amount)
// FROM Opportunity
SOQL.of(Opportunity.SObjectType)
.format(Opportunity.Amount)
.toList();// SELECT FORMAT(Amount) amt
// FROM Opportunity
SOQL.of(Opportunity.SObjectType)
.format(Opportunity.Amount, 'amt')
.toList();v3.1.0
08-November-2023
- Changes
- AVG
- COUNT_DISTINCT
- MIN
- MAX
- SUM
ignoreWhenfor FilterGroup- Code refactoring
AVG
Introducing the avg method.
Queryable avg(SObjectField field);
Queryable avg(SObjectField field, String alias);// SELECT CampaignId, AVG(Amount) amount
// FROM Opportunity
// GROUP BY CampaignId
SOQL.of(Opportunity.SObjectType)
.with(Opportunity.CampaignId)
.avg(Opportunity.Amount)
.groupBy(Opportunity.CampaignId)
.toAggregated();// SELECT CampaignId, AVG(Amount) amount
// FROM Opportunity
// GROUP BY CampaignId
SOQL.of(Opportunity.SObjectType)
.with(Opportunity.CampaignId)
.avg(Opportunity.Amount, 'amount')
.groupBy(Opportunity.CampaignId)
.toAggregated();COUNT_DISTINCT
Introducing the countDistinct method.
Queryable countDistinct(SObjectField field);
Queryable countDistinct(SObjectField field, String alias);// SELECT COUNT_DISTINCT(Company) FROM Lead
SOQL.of(Lead.SObjectType)
.countDistinct(Lead.Company)
.toAggregated();// SELECT COUNT_DISTINCT(Company) company FROM Lead
SOQL.of(Lead.SObjectType)
.countDistinct(Lead.Company, 'company')
.toAggregated();MIN
Introducing the min method.
Queryable min(SObjectField field);
Queryable min(SObjectField field, String alias);// SELECT FirstName, LastName, MIN(CreatedDate)
// FROM Contact
// GROUP BY FirstName, LastName
SOQL.of(Contact.SObjectType)
.with(Contact.FirstName, Contact.LastName)
.min(Contact.CreatedDate)
.groupBy(Contact.FirstName)
.groupBy(Contact.LastName)
.toAggregated();// SELECT FirstName, LastName, MIN(CreatedDate) createdDate
// FROM Contact
// GROUP BY FirstName, LastName
SOQL.of(Contact.SObjectType)
.with(Contact.FirstName, Contact.LastName)
.min(Contact.CreatedDate, 'createdDate')
.groupBy(Contact.FirstName)
groupBy(Contact.LastName)
.toAggregated();MAX
Introducing the max method.
Queryable max(SObjectField field);
Queryable max(SObjectField field, String alias);// SELECT Name, MAX(BudgetedCost)
// FROM Campaign
// GROUP BY Name
SOQL.of(Campaign.SObjectType)
.with(Campaign.Name)
.max(Campaign.BudgetedCost)
.groupBy(Campaign.Name)
.toAggregated();// SELECT Name, MAX(BudgetedCost) budgetedCost
// FROM Campaign
// GROUP BY Name
SOQL.of(Campaign.SObjectType)
.with(Campaign.Name)
.max(Campaign.BudgetedCost, 'budgetedCost')
.groupBy(Campaign.Name)
.toAggregated();SUM
Introducing the sum method.
Queryable sum(SObjectField field);
Queryable sum(SObjectField field, String alias);// SELECT SUM(Amount) FROM Opportunity
SOQL.of(Opportunity.SObjectType)
.sum(Opportunity.Amount)
.toAggregated()// SELECT SUM(Amount) amount FROM Opportunity
SOQL.of(Opportunity.SObjectType)
.sum(Opportunity.Amount, 'amount')
.toAggregated()ignoreWhen for FilterGroup
The ignoreWhen method was previously available only for Filter, but now you can use it for FilterGroup.
Boolean isPartnerUser = false;
SOQL.of(Account.SObjectType)
.whereAre(SOQL.FilterGroup
.add(SOQL.FilterGroup
.add(SOQL.Filter.with(Account.BillingCity).equal('Krakow'))
.add(SOQL.Filter.with(Account.BillingCity).equal('Warsaw'))
.anyConditionMatching()
.ignoreWhen(!isPartnerUser) // <===
)
.add(SOQL.FilterGroup
.add(SOQL.Filter.with(Account.Industry).equal('IT'))
.add(SOQL.Filter.name().contains('MyAcccount'))
)
)
.toList();