Giter Club home page Giter Club logo

query.apex's Introduction

Query.apex

Build Status

Query.apex provides a flexible and dynamic way of building a SOQL/SOSL query on the Salesforce platform.

Our Page    Documentation    Tutorials

Why Query.apex

Although Salesforce provides Database.query method to dynamically execute a query from a string, it is far from easy to construct such a string in a bug-free, structural and flexible way. Query.apex is made to improve the flexibility of the code and consequently enhance the productivity of the development.

Features

  • Allows object oriented programming paradigm and function chaining

  • Supports complex queries including parent/child relationships, and nested conditions in a flexible way

  • Prevents SOQL injections, without manually escaping the string variables

  • Supports aggregate functions including group by methods

  • Manages the namespace of the object names and field names, while also provides the Object/Field Level Security checking

  • SOSL features are in active development. Please follow https://github.com/PropicSignifi/Query.apex/projects/1 for the progress

Examples

Get all accounts

This will return a list of all Accounts from the database.

By default it will select only the Id field.

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

Select all fields

This will query all Accounts from the database, selecting all fields.

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

Select all user readable fields

This will query all Accounts from the database, selecting all fields which the user has read access on.

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

Select specific fields

This will query all Accounts from the database, selecting specified fields only.

List<Account> accounts =
    new Query('Account').
    selectField('Name').
    selectFields('CreatedById').
    selectFields('CreatedDate, LastModifiedDate').
    selectFields(new List<String>{'LastActivityDate', 'LastViewedDate'}).
    run();

Get an account based on its Id

This will query the Accounts with a specific Id, and return only one SObject as a result.

Account account =
    (Account)new Query('Account').
    byId('001O000001HMWZVIA5').
    fetch();

Get a list of contacts based on a foreign key

This will query the Contacts given the foreign key AccountId.

List<Contact> contacts =
    new Query('Contact').
    lookup('AccountId', '001O000001HMWZVIA5').
    run();

Get a list of Id of the query result

This will query all the Accounts and return a list of Id as a result.

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

Get the executable query string

If you want to get the raw query string intead of executing the query, you can use the toQueryString method.

String queryString = new Query('Account').
    selectFields('Name').
    addConditionLike('Name', '%Sam%').
    toQueryString();

Database.query(queryString);

Debug

You can use the debug method to print the current query string to the log.

new Query('Account').
    selectFields('Name').
    debug().
    addConditionLike('Name', '%Sam%').
    debug()
    run();

Enforce security check

By default, when read permission of an object/field is missing, we will print a warning to the log. But we can change it to an exception if that's needed.

new Query('Account').
    enforceSecurity().
    selectFields('Name').
    run();

You may also call the static method enforceGlobalSecurity to enforce exception on all Query instances.

Query.enforceGlobalSecurity();

new Query('Account').
    selectFields('Name').
    run();

Select parent fields

This will select all the fields from the parent object Account.

List<Contact> contacts =
    new Query('Contact').
    selectAllFields('Account').
    run();

This will select all user readable fields from the parent object Account.

List<Contact> contacts =
    new Query('Contact').
    selectReadableFields('Account').
    run();

Query with simple conditions

This will query all the accounts whose 'FirstName' is 'Sam' and 'LastName' is 'Tarly'.

By default, all the conditions are joined by the 'AND' operator.

List<Account> accounts =
    new Query('Account').
    addConditionEq('Name', 'Sam').
    addConditionLt('NumberOfEmployees', 10).
    run();

Query with complex conditions

For more complicated conditions, we can use the method 'conditionXX' to create a condition variable, before using the 'doOr' method or 'doAnd' boolean operation methods to join these conditions.

List<Account> accounts =
    new Query('Account').
    addCondition(
        Query.doAnd(
            Query.doOr(
                Query.conditionEq('FirstName', 'Sam'),
                Query.conditionEq('Id', '001O000001HMWZVIA5')
            ),
            Query.conditionLe('NumberOfEmployees', 15)
        )
    ).
    run();

Query with date literal conditions

We can also use date literals in conditions.

List<Account> accounts =
    new Query('Account').
    addConditionLe('LastModifiedDate', Query.TODAY).
    addConditionEq('CreatedDate', Query.LAST_N_WEEKS(3)).
    run();

Query with conditions with INCLUDES/EXCLUDES operator

INCLUDES and EXCLUDES operator can be used on multi-picklist fields.

The following example is querying QuickText with the Channel field that includes both the two values at the same time:

List<QuickText> result = new Query('QuickText').
    addConditionIncludes('Channel', 'MyChannel;OtherChannel').
    run();

