Models
In this section, we'll provide a brief overview of how to get use out of Jennifer ORM. Note that this section is based on Jennifer Docs.
Jennifer in Action
Mappings
Jennifer ORM uses a variety of macros to programatically define your model's associations and attributes. An important macro in particular is mapping
which is used to describe model attributes. If a field has no extra parameter, you can specify name and type (type in case of crystal language): field_name: :Type
. But you can use a tuple and provide the following parameters:
:type
crystal data type (don't use question mark - for now you can use only :null
option)
:primary
mark field as primary key (default is false
)
:null
allows field to be nil
(default is false
for all fields except primary key
:default
default value which is set in creating a new object
:getter
if getter should be created (default - true
)
:setter
if setter should be created (default - true
)
Let's look at an example
In the above codeblock Jennifer's mapping
macro is used to define a variety of attributes for the Contact
model which map onto the database table neatly. Note that there are a variety of optional configurations such as default
values and null
security that can be appended to the mapping
definition.
By default, expect that all fields are defined in the model. If that is not true, you should pass false
as the second argument and override the ::field_count
method to represent the correct field count.
Using a mapping will cause Jennifer to define the following methods on your model:
#initialize
Hash(String | Symbol, DB::Any), NamedTuple, MySql::ResultSet
constructors
::field_count
number of fields
::field_names
all fields names
#{{field_name}}
getter
#{{field_name}}_changed?
represents if field is changed
#{{field_name}}!
getter with not_nil!
if null: true
was passed
#{{field_name}}=
setter
::_{{field_name}}
helper method for building queries
#{{field_name}}_changed?
shows if field was changed
#changed?
shows if any field was changed
#primary
value of primary key field
::primary
returns criteria for primary field (query dsl)
::primary_field_name
name of primary field
::primary_field_type
type of primary key
#new_record?
returns true
if record has nil
primary key (is not stored to db)
::create
Hash(String | Symbol, DB::Any)
, NamedTuple
creates object, stores it to db and returns it
::create!
Hash(String | Symbol, DB::Any)
, NamedTuple
creates object, stores it to db and returns it; otherwise raise exception
::build
Hash(String | Symbol, DB::Any), NamedTuple
builds object
::create
Hash(String | Symbol, DB::Any)
, NamedTuple
builds object from hash and saves it to db with all callbacks
::create!
Hash(String | Symbol, DB::Any)
, NamedTuple
builds object from hash and saves it to db with callbacks or raise exception
#save
saves object to db; returns true
if success and false
elsewhere
#save!
saves object to db; returns true
if success or raise exception otherwise
#to_h
returns hash with all attributes
#to_str_h
same as #to_h
but with String keys
#attribute
String | Symbol
returns attribute value by its name
#attributes_hash
returns to_h
with deleted nil
entries
#changed?
check if any field was changed
#set_attribute
String | Symbol
, DB::Any
sets attribute by given name
#attribute
String | Symbol
returns attribute value by its name
The model is automatically associated with a table by its underscored and pluralized class name, but a custom name can be defined using the ::table_name
method in its own body before using any relation (::singular_table_name
- for singular variant).
Relations
Jennifer provides 4 types of relations out of the box: has_many
, belongs_to
, has_and_belongs_to_many
, and has_one
. While they sound similar they generate slightly different methods and behaviour.
Relation take the following arguments:
Relation name
Target class
request
- additional request (will be used inside of where clause) - optionalforeign
- name of foreign key - optional; by default it uses the singularized table name + "_id"primary
- primary field name - optional; by default it uses the default primary field of the class.
has_and_belongs_to_many
also accepts the next 2 arguments and uses regular arguments in a slightly different way:
join_table
- join table name; by default, relation table names are in alphabetic order, joined by underscores.join_foreign
- foreign key for current model (left foreign key of join table)foreign
- used as right foreign keyprimary
- used as primary field of current table; for now it properly works only if both models in this relation have a primary field namedid
All relation macros provide the following methods:
#{{relation_name}}
- cache relation object (or array of them) and returns it.#{{relation_name}}_reload
- reload relation and returns it.#{{relation_name}}_query
- returns query which is used to get objects of this object relation entities from the db.#remove_{{relation_name}}
- removes given object from relation.#add_{{relation_name}}
- adds given object to relation or builds it from has and adds.
This allows for dynamically adding/removing objects to relations and automatically sets a foreign id:
belongs_to
and has_one
add extra method #relation_name!
which also adds assertion for nil
.
STI
Single table inheritance can be used in this way:
The subclass extends the superclass definition with new fields and uses string field type
to identify itself.
Now
Profile.all
will return objects ofProfile
class, not taking into account thetype
field and will raise an exception if the superclass doesn't override::field_count
.
Scopes (aka. Prepared queries)
Scopes Jennifer's way of creating reusable prepared query statements. The scope
macro allows you to define a Crystal block containing the desired prepared query statement, and creates a class method on the model to execute. The block is executed in the query context and provides a chainability with various Jennifer queries such as join, where, having, etc.
Scopes are helpful as they can be chained together to create complex queries which are readable and cognitively ergonomic.
Validations
Jennifer uses accord for validations. Validations are mechanisms to run pre-flight checks on your models to ensure desired fields are present or values belong to a particular set. There are several general macros for declaring validations:
validates_with_method(*names)
- accepts method name/namesvalidates_inclusion(field, value)
- checks ifvalue
includes@{{field}}
validates_exclusion(field, value)
- checks ifvalue
excludes@{{field}}
validates_format(field, format)
- checks if{{format}}
matches@{{field}}
validates_length(field, **options)
- check@{{field}}
size; allowed options are::in
,:is
,:maximum
,:minimum
validates_uniqueness(field)
- check if@{{field}}
is unique
If a record is not valid, it will not be saved.
Callbacks
Callbacks are Jennifer's way of providing hooks into the lifecycle of a record. The available macros for defining callbacks include:
before_save
after_save
before_create
after_create
after_initialize
before_destroy
They accept method names.
Timestamps
with_timestamps
macros add callbacks for created_at
and updated_at
fields in an update.
Destroy
To destroy an object, use .delete
(called without callback) or .destroy
. To destroy several objects by their ids, use the class method:
Update
There are several ways to update an object. Some of them were mentioned in the mapping section. A few extra methods to do this include:
#update_column(name, value)
- sets attribute directly and stores it to the db without any callback.#update_columns(values)
- same for several columns.#update_attributes(values)
- just set attributes.#set_attribute(name, value)
- set attribute by given name.
Query DSL
Jennifer allows you to build lazy evaluated queries with a chainable syntax. However, some methods can only be used at the end of a chain (such as .first
or .pluck
). Here is a list of all DSL methods:
Find
An object can be retrieved by id using find
(returns T?
) and find!
(returns T
or raise RecordNotFound
exception) methods.
Where
all
retrieves everything (only at the beginning; creates empty request).
where
clauses are extremely flexible. The where
method accepts a block which represents the where clause of the query statement. You can chain several where
methods together and they will be concatenated using AND
.
To specify a field, use c
method which accepts a string as the field name. As mentioned, after declaring model attributes, you can use their names inside of a block: _field_name
if it is for the current table and ModelName._field_name
if for another model. As a method name, you can specify an attribute of the 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.
Below are a few examples:
You can also use primary
to mention primary field:
Supported operators:
| Operator | SQL variant | | | --- | --- | | ==
| =
| | | !=
| !=
| | | <
| <
| | | <=
| <=
| | | >
| >
| | | >=
| >=
| | | =~
| REGEXP
, ~
| | | &
| AND
| | | ` | ` | OR
|
And operator-like methods:
regexp
REGEXP
, ~
(accepts String
)
not_regexp
NOT REGEXP
like
LIKE
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
And Postgres specific:
contain
@>
contained
<@
overlap
&&
Jennifer supports literal SQL query statements via the .sql
method:
The query will be inserted "as is". Usage of .sql
allows the use of nested plain request.
Tips
All regular expression methods accept string representation of the regular expression
Use parentheses for binary operators (
&
and|
)nil
given to!=
and==
will be transformed toIS NOT NULL
andIS NULL
is
andnot
operator accepts values::nil
,nil
,:unknown
,true
,false
At the end - several examples:
Select
Raw SQL for SELECT
clause can be passed into .select
method. This has the highest priority in forming this query part.
From
Also you can provide a sub-query to specify the FROM clause.
Be careful when using sub-queries with source fields during result retrieving and mapping to objects.
Delete and Destroy
For now they both are the same - creates delete query with given conditions. destroy
first loads objects and runs callbacks and then calls delete on each.
It can be only at the end of chain.
Joins
To join another table you can use join
method passing model class or table name (String
) and join type (default is :inner
).
Query, built inside of block, will be passed to ON
section of JOIN
. Current context of block is joined table.
Also there are two shortcuts for left and right joins:
For now Jennifer provides manual aliasing as a second argument for
#join
and automatic when using#includes
and#with
methods. For details check out the code.
Relation
To join a model relation such as has_many
, belongs_to
and has_one
you can pass it's name and join type as:
Includes
To preload a relation use includes
and pass relation name:
If there are several includes with same table - Jennifer will auto alias tables.
Group
.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:
Here keys should be table names.
Having
.having
allows adding HAVING
to the query. It accepts a block the same way as .where
does.
Exists
.exists?
check if there is any record with provided conditions. Can only be at the end of the query chain - it hits the db.
Distinct
.distinct
retrieves database column values without repeats. It can accept column names and the table name as an optional second parameter. distinct
can only be at the end of the query chain as it results in a direct database query.
Aggregation
There are 2 types of aggregation functions: ones which are working without a GROUP clause and returns single values (e.g. max
, min
, count
), and ones working with a GROUP clause and returning an array of values.
Max
Min
Avg
Sum
Count
Group Max
Group Min
Group Avg
Group Sum
Pagination
For now you can only specify limit
and offset
:
Order
You can specifies orders to sort:
It accepts hash as well.
Update
You can provide a hash or a named tuple with new field values:
Will not trigers any callback.
Eager load
As mentioned previously, Jennifer provides lazy query evaluation, this means it will perform the operation only after trying to access an element from the collection (any array method - it implements Enumerable). Also you can extract the first entity via first
. If you are sure that at least one entity in the database satisfies your query, you can call .first!
.
To extract only some fields rather than entire objects, use pluck
:
It returns an array of values if only one field was given, and an array of arrays if more. It accepts raw sql arguments, so be carefuo when using this when joining tables with same field names. This allows to retrieving custom data from specified select clauses.
To load relations using same query joins needed tables (yep you should specify join on condition by yourself again) and specifies all needed relations in with
(relation name not table).
Transaction
Transaction mechanism provides block-like syntax:
If an error was raised in the transaction block, the transaction will be rolled back. To rollback a transaction, raise DB::Rollback
exception.
A transaction will lock the connection for the current fiber, avoiding getting a new one from pool.
Truncation
To truncate an entire table use:
This functionality could be useful to clear the db between test cases.
Important restrictions
sqlite3 has a lot of limitations so its support will not be added soon.
Testing
The quickest way to rollback any changes made to the database after a test case is using transaction. You can ensure smooth testing by adding the following to your spec_helper.cr
:
Also just regular deleting or truncation could be used but transactions provide 15x speed up (at least for postgres; mysql gets less impact).
This functions can be safely used only under test environment.
Last updated