module
Lustra::Model
Overview
Model definition is made by adding the Lustra::Model
mixin in your class.
Simple Model
class MyModel
include Lustra::Model
column my_column : String
end
We just created a new model, linked to your database, mapping the column my_column
of type String (text
in postgres).
Now, you can play with your model:
row = MyModel.new # create an empty row
row.my_column = "This is a content"
row.save! # insert the new row in the database !
By convention, the table name will follow an underscore, plural version of your model: my_models
.
A model into a module will prepend the module name before, so Logistic::MyModel
will check for logistic_my_models
in your database.
You can force a specific table name using:
class MyModel
include Lustra::Model
self.table = "another_table_name"
end
Presence validation
Unlike many ORM around, Lustra carry about non-nullable pattern in crystal. Meaning column my_column : String
assume than a call to row.my_column
will return a String.
But it exists cases where the column is not yet initialized:
- When the object is built with constructor without providing the value (See above).
- When an object is semi-fetched through the database query. This is useful to ignore some large fields non-interesting in the body of the current operation.
For example, this code will compile:
row = MyModel.new # create an empty row
puts row.my_column
However, it will throw a runtime exception You cannot access to the field 'my_column' because it never has been initialized
Same way, trying to save the object will raise an error:
row.save # Will return false
pp row.errors # Will tell you than `my_column` presence is mandatory.
Thanks to expressiveness of the Crystal language, we can handle presence validation by simply using the Nilable
type in crystal:
class MyModel
include Lustra::Model
column my_column : String? # Now, the column can be NULL or text in postgres.
end
This time, the code above will works; in case of no value, my_column will be nil
by default.
Querying your code
Whenever you want to fetch data from your database, you must create a new collection query:
MyModel.query # Will setup a vanilla 'SELECT * FROM my_models'
Queries are fetchable using each
:
MyModel.query.each do |model|
# Do something with your model here.
end
Refining your query
A collection query offers a lot of functionalities.
Column type
By default, Lustra map theses columns types:
String
=>text
Numbers
(any from 8 to 64 bits, float, double, big number, big float) =>int, large int etc... (depends of your choice)
Bool
=>text or bool
Time
=>timestamp without timezone or text
JSON::Any
=>json and jsonb
Nilable
=>NULL
(treated as special !)
NOTE: The crystal-pg
gems map also some structures like GIS coordinates, but their implementation is not tested in Lustra. Use them at your own risk. Tell me if it's working 😉
If you need to map special structure, see Mapping Your Data guides for more informations.
Primary key
Primary key is essential for relational mapping. Currently Lustra support only one column primary key.
A model without primary key can work in sort of degraded mode, throwing error in case of using some methods on them:
collection#first
will be throwing error if noorder_by
has been setup
To setup a primary key, you can add the modifier primary: true
to the column:
class MyModel
include Lustra::Model
column id : Int32, primary: true, presence: false
column my_column : String?
end
Note the flag presence: false
added to the column. This tells Lustra than presence checking on save is not mandatory. Usually this happens if you setup a default value in postgres. In the case of our primary key id
, we use a serial auto-increment default value.
Therefore, saving the model without primary key will works. The id will be fetched after insertion:
m = MyModel
m.save!
m.id # Now the id value is setup.
Helpers
Lustra provides various built-in helpers to facilitate your life:
Timestamps
class MyModel
include Lustra::Model
timestamps # Will map the two columns 'created_at' and 'updated_at', and map some hooks to update their values.
end
Theses fields are automatically updated whenever you call save
methods, and works as Rails ActiveRecord.
With Serial Pkey
class MyModel
include Lustra::Model
primary_key "my_primary_key"
end
Basically rewrite column id : UInt64, primary: true, presence: false
Argument is optional (default = id)
Included Modules
- Lustra::ErrorMessages
- Lustra::Model::ClassMethods
- Lustra::Model::Connection
- Lustra::Model::FullTextSearchable
- Lustra::Model::HasColumns
- Lustra::Model::HasFactory
- Lustra::Model::HasHooks
- Lustra::Model::HasRelations
- Lustra::Model::HasSaving
- Lustra::Model::HasScope
- Lustra::Model::HasSerialPkey
- Lustra::Model::HasTimestamps
- Lustra::Model::HasValidation
- Lustra::Model::Initializer
- Lustra::Model::JSONDeserialize
Direct including types
Defined in:
lustra/extensions/full_text_searchable/full_text_searchable.crlustra/model/collection.cr
lustra/model/errors.cr
lustra/model/model.cr
Macro Summary
-
default_scope(&block)
Define a default scope that will be automatically applied to all queries.
-
scope(name, &block)
A scope allow you to filter in a very human way a set of data.
Instance Method Summary
-
#__pkey__
Alias method for primary key.
- #cache : Lustra::Model::QueryCache | Nil
Class methods inherited from module Lustra::Model::FullTextSearchable
to_tsq(text)
to_tsq
Macros inherited from module Lustra::Model::FullTextSearchable
full_text_searchable(through = "full_text_vector", catalog = "pg_catalog.english", scope_name = "search")
full_text_searchable
Macros inherited from module Lustra::Model::HasFactory
polymorphic(through = "type")
polymorphic
Macros inherited from module Lustra::Model::HasRelations
belongs_to(name, foreign_key = nil, no_cache = false, primary = false, foreign_key_type = Int64, touch = nil, counter_cache = nil)
belongs_to,
has_many(name, through = nil, foreign_key = nil, own_key = nil, primary_key = nil, no_cache = false, polymorphic = false, foreign_key_type = nil, autosave = false)
has_many,
has_one(name, foreign_key = nil, primary_key = nil, no_cache = false, polymorphic = false, foreign_key_type = nil, autosave = false)
has_one
Instance methods inherited from module Lustra::Model::HasValidation
add_error(column, reason)add_error(reason) add_error, clear_errors clear_errors, error? error?, errors : Array(Error) errors, print_errors print_errors, valid! valid!, valid? valid?, validate validate
Macros inherited from module Lustra::Validation::Helper
ensure_than(field, message, &block)
ensure_than,
on_presence(*fields, &block)
on_presence
Instance methods inherited from module Lustra::Model::HasSaving
decrement(column : Symbol | String, by = 1)
decrement,
decrement!(column : Symbol | String, by = 1)
decrement!,
delete
delete,
destroy
destroy,
increment(column : Symbol | String, by = 1)
increment,
increment!(column : Symbol | String, by = 1)
increment!,
persisted? : Bool
persisted?,
reload : self
reload,
save(on_conflict : Lustra::SQL::InsertQuery -> | Nil = nil)save(&block) save, save!(on_conflict : Lustra::SQL::InsertQuery -> | Nil = nil)
save!(&block : Lustra::SQL::InsertQuery -> ) save!, save_with_associations(on_conflict : Lustra::SQL::InsertQuery -> | Nil = nil) save_with_associations, update(**args) update, update!(**args) update!, update_column(column : Symbol | String, value) update_column, update_columns(columns : NamedTuple)
update_columns(columns : Hash(String, Lustra::SQL::Any))
update_columns(**columns) update_columns
Macros inherited from module Lustra::Model::HasSerialPkey
add_pkey_type(type, &block)
add_pkey_type,
primary_key(name = "id", type = :bigserial)
primary_key
Macros inherited from module Lustra::Model::HasTimestamps
timestamps
timestamps
Instance methods inherited from module Lustra::Model::HasColumns
[](x) : Lustra::SQL::Any
[],
[]?(x) : Lustra::SQL::Any
[]?,
reset(h : Hash(String, _))reset(h : Hash(Symbol, _))
reset(**t : **T) forall T reset, set(h : Hash(String, _))
set(h : Hash(Symbol, _))
set(**t : **T) forall T set, to_h(full = false) to_h, update_h update_h
Macros inherited from module Lustra::Model::HasColumns
column(name, primary = false, converter = nil, column_name = nil, presence = true, mass_assign = true)
column
Instance methods inherited from module Lustra::Model::HasHooks
trigger_after_events(event_name)
trigger_after_events,
trigger_before_events(event_name)
trigger_before_events,
with_triggers(event_name, &)
with_triggers
Macros inherited from module Lustra::Model::HasHooks
after(event_name, method_name)
after,
before(event_name, method_name)
before
Instance methods inherited from module Lustra::ErrorMessages
build_error_message(message : String, ways_to_resolve : Tuple | Array = Tuple.new)
build_error_message,
converter_error(from, to)
converter_error,
format_width(x, w = 80)
format_width,
illegal_setter_access_to_undefined_column(name)
illegal_setter_access_to_undefined_column,
lack_of_primary_key(model_name)
lack_of_primary_key,
migration_already_down(number)
migration_already_down,
migration_already_up(number)
migration_already_up,
migration_irreversible(name = nil, operation = nil)
migration_irreversible,
migration_not_found(number)
migration_not_found,
migration_not_unique(numbers)
migration_not_unique,
no_migration_yet(version)
no_migration_yet,
null_column_mapping_error(name, type)
null_column_mapping_error,
order_by_error_invalid_order(current_order)
order_by_error_invalid_order,
polymorphic_nil(through)
polymorphic_nil,
polymorphic_unknown_class(class_name)
polymorphic_unknown_class,
query_building_error(message)
query_building_error,
uid_not_found(class_name)
uid_not_found,
uninitialized_db_connection(connection)
uninitialized_db_connection
Macro Detail
Define a default scope that will be automatically applied to all queries. Useful for soft deletes, multi-tenancy, or any filter that should always apply.
Warning: Default scopes can be confusing as they're implicit. Use sparingly and document clearly.
Usage:
class Post
include Lustra::Model
column deleted_at : Time?
default_scope { where { deleted_at == nil } }
end
Post.query # SELECT * FROM posts WHERE deleted_at IS NULL
Post.query.first # Also applies default scope
To bypass default scope, use unscoped
:
Post.query.unscoped # SELECT * FROM posts (no default scope)
Post.query.unscoped.count # Works with any query method
A scope allow you to filter in a very human way a set of data.
Usage:
scope("admin") { where({role: "admin"}) }
for example, instead of writing:
User.query.where { (role == "admin") & (active == true) }
You can write:
User.admin.active
Scope can be used for other purpose than just filter (e.g. ordering), but I would not recommend it.
Instance Method Detail
Alias method for primary key.
If Model#id
IS the primary key, then calling Model#__pkey__
is exactly the same as Model#id
.
This method exists to tremendously simplify the meta-programming code. If no primary key has been setup to this model, raise an exception.