In contrast, this example is a condition that includes any of the two values:

List<QuickText> result = new Query('QuickText').
    addConditionIncludes('Channel', new List<String>{'MyChannel', 'OtherChannel'}).
    run();

Query with subqueries

Query.apex also allows selecting child relationships (subqueries), in a method chain style similar to the conditions.

List<Account> accounts =
    new Query('Account').
    addSubquery(
        Query.subquery('Contacts').
        addConditionEq('FirstName', 'Sam').
        addConditionIn('LastName', new List<String>{'Tarly'})
    ).
    run();

Query with semi-join

It is also possible to have a subquery in the condition, known as the semi-join.

List<Account> accounts =
    new Query('Account').
    lookup('Id', new Query('Opportunity').
            selectField('AccountId')).
    run();

Simple aggregate functions

Aggregate functions including 'count', 'countDistinct', 'max', 'min', 'avg', 'sum' are supported. Optional alias can be provided as the second parameter. To get the aggregate result, user needs to call the method 'aggregate()'.

The 'aggregate' method returns a list of 'AggregateResult' items.

AggregateResult result =
    new Query('Account').
    count('Name', 'countName').
    countDistinct('Rating', 'countRating').
    max('NumberOfEmployees', 'maxEmployee').
    min('NumberOfEmployees', 'minEmployee').
    avg('NumberOfEmployees', 'avgEmployee').
    sum('NumberOfEmployees', 'sumEmployee').
    aggregate()[0];

Integer countName = (Integer)result.get('countName');
Integer countRating = (Integer)result.get('countRating');
Integer maxEmployee = (Integer)result.get('maxEmployee');
Integer minEmployee = (Integer)result.get('minEmployee');
Decimal avgEmployee = (Decimal)result.get('avgEmployee');
Integer sumEmployee = (Integer)result.get('sumEmployee');

In this example, 'countName', 'maxEmployee', and so forth are the alias for the aggregate functions. Since there is no group by clauses used, the returned list has one and only one item. You can get the value of an aggregated field using the 'get' method in the first 'AggregateResult' item.

Aggregate functions combined with GROUP BY clause

Aggregate functions are more useful combined with the 'groupBy' method, so that each group can have its own aggregate result. Similar to the simple aggregate functions, the 'aggregate' method is needed to get the aggregate results, which will return a list of 'AggregateResult' items.

We can also select fields that appear in the group by list. Similar to the methods 'count', 'max', etc, optional alias as the second argument is allowed in the 'selectField' method.

List<AggregateResult> results =
    new Query('Account').
    selectField('Rating', 'rate').
    count('Name', 'countName').
    max('NumberOfEmployees', 'maxEmployees').
    min('NumberOfEmployees', 'minEmployees').
    avg('NumberOfEmployees', 'avgEmployees').
    sum('NumberOfEmployees', 'sumEmployees').
    groupBy('Rating').
    aggregate();

for (AggregateResult result : results) {
    System.debug('Rating: ' + result.get('rate'));
    System.debug('maxEmployees: ' + result.get('maxEmployees'));
    System.debug('minEmployees: ' + result.get('minEmployees'));
    System.debug('avgEmployees: ' + result.get('avgEmployees'));
    System.debug('sumEmployees: ' + result.get('sumEmployees'));
}

Note that we can only select fields that appear in the group by method. In this example, only the 'Rating' field is in the group by clause, so only the 'Rating' field can be selected.

Aggregate functions with HAVING clauses

With HAVING clauses, aggregate functions can be even more powerful. See this example:

Suppose we have a parent object Account, and a child object Opportunity, I want to query all the Accounts with at least one Opportunity. If we use child relationship query (subquery), we might still get all the Accounts, with some of them having the Opportunity child as an empty list. And then we need to do the filter manually, removing the Accounts with empty Opportunity list. Apparently, such way costs unnecessary memory.

But we can actually do it in one query, using GROUP BY and HAVING clauses.

List<AggregateResult> results =
    new Query('Opportunity').
    selectField('AccountId').
    count('Name', 'countName').
    groupBy('AccountId').
    addHaving(Query.conditionGe('Count(Name)', 1)).
    aggregate();

// Loop the aggregate result to get the account ids
List<Id> accountIds = new List<Id>();
for (AggregateResult result : results) {
    accountIds.add((Id)result.get('AccountId'));
}

query.apex's People

Contributors

aranwe avatar coderdecoder01 avatar dependabot[bot] avatar dougliu1111 avatar eliver avatar henryrlee avatar herakles86 avatar kratoon avatar mritzi avatar sksonalkothari avatar stephanspiegel avatar triandicant avatar xiaoyangyan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

