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:

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:

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:

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

Direct including types

Defined in:

lustra/extensions/full_text_searchable/full_text_searchable.cr
lustra/model/collection.cr
lustra/model/errors.cr
lustra/model/model.cr

Macro Summary

Instance Method Summary

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

macro default_scope(&block) #

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

[View source]
macro scope(name, &block) #

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.


[View source]

Instance Method Detail

def __pkey__ #

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.


[View source]
def cache : Lustra::Model::QueryCache | Nil #

[View source]