Query DSL
My favorite part. Jennifer allows you to build lazy evaluated queries with chaining syntax. But some of them could be only at the and of a chain (such as #first
, #find
or #pluck
) because they trigger request.
WHERE
.all
creates empty request for a model it is invoked on.
Contact.all
Specifying where clause is really flexible. Method accepts block which presents WHERE
clause of request (or it’s part - you can chain several #where
and they will be concatenated using AND
).
To specify table column use #c
method which accepts string parameter as a name. As it was mentioned earlier after declaring model attributes, you can use their names inside of block: _field_name
if it is for current table and ModelName._field_name
if you need to reference a column field_name
of the table related to ModelName
model. Also there you can specify attribute of some model or table using underscores: _some_model_or_table_name__field_name
- model/table name is separated from field name by __
. You can specify relation in space of which you want to declare condition using double _
at the beginning and block. Several examples:
Contact.where { c("id") == 1 }
Contact.where { _id == 1 }
Contact.join(Address) { Contact._id == _contact_id }
Contact.relation(:addresses).where { __addresses { _id > 1 } }
Contact.where { _contacts__id == 1 }
Also you can use primary
to mention primary field:
Passport.where { primary.like("%123%") }
Supported operators:
Operator | SQL variant |
---|---|
== |
= |
!= |
!= |
< |
< |
<= |
<= |
> |
> |
>= |
>= |
=~ |
REGEXP , ~ |
& |
AND |
| |
OR |
xor |
XOR |
Also there are shortcuts for AND
, OR
and XOR
operators to emit extra brackets around operands and wraps the result into them:
Post.where { _active & (_likes > 10) } # WHERE posts.active AND users.likes > 10
Post.where { and(_active, _likes > 10) } # WHERE (posts.active AND users.likes > 10)
Operator-like methods:
Method | SQL variant |
---|---|
regexp |
REGEXP , ~ (accepts String ) |
not_regexp |
NOT REGEXP |
like |
LIKE |
ilike |
ILIKE for pg and LIKE for mysql |
not_like |
NOT LIKE |
is |
IS and provided value |
not |
NOT and provided value (or as unary operator if no one is given) |
in |
IN |
between |
BETWEEN |
Postgres specific:
Method | SQL variant |
---|---|
contain |
@> |
contained |
<@ |
overlap |
&& |
Also Jennifer supports json field path methods for criteria: Criteria#take
(also accessible as Criteria#[]
) and Criteria#path
.
If you need to fetch the first matched record
Contact.find_by!({:name => "John", :surname => "Doe"}) # #<Contact name: "John", surname: "Doe">
MySQL
For mysql both take
and path
methods behave in the same way.
There are 2 supported cases:
*
WHERE field_name->"$.selector"
could be specified using
where { _field_name["$.selector"]}
*
WHERE field_name->"$[1]"
can be specified as:
where { _field_name.take(1) }
PostgreSQL
#path
method use#>
operator#take
method use->
operator
Tips
- all regexp methods accepts string presentation of regexp;
- use parenthesis with binary operators (
&
and|
); nil
given to!=
and==
will be transformed toIS NOT NULL
andIS NULL
;is
andnot
operator accepts next values:nil
,:unknown
,true
andfalse
;ANY
andALL
statement allow to path nested query;- you can also use query instance (wrapped into
Grouping
object) as condition argument
Several examples:
Contact.where { (_id > 40) & _name.regexp("^[a-d]") }
Contact.where { and(_id > 40, _name.regexp("^[a-d]")) }
Address.where { _contact_id.is(nil) }
nested_query = Address.all.where { _main }.select(:contact_id)
Contact.all.where { _id == any(nested_query) }
Contact.all.where { _id.in(g(Contact.all.where { _name.like("%ohn") })) }
Raw query
To specify exact SQL query use #sql
method:
# it behaves like regular criterion
Contact.where { sql("age > ?", [15]) & (_name == "Stephan") }
Query will be inserted “as is”. Prefer to avoid such usage but it allows to use database specific functions and features. By default given SQL statement is surrounded with brackets, to avoid them - pass false
as 2nd (or 3rd) argument.
#sql
also excepts prepared arguments. To mark place to be replaced with db specific argument placeholder(? for mysql and $ notation for postgres) use common crystal %s
:
Contact.where { _name == sql("lower(%s)", ["Sam"], false) }
which will be transformed to:
SELECT contacts.*
FROM contacts
WHERE contacts.name = lower($1)
Complex logical condition
To design some complex logical expression like a & (b | c) & d
use ExpressionBuilder#g
method:
Contact.where do
(_id > 0) & g(_name.like("%asd%") | _age > 15) & (id < 100)
end
Functions
There is special mechanism to define SQL functions like CURRENT_DATE
, ABS
or CONCAT
. There is already a predefined list of such functions so you can use them in the expression builder context:
Contact.all.where { ceil(_balance) > 10 }
Here is the list of such functions:
- lower
- upper
- current_timestamp
- current_date
- current_time
- now
- concat
- abs
- ceil
- floor
- round
To define own function:
Jennifer::QueryBuilder::Function.define("ceil", arity: 1) do
def as_sql(generator)
"CEIL(#{operand_sql(operands[0], generator)})"
end
end
It is necessary to define #as_sql
method, which returns function SQL. #operands
is an array of given function arguments. #operand_sql
is a helper method to automatically parse how a given argument should be inserted to the SQL.
Smart arguments parsing
Next methods provide flexible api for passing arguments:
#order
#reorder
#group
#select
They allows pass argument (tuple, named tuple or hash - depending on context) of String
, Symbol
or Cryteria
. String
arguments will be parsed as plain SQL (RawSql
) and Symbol
- as Criteria
.
SELECT
Raw SQL for SELECT
clause could be passed into #select
method. This have the highest priority during SELECT
clause composing of SQL request.
Contact
.select("COUNT(id) AS count")
.group("name")
.having { sql("COUNT(id)") > 1 }
.pluck(:count)
Also #select
accepts block where all fields could be specified and aliased:
Contact
.select { [sql("COUNT(id)").alias("count")] }
.group("name")
.having { sql("count") > 1 }
.pluck(:count)
It is important to note that currently it is impossible to pass Jennifer::QueryBuilder::Condition
to #select
. In other words Contact.select { [(_age * 2).alias("age")] }
isn’t allowed. As a workaround you can temporary use #sql
: Contact.select { [sql("age * 2").alias("age")] }
.
JOIN
To join another table you can use join
method passing model class or table name (String
) and join type (default is :inner
).
field = "contact_id"
table = "passports"
Contact.join(Address) { Contact._id == _contact_id }.join(table) { c(field) == _id }
Query, built inside of block, will passed to ON
section of JOIN
. Current context of block is joined table.
Also there is two shortcuts for left and right joins:
Contact.left_join(Address) { _contacts__id == _contact_id }
Contact.right_join("addresses") { _contacts__id == c("contact_id") }
For now Jennifer provide manual aliasing as second argument for
#join
and automatic when using#eager_load
and#with
methods. For details check out the code.
Relation
To join model relation (has_many, belongs_to and has_one) pass it’s name and join type:
Contact.relation("addresses").relation(:passport, type: :left)
Relation eager loading
Actual eager load
To automatically join some relation and get it from db use #eager_load
and pass relation name:
Contact.eager_load("addresses")
If there are several eager_load with same table - Jennifer will auto alias tables.
Includes (preload)
To load all related objects after main query being executed use #includes
method (or it’s alias #preload
):
Contact.includes(:addresses)
GROUP BY
Contact.group("name", "id").pluck(:name, :id)
#group
allows to add columns for GROUP BY
section. If passing arguments are tuple of strings or just one string - all columns will be parsed as current table columns. If there is a need to group on joined table or using fields from several tables use next:
Contact
.select { [_addresses__street, _contacts__name] }
.relation("addresses")
.group(addresses: ["street"], contacts: ["name"])
.results
HAVING
Contact.group("name").having { _age > 15 }
#having
allows to add HAVING
part of query. It accepts block same way as #where
does.
EXISTS
Contact.where { _age > 42 }.exists? # returns true or false
#exists?
check is there is any record with provided conditions. Can be only at the end of query chain - it hit the db.
DISTINCT
Adds DISTINCT
keyword of at the very beginning of SELECT
statement
Contact.distinct # Array(Contact) with unique attributes (all)
UNION
To make common SQL UNION
you can use #union
method which accepts other query object. But be careful - all selected fields should have same name and type.
Address
.select(:contact_id)
.where { _street.like("%St. Paul%") }
.union(
Profile
.select(:contact_id)
.where { _login.in(["login1", "login2"]) }
)
.results
In this example you can’t use regular #to_a
because resulted records are not an address neither profile so they couldn’t be mapped to any model. That’s why only Jennifer::Record
could be obtained (which is done by #results
).
WITH
You can specify common table expression (even recursive):
Jennifer::Query["cte"].with(
"cte",
Jennifer::Query[""]
.select("1 as n")
.union(
Jennifer::Query["cte"].select("1 + n AS n").where { _n < 5 },
true
),
true
)
None
If at some point you decides to make query to return empty result set - use next:
q = Contacts.where { _age > 19 }
q.none
q.where { _name.like("Jo%") }
q.to_a
But be careful - all further chainable method calls will continue modify the object - only db call will be avoided.