query.apex's Issues

common.apex.runtime.impl.ExecutionException

I Am using one query like this
List<Case> oList= new DMLOperationsHandler('Case'). selectFields('Id,CaseNumber,ReceivedDate__c,CreatedDate,Owner.FirstName,Owner.Lastname,Origin'). run();
and getting error as

common.apex.runtime.impl.ExecutionException: Cannot find firstname in object Group

Can you please correct me if I do anything wrong?

Add `toMap` method for result

Instead of returning an SObject as a result, Query should also support returning a map, which automatically resolves namespace.

Allow Query as the second parameter in the `lookup` method

The goal is to support queries such as

SELECT Id FROM Account WHERE OwnerId IN (SELECT Id FROM User WHERE CreatedDate = TODAY)

Although at this stage, we can implement the same with

new Query('Account').
  lookup('OwnerId', new Query('User').addConditionEq('CreatedDate', Query.TODAY).run()).
  run();

But this actually executes two Database.query actions.

Instead, we hope we can do

new Query('Account').
  lookup('OwnerId', new Query('User').addConditionEq('CreatedDate', Query.TODAY)).
  run();

Add the anti-join feature

Since 983d193, we've had the semi-join feature supported. It would be worthwhile adding the anti-join feature as well.

The implementation should be easy: just follow the commit 983d193 and add the similar code to the methods addConditionNotIn and conditionNotIn.

Support DISTANCE function

https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_geolocate.htm

I haven't come up with a design yet.

Anyway, the feature is still available by manually feeding the whole DISTANCE expression to the first parameter of the methods addConditionXX or orderBy.

For example:

new Query('Account')
.addConditionGt('DISTANCE(Location__c, GEOLOCATION(37.775,-122.418), \'mi\')', 10);
new Query('Account')
.orderBy('DISTANCE(Location__c, GEOLOCATION(37.775,-122.418), \'mi\')', 'DESC');

It's just this way of coding does not fit the style of Query.apex.

Add more condition methods to SOSL QuerySearch

An example of how it is done can be referred to #49.

Basically, the returning clause can be further constrained by more condition methods. These include:

addConditionIn
addConditionNotIn
addConditionLt
addConditionLe
addConditionGt
addConditionGe
addConditionIncludes
addConditionExcludes

Example usage:

new QuerySearch().find('ABC').returning('Account').addConditionLt('CreatedDate', Date.today())

Support FORMAT() for Currency fields

Currently if you try to add Query.selectField('FORMAT(Amount) Amount') the Query formating fails on:
checkFieldLevelSecurity(Schema.SObjectType objType, String fieldPath) because it tries to get the whole String from fieldMap.

FunctionFieldTuples are supported only in aggregate queries.

Either there should be regexp that would support FORMAT() in checkFieldLevelSecurity or there should be some more advanced mechanism to allow FunctionFieldTuples in non-aggregation queries ... though SOQL supports only FORMAT() as function call in non-aggregation queries?

What do you think?

Cheers,
O.

Test code fails when using scratch org with Enterprise edition

The root cause is that the default System Administrator profile under an Enterprise edition scratch org have missed some field level security on the account and opportunity object. These fields include:

Account.Sic
Account.Site
Account.AccountNumber
Account.Rating
Opportunity.TotalOpportunityQuantity

To fix it, we should avoid using these fields in the test code.

Temporary workaround: avoid using the Enterprise edition scratch org.

Directly set Condition.conditionString

Hi,

First - Query.apex is awesome, however I have now pretty specific usecase:

I need to set the conditionString of Condition directly - i.e. I have configuration object where there is manually entered part of the WHERE Clause (that is then appended with doAnd() call).

I have two possible solutions, I can send PR - just would like to know your throughts

  1. just make the Condition constructor public with 1 String param - i.e.:
public Condition(String conditionString) {
    this();
    this.conditionString = conditionString;
}
  1. Add methods addConditionString and conditionString to the Query class - i.e.:
public Query addConditionString(String conditionString){
    return addCondition(conditionString(conditionString));
}
public static Condition conditionString(String conditionString){
   return new Condition(conditionString);
}

And in the Condition class

private Condition(String conditionString){
    this();
    this.conditionString = conditionString;
}

What do you think?

Calling .debug() breaks countQuery()

Hi

Thanks for great work!

So here are steps to repro:
new Query('Account').debug().countQuery();

throws:
Error on line 1587, column 1: System.QueryException: unexpected token: ')' Class.Query.countQuery: line 1587, column 1

After removing the debug() call it works fine:
new Query('Account').countQuery();

