Database QueryBuilder
Conditions concept
For conditions optional match operators are supported.
The operator must be added to field name like field {OP}
.
For example:
db.select('users')
.where('some_field >=', 10)
.execute(as);
The following standard ops are assumed:
=
- equal<>
- not equal>
- greater>=
- greater or equal<
- less<=
- less or equalIN
- in array or subquery (assumed)NOT IN
- not in array or subquery (assumed)BETWEEN
- two value tuple is assumed for inclusive range matchNOT BETWEEN
- two value tuple is assumed for inverted inclusive range matchLIKE
- LIKE matchNOT LIKE
- NOT LIKE match- other ops may be implicitely supported
Note: EXISTS
, ANY
and SOME
are not supported by design due to known performance issues in many database implementations.
Advanced condition builders
Internally, all conditions are represented as array with optional sub-arrays. It’s possible to use this representation directly for very complex cases.
The details are out of scope of this guide.
Unsafe DML concept
Sometimes developers forget to add conditions to DML statements like delete or update. QueryBuilder has a built-in safety feature to raise error, unless unsafe DML statement is explicitly allowed.
Native QueryBuilder API
-
QueryBuilder clone()
- create copy of builder
-
Expression escape(value)
- shorcut for
helpers().escape(value)
- shorcut for
-
Expression identifier(name)
- shorcut for
helpers().identifier(name)
- shorcut for
-
Expression expr(expr)
- shorcut for
helpers().expr(expr)
- shorcut for
-
Expression param(name)
name
- parameter name- wrapped placeholder for prepared statement
-
Helpers helpers()
- Get additional helpers which may not be implemented for all database types
-
QueryBuilder get(field[, value])
field
- field namevalue
- arbitrary value, expression or QueryBuilder sub-query
-
QueryBuilder get(List fields)
fields
- list of field names to select
-
QueryBuilder get(Map fields)
fields
- name => expression pairs to select
-
QueryBuilder getInsertID(String field)
field
- name of field with auto-generated value
-
QueryBuilder newRow()
- enables multi-row insert on first call
- saves current
set()
values into list, if any
-
QueryBuilder set(field[, value])
field
- stringvalue
- arbitrary value, expression or QueryBuilder sub-query
-
QueryBuilder set(Map fieldValueMap)
- calls
set()
for each pair offieldValueMap
- calls
-
QueryBuilder set(QueryBuilder select_query)
- special for “INSERT-SELECT” cases
select_query
field names are used for target field names
-
QueryBuilder where(conditions)
conditions
- see concept
-
QueryBuilder where(field, value)
- handy shorcut for
where({ field: value })
- handy shorcut for
-
QueryBuilder having(conditions)
conditions
- see concept
-
QueryBuilder having(field, value)
- handy shorcut for
having({ field: value })
- handy shorcut for
QueryBuilder group(field_expr)
QueryBuilder order(field_expr, Boolean ascending=true)
QueryBuilder limit(Integer count, Integer offset)
-
QueryBuilder join(join_type, entity, conditions)
join_type
- “INNER”, “LEFT”entity
- see L1.queryBuilder()conditions
- see concept
-
QueryBuilder innerJoin(entity, conditions)
- shortcut for
join("INNER", entity, conditions)
- shortcut for
-
QueryBuilder leftJoin(entity, conditions)
- shortcut for
join("LEFT", entity, conditions)
- shortcut for
-
void execute(AsyncSteps as, Boolean unsafe_dml=false)
- creates query string and calls
query()
unsafe_dml
- fail on DML query without conditions, if true
- creates query string and calls
-
void executeAssoc(AsyncSteps as, Boolean unsafe_dml=false)
- Same as
execute()
, but process response and passes array of maps and amount of affected rows instead.
- Same as
-
Prepared prepare(Boolean unsafe_dml=false)
- Return an interface for efficient execution of built query multiple times
Examples
1. Simple queries
const db = ccm.db();
let q;
// prepare table
// ---
db.query(as, 'DROP TABLE IF EXISTS SomeTbl');
db.query(as, 'CREATE TABLE SomeTbl(' +
'id int auto_increment primary key,' +
'name varchar(255) unique)');
// insert some data
// ---
// - simple set
db.insert('SomeTbl').set('name', 'abc').execute(as);
// - set as object key=>value pairs
db.insert('SomeTbl').set({name: 'klm'}).execute(as);
// - set with Map key=>value pairs
db.insert('SomeTbl')
.set(new Map([['name', 'xyz']]))
.getInsertID('id')
.executeAssoc(as);
// use insert ID
as.add((as, res, affected) => console.log(`Insert ID: ${res[0].$id}`));
// INSERT-SELECT like query
// ---
// sub-query must be the only parameter for .set()
db.insert('SomeTbl').set(
// DANGER: .get() expects expressions and does not escape them!
db.select('SomeTbl').get('name', "CONCAT('INS', name)").where('id <', 3)
).execute(as);
// update data
const helpers = db.helpers();
q = db.update('SomeTbl')
// - .set can be called multiple times
.set('id', 10)
// - please note that set auto-escapes all values, unless wrapped with .expr()
.set('name', helpers.expr('CONCAT(id, name)'))
// - simple condition
.where('name', 'klm')
// - extra calls are implicit "AND"
// - Most powerful array-based definition which is
// very close to how all conditions are handled internally.
.where([
'OR', // The scope of OR is only children of this array
// object as member, all fields are AND assumed
{
// there are various generic suppported operators
'name LIKE': 'kl%',
// another example
'id >': 1,
},
// Inner complex array
[
'AND', // this can be omitted as "AND" is implicit for arrays
// raw expression as string - DANGER of SQLi, please avoid
'name NOT LIKE \'xy%\'',
// another example of operator with two values
{ 'id BETWEEN': [1, 10] }
],
// Note: Map object can also be used
]);
// Dump raw query for inspection
console.log(`Query: ${q}`);
// UPDATE SomeTbl SET id=10,name=CONCAT(id, name) WHERE name = 'klm' AND (name LIKE 'kl%' OR id > 1 OR (name NOT LIKE 'xy%' AND id BETWEEN 1 AND 10))
// Finally, execute it
q.execute(as);
// Select without entity
// ---
db.select().get('atm', 'NOW()').executeAssoc(as);
as.add((as, res) => console.log(`At the moment: ${res[0].atm}`));
// Select with join of result of sub-query (instead of normal table)
// ---
q = db.select('SomeTbl')
.innerJoin(
// sub-query
// NOTE: use of .escape() for .get()
[ db.select().get('addr', helpers.escape('Street 123')), 'Alias'],
// all where-like conditions are supported here
'1 = 1' // can be omitted
);
console.log(`Query: ${q}`);
// SELECT * FROM SomeTbl INNER JOIN (SELECT 'Street 123' AS addr) AS Alias ON 1 = 1
q.executeAssoc(as);
// inspect result
as.add((as, res) => console.log(res));
/*
* [
* { id: 10, name: '10klm', addr: 'Street 123' },
* { id: 1, name: 'abc', addr: 'Street 123' },
* { id: 4, name: 'INSabc', addr: 'Street 123' },
* { id: 5, name: 'INSklm', addr: 'Street 123' },
* { id: 3, name: 'xyz', addr: 'Street 123' },
* ]
*/
2. Prepared QueryBuider
// create a prepared statement with query builder
// ---
const helpers = db.helpers();
const prepared_q = db.insert('SomeTbl')
// notice .param() placeholder
.set('name', helpers.param('nm'))
.getInsertID('id')
.prepare();
for (let nm of ['abc', 'klm', 'xyz']) {
// prepared_q is not QueryBuilder, but Prepared object
prepared_q.executeAssoc(as, {nm});
as.add((as, res) =>
console.log(`Inserted ${nm} ID ${res[0].$id}`));
}
// Inserted abc ID 1
// Inserted klm ID 2
// Inserted xyz ID 3