Skip to content

Commit 40d8ad1

Browse files
authored
Merge pull request #220 from beyond-the-cloud-dev/feature/critique
SOQL Lib Critique
2 parents 048a47e + a71592d commit 40d8ad1

File tree

4 files changed

+192
-89
lines changed

4 files changed

+192
-89
lines changed

website/docusaurus.config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ const config = {
6868
{ name: 'canonical', content: 'https://soql.beyondthecloud.dev' }
6969
],
7070
colorMode: {
71-
defaultMode: 'dark',
71+
defaultMode: 'light',
7272
disableSwitch: false,
7373
},
7474
docs: {
@@ -112,6 +112,11 @@ const config = {
112112
position: 'left',
113113
label: '🚀 Playground',
114114
},
115+
{
116+
to: '/critique',
117+
position: 'left',
118+
label: 'Critique',
119+
},
115120
{
116121
href: 'https://github.com/beyond-the-cloud-dev/soql-lib',
117122
label: 'GitHub',

website/src/css/custom.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,20 @@ body {
7575
text-align: right;
7676
color: #999;
7777
}
78+
79+
/* Add separators between navbar items on the left */
80+
.navbar__item {
81+
padding: 0 0.5rem;
82+
}
83+
.navbar__item:not(:last-child)::after {
84+
content: '|';
85+
margin-left: 0.75rem;
86+
color: var(--ifm-navbar-link-color);
87+
opacity: 0.5;
88+
}
89+
90+
/* Don't add separator after GitHub/Blog on the right side */
91+
.navbar__items--right .navbar__item::after {
92+
content: none;
93+
}
94+

website/src/pages/critique.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# SOQL Lib Critique
2+
3+
## SOQL Lib is More Complex Than Traditional SOQL
4+
_aka "I have to learn a new syntax"_
5+
6+
### Use SOQL Evaluator
7+
8+
SOQL Lib has 3 different modules: [SOQL](/soql/getting-started), [SOQL Cache](/cache/getting-started), and [SOQL Evaluator](/evaluator/getting-started). SOQL Evaluator was created for developers who don't want to learn a new syntax but still want to benefit from features like mocking and result functions. You can use [this module](https://github.com/beyond-the-cloud-dev/soql-lib/tree/main/force-app/main/default/classes/main/soql-evaluator) without switching to an entirely new syntax.
9+
10+
```apex
11+
Set<Id> accountIds = SOQLEvaluator.of([SELECT Id FROM Account]).toIds();
12+
List<String> accountNames = SOQLEvaluator.of([SELECT Id, Name FROM Account]).toValuesOf(Account.Name);
13+
```
14+
15+
### It's Not That Complicated
16+
17+
#### Documentation
18+
19+
SOQL Lib provides comprehensive online documentation with the [playground](./playground) and numerous [examples](/soql/examples/select). You can also use the search feature in the top-right corner to find what you're looking for. The Fluent API was designed to stay as close to traditional SOQL syntax as possible. However, due to Apex's `Identifier name is reserved` restriction, some keywords like `select`, `where`, and `limit` couldn't be used.
20+
21+
#### Interfaces
22+
23+
"Do I need to go to the documentation and spend a lot of time searching for what I need?"
24+
25+
No. At the top of [SOQL.cls](https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/force-app/main/default/classes/main/standard-soql/SOQL.cls), we've placed all the interfaces you can interact with. Even as the author, I don't remember all the methods. However, I can quickly navigate to [SOQL.cls](https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/force-app/main/default/classes/main/standard-soql/SOQL.cls) and identify what I need in seconds. Everything important is at the top—you don't have to scroll through the entire class searching for methods. Just focus on the interfaces.
26+
27+
#### Use AI
28+
29+
A simple prompt in your IDE integrated with AI can be very helpful: "Based on SOQL.cls and SOQL_Test.cls, understand how SOQL Lib works. Write an inline query that returns all accounts with Employee Number greater than 100." Voilà! That's it. You don't need to read documentation or check interfaces manually.
30+
31+
### Less Complicated Than Traditional SOQL
32+
33+
#### Result Functions
34+
35+
SOQL Lib provides numerous [result functions](/soql/examples/result) that make your code easier to read and understand. Most operations you typically perform on SOQL results are available as methods in SOQL Lib. Instead of repeating the same transformations throughout your codebase, simply use result methods.
36+
37+
**Apex**
38+
39+
```apex
40+
Map<String, List<Account>> industryToAccounts = new Map<String, List<Account>>();
41+
42+
for (Account acc : [SELECT Id, Name, Industry FROM Account]) {
43+
if (!industryToAccounts.containsKey(acc.Industry)) {
44+
industryToAccounts.put(acc.Industry, new List<Acccount>());
45+
}
46+
47+
industryToAccounts.get(acc.Industry).put(acc);
48+
}
49+
```
50+
51+
**SOQL Lib**
52+
53+
```apex
54+
Map<String, List<Account>> industryToAccounts = (Map<String, List<Account>>) SOQL.of(Account.SObjectType)
55+
.toAggregatedMap(Account.Industry);
56+
```
57+
58+
#### Dynamic Query Builder
59+
60+
Without SOQL Lib, approximately 90% of your queries use traditional SOQL. The remaining 10% need to be dynamic, requiring numerous string operations. Your code typically looks like this:
61+
62+
```apex
63+
String accountName = '';
64+
65+
String query = 'SELECT Id, Name WHERE BillingCity = \'Krakow\'';
66+
67+
if (String.isNotEmpty(accountName)) {
68+
query += ' AND Name LIKE \'%' + accountName +'\%';
69+
}
70+
71+
query += ' FROM Account';
72+
73+
Database.query(query);
74+
```
75+
76+
This code is difficult to read and maintain. With SOQL Lib, you can refactor it to:
77+
78+
```apex
79+
String accountName = '';
80+
81+
SOQL.of(Account.SObjectType)
82+
.with(Account.Id, Account.Name)
83+
.whereAre(SOQL.FilterGroup
84+
.add(SOQL.Filter.with(Account.BillingCity).equal('Krakow'))
85+
.add(SOQL.Filter.name().contains(accountName).ignoreWhen(String.isEmpty(accountName)))
86+
)
87+
.toList();
88+
```
89+
90+
This is much easier to read. Additionally, the `ignoreWhen` function automatically checks if accountName is empty and ignores the condition accordingly—no more if statements cluttering your code.
91+
92+
## Additional Processing Time
93+
94+
SOQL Lib builds a query string and passes it to the `Database.queryWithBinds` method. How long do you think it takes to build a string like `SELECT Id, Name FROM Account`?
95+
96+
Not much. While dynamic code can be CPU-intensive, we've run extensive performance tests (full results coming soon). Here's a preview:
97+
98+
### Result Functions
99+
100+
Building a complex query dynamically with SOQL Lib consumes less than **2ms**, and around **1ms** for simple queries.
101+
Even if you execute 100 complex queries in one transaction (101 SOQL queries per synchronous transaction), in the worst-case scenario, SOQL Lib uses only ~200ms out of the 10,000ms CPU limit available.
102+
103+
Additionally, SOQL Lib can be faster than your own implementation. We perform internal optimizations for certain result functions.
104+
For instance:
105+
106+
```apex
107+
Set<String> accountNames = new Set<String>();
108+
109+
for (Account acc : [SELECT Name FROM Account]) {
110+
accountNames.add(acc.Name);
111+
}
112+
```
113+
114+
The SOQL Lib version is approximately 2x faster because we use internal aggregation optimizations. Learn more about this technique: https://salesforce.stackexchange.com/questions/393308/get-a-list-of-one-column-from-a-soql-result
115+
116+
```apex
117+
Set<String> accountNames = SOQL.of(Account.SObjectType).toValuesOf(Account.Name);
118+
```
119+
120+
### Mocking
121+
122+
How long does it take to run Apex unit tests with all test data inserted? Typically seconds, or even minutes.
123+
124+
How long does it take to run Apex unit tests when query results are mocked and there's no need to create test data? Milliseconds to seconds—definitely not minutes.
125+
126+
I don't need to emphasize the benefits of writing fast, reliable unit tests. Instead of spending time figuring out how to set fields so validation rules pass, or determining what setup is needed to avoid trigger errors, mocking allows you to return query results without any database operations.
127+
128+
With mocking, you not only save hours on test data creation but also reduce test execution time by minutes. If someone argues that SOQL Lib consumes CPU time, they should consider that they cannot afford NOT to mock query results.
129+
130+
## It's Just a Query Builder
131+
132+
No, it's much more than that.
133+
134+
The query builder is just one component of SOQL Lib. SOQL Lib itself is a lightweight yet powerful alternative to FFLib Selectors. It provides all the benefits of FFLib and significantly more. The main advantage is that it's extremely easy to use compared to FFLib.
135+
136+
**You can:**
137+
- Mock your queries
138+
- Cache your query results
139+
- Build your own lightweight selectors
140+
- Control Field-Level Security (FLS)
141+
- Control sharing rules
142+
- Use result functions to make your code cleaner and faster
143+
- Use the query builder to avoid string concatenation
144+
145+
**For a comprehensive list of benefits, check:**
146+
- [SOQL Basic Features](/soql/basic-features)
147+
- [SOQL Cache Basic Features](/cache/basic-features)
148+

website/src/pages/playground.js renamed to website/src/pages/playground.jsx

Lines changed: 21 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,26 +1107,11 @@ class SOQLToSOQLLibTranslator {
11071107
}
11081108

11091109
export default function Playground() {
1110-
const [soqlInput, setSoqlInput] = useState(`SELECT Id, Name, Industry, BillingCity
1111-
FROM Account
1112-
WHERE Industry = 'Technology'
1113-
AND BillingCity = 'San Francisco'
1114-
ORDER BY Name ASC
1115-
LIMIT 10
1116-
WITH USER_MODE`);
1110+
const [soqlInput, setSoqlInput] = useState('SELECT Id, Name, Industry, BillingCity\nFROM Account\nWHERE Industry = \'Technology\' \n AND BillingCity = \'San Francisco\'\nORDER BY Name ASC\nLIMIT 10\nWITH USER_MODE');
11171111

11181112
const [soqlLibOutput, setSoqlLibOutput] = useState('');
11191113
const [isLoading, setIsLoading] = useState(false);
11201114

1121-
// Trigger syntax highlighting when output changes
1122-
useEffect(() => {
1123-
if (typeof window !== 'undefined' && window.Prism && soqlLibOutput) {
1124-
setTimeout(() => {
1125-
window.Prism.highlightAll();
1126-
}, 100);
1127-
}
1128-
}, [soqlLibOutput]);
1129-
11301115
const translator = new SOQLToSOQLLibTranslator();
11311116

11321117
const handleTranslate = () => {
@@ -1153,124 +1138,72 @@ WITH USER_MODE`);
11531138
};
11541139

11551140
const examples = [
1156-
{
1157-
name: "Simple Query",
1158-
query: `SELECT Id, Name
1159-
FROM Account
1160-
WHERE Name LIKE '%Test%'
1161-
WITH USER_MODE`
1162-
},
1141+
{
1142+
name: "Simple Query",
1143+
query: "SELECT Id, Name\nFROM Account\nWHERE Name LIKE '%Test%'\nWITH USER_MODE"
1144+
},
11631145
{
11641146
name: "Multiple Conditions",
1165-
query: `SELECT Id, Name, Owner.Name
1166-
FROM Account
1167-
WHERE Industry = 'Technology'
1168-
AND BillingCity = 'San Francisco'
1169-
WITH USER_MODE`
1147+
query: "SELECT Id, Name, Owner.Name\nFROM Account\nWHERE Industry = 'Technology'\n AND BillingCity = 'San Francisco'\nWITH USER_MODE"
11701148
},
11711149
{
11721150
name: "OR Conditions",
1173-
query: `SELECT Id, Name
1174-
FROM Account
1175-
WHERE Industry = 'Technology'
1176-
OR Industry = 'Healthcare'
1177-
WITH USER_MODE`
1151+
query: "SELECT Id, Name\nFROM Account\nWHERE Industry = 'Technology'\n OR Industry = 'Healthcare'\nWITH USER_MODE"
11781152
},
11791153
{
11801154
name: "Parent Fields",
1181-
query: `SELECT Id, Name, CreatedBy.Id, CreatedBy.Name, Parent.Id, Parent.Name
1182-
FROM Account
1183-
WITH USER_MODE`
1155+
query: "SELECT Id, Name, CreatedBy.Id, CreatedBy.Name, Parent.Id, Parent.Name\nFROM Account\nWITH USER_MODE"
11841156
},
11851157
{
11861158
name: "COUNT & SUM",
1187-
query: `SELECT CampaignId, COUNT(Id) totalRecords, SUM(Amount) totalAmount
1188-
FROM Opportunity
1189-
GROUP BY CampaignId
1190-
WITH USER_MODE`
1159+
query: "SELECT CampaignId, COUNT(Id) totalRecords, SUM(Amount) totalAmount\nFROM Opportunity\nGROUP BY CampaignId\nWITH USER_MODE"
11911160
},
11921161
{
11931162
name: "AVG & MIN",
1194-
query: `SELECT Industry, AVG(AnnualRevenue) avgRevenue, MIN(NumberOfEmployees) minEmployees
1195-
FROM Account
1196-
GROUP BY Industry
1197-
WITH USER_MODE`
1163+
query: "SELECT Industry, AVG(AnnualRevenue) avgRevenue, MIN(NumberOfEmployees) minEmployees\nFROM Account\nGROUP BY Industry\nWITH USER_MODE"
11981164
},
11991165
{
12001166
name: "SubQuery",
1201-
query: `SELECT Id, Name, (SELECT Id, Name FROM Contacts)
1202-
FROM Account
1203-
WITH USER_MODE`
1167+
query: "SELECT Id, Name, (SELECT Id, Name FROM Contacts)\nFROM Account\nWITH USER_MODE"
12041168
},
12051169
{
12061170
name: "Complex WHERE",
1207-
query: `SELECT Id
1208-
FROM Account
1209-
WHERE Industry = 'IT'
1210-
AND ((Name = 'My Account' AND NumberOfEmployees >= 10)
1211-
OR (Name = 'My Account 2' AND NumberOfEmployees <= 20))
1212-
WITH USER_MODE`
1171+
query: "SELECT Id\nFROM Account\nWHERE Industry = 'IT'\n AND ((Name = 'My Account' AND NumberOfEmployees >= 10)\n OR (Name = 'My Account 2' AND NumberOfEmployees <= 20))\nWITH USER_MODE"
12131172
},
12141173
{
12151174
name: "LIKE Patterns",
1216-
query: `SELECT Id, Name
1217-
FROM Account
1218-
WHERE Name LIKE 'Test%'
1219-
AND BillingCity LIKE '%Francisco%'
1220-
WITH USER_MODE`
1175+
query: "SELECT Id, Name\nFROM Account\nWHERE Name LIKE 'Test%'\n AND BillingCity LIKE '%Francisco%'\nWITH USER_MODE"
12211176
},
12221177
{
12231178
name: "IN Operator",
1224-
query: `SELECT Id, Name
1225-
FROM Account
1226-
WHERE Industry IN ('Technology', 'Healthcare', 'Finance')
1227-
WITH USER_MODE`
1179+
query: "SELECT Id, Name\nFROM Account\nWHERE Industry IN ('Technology', 'Healthcare', 'Finance')\nWITH USER_MODE"
12281180
},
12291181
{
12301182
name: "ORDER BY Multiple",
1231-
query: `SELECT Id, Name, Industry
1232-
FROM Account
1233-
ORDER BY Name DESC, Industry ASC
1234-
LIMIT 50
1235-
WITH USER_MODE`
1183+
query: "SELECT Id, Name, Industry\nFROM Account\nORDER BY Name DESC, Industry ASC\nLIMIT 50\nWITH USER_MODE"
12361184
},
12371185
{
12381186
name: "Complex Query",
1239-
query: `SELECT Id, Name
1240-
FROM Account
1241-
WHERE (Industry = 'Technology' OR Industry = 'Healthcare')
1242-
AND NumberOfEmployees > 100
1243-
ORDER BY Name
1244-
LIMIT 20
1245-
WITH USER_MODE`
1187+
query: "SELECT Id, Name\nFROM Account\nWHERE (Industry = 'Technology' OR Industry = 'Healthcare')\n AND NumberOfEmployees > 100\nORDER BY Name\nLIMIT 20\nWITH USER_MODE"
12461188
},
12471189
{
12481190
name: "Boolean Fields",
1249-
query: `SELECT Id, Name
1250-
FROM Account
1251-
WHERE IsDeleted = false
1252-
AND IsPersonAccount = true
1253-
WITH USER_MODE`
1191+
query: "SELECT Id, Name\nFROM Account\nWHERE IsDeleted = false\n AND IsPersonAccount = true\nWITH USER_MODE"
12541192
},
12551193
{
12561194
name: "NULL Checks",
1257-
query: `SELECT Id, Name
1258-
FROM Account
1259-
WHERE ParentId != null
1260-
AND BillingCity = null
1261-
WITH USER_MODE`
1195+
query: "SELECT Id, Name\nFROM Account\nWHERE ParentId != null\n AND BillingCity = null\nWITH USER_MODE"
12621196
},
12631197
{
12641198
name: "System Mode",
1265-
query: `SELECT Id, Name, CreatedBy.Id, CreatedBy.Name, Parent.Id, Parent.Name
1266-
FROM Account
1267-
WITH SYSTEM_MODE`
1199+
query: "SELECT Id, Name, CreatedBy.Id, CreatedBy.Name, Parent.Id, Parent.Name\nFROM Account\nWITH SYSTEM_MODE"
12681200
}
12691201
];
12701202

12711203
// Initial translation
1272-
React.useEffect(() => {
1204+
useEffect(() => {
12731205
handleTranslate();
1206+
// eslint-disable-next-line react-hooks/exhaustive-deps
12741207
}, []);
12751208

12761209
return (

0 commit comments

Comments
 (0)