This is because debug() calls toQueryString() -> formQueryStringPreformat() -> formFieldString(), which does functionFieldList.isEmpty() too early and adds Id to field set.

Add a `lookup` method for condition

To support parameters:
One Id as a foreign key,
A collection of Ids as a collection of foreign keys,
One SObject, using its Id as a foreign key,
A collection of SObject, using their Ids as a collection of foreign keys.

The order by clause is incorrect when there are multiple fields with different directions

To be more specific:

new Query('Account').orderBy('Name', 'DESC').orderBy('Phone', 'ASC').run()

This will create a query with two order by clauses, but both of them are in ascending order, which is incorrect.

The current result of the query will be like:

SELECT Id FROM Account ORDER BY Name, Phone ASC

The expected result should be:

SELECT Id FROM Account ORDER BY Name DESC, Phone ASC

Moreover, the nulls first method should be related to one order by clause, so that each one can have a nulls first control. Right now we only have a global one.

Using enforceGlobalSecurity and lookup() throws System.QueryException: expecting an equals sign, found ')'

Consider example:

Query.enforceGlobalSecurity();
Query q = new Query(Account.SObjectType)
        .selectFields(new String[]{'Name'})
        .lookup('Id',
                new Query(Contact.SObjectType)
                        .selectField(Contact.AccountId));
System.debug(q.toQueryString());
q.run();

Which creates query

SELECT name FROM Account WHERE (Id IN (SELECT accountid FROM Contact WITH SECURITY_ENFORCED)) WITH SECURITY_ENFORCED

The problem is with inner join query has WITH SECURITY_ENFORCED which fails SOQL parser.

As a workaround you can disable security check for lookup query:

Query q = new Query(Account.SObjectType)
        .selectFields(new String[]{'Name'})
        .lookup('Id',
                new Query(Contact.SObjectType)
                        .enforceSecurity(false)
                        .selectField(Contact.AccountId));
                       

Add method to query by Ids only

It would be rather useful to re-query db by a set of Ids without explicitly declaring type required - library should infer the types of record itself and also handle cases where set has mixed types.

Also might be useful to have a method to pass a collection of SObjects and it re-queries with same fields that have been passed in.

Very useful when doing async and you wanna check if database still has same records!

Allow user to config flexible field attributes when select all fields

So far Query.apex supports selectAllFields, selectCreatableFields and selectEditableFields. But indeed Salesforce has way more attributes in the DescribeFieldResult type (https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_fields_describe.htm).

So my proposal is to add a new subclass named FieldSetting, which consists of the following fields:

public class FieldSetting {
    Boolean isAccessible;
    Boolean isAutoNumber;
    Boolean isCalculated;
    Boolean isCascadeDelete;
    Boolean isCaseSensitive;
    Boolean isCreateable;
    Boolean isCustom;
    Boolean isDefaultedOnCreate;
    Boolean isDependentPicklist;
    Boolean isExternalID;
    Boolean isFilterable;
    Boolean isFormulaTreatNullNumberAsZero;
    Boolean isGroupable;
    Boolean isHtmlFormatted;
    Boolean isIdLookup;
    Boolean isNameField;
    Boolean isNamePointing;
    Boolean isNillable;
    Boolean isPermissionable;
    Boolean isRestrictedDelete;
    Boolean isRestrictedPicklist;
    Boolean isSearchPrefilterable;
    Boolean isSortable;
    Boolean isUnique;
    Boolean isUpdateable;
    Boolean isWriteRequiresMasterRead;
}

Each of these fields matches an attribute in DescribeFieldResult.

In the meantime, allow selectAllFields to take FieldSetting as a parameter.

Query selectAllFields(FieldSetting setting) {}

Let's use an example to demonstrate this. Suppose a user wants to select all fields which are accessible and sortable, he may construct a FieldSetting object, and pass it to the selectAllFields method in Query:

FieldSetting setting = new FieldSetting();
setting.isAccessible = true;
setting.isSortable = true;

new Query('Account').selectAllFields(setting);

Then selectAllFields would select all the fields that matches the setting (accessible AND sortable).

The setting should have default values if user did not specify. isAccessible should be defaulted to be true, because it would be meaningless to select non-accessible fields. And the rest of the fields should be defaulted to be false.

To remain backward compatibility, we still need to preserve the selectCreatableFields(), selectEditableFields() and selectAllFields() methods.

Code coverage is not sufficient.

The code coverage for Query.cls is around 80%, which is low and just over the baseline 75% as org requirement. Need to add more test methods to reach higher code coverage, ideally 100%.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.