Class: Cybertrain::Model
- Inherits:
-
Object
- Object
- Cybertrain::Model
- Defined in:
- cybertrain/model.rb,
cybertrain/template/interpreter.rb
Overview
Base class of every model.
You never subclass it by hand: spin run gen writes one subclass per table
in db/schema.rb into gen/models/<singular>.rb (attribute accessors,
casts, finders and the associations read off foreign keys; see Article
for the full generated surface), and app/models/<singular>.rb reopens it,
with no superclass, for validations, callbacks and your own methods:
# app/models/article.rb
class Article
validates :title, presence: true
validates :body, presence: true, length: { minimum: 10 }
before_save { |article| article.title = article.title.strip }
def summary = body[0, 80]
end
Re-run spin run gen after changing the schema (cybertrain db migrate
and the development server do it for you).
Not available (compared with ActiveRecord): declared associations,
has_many :through, includes, pluck, update_all, destroy_all,
dirty tracking, transactions around save, STI, enums and the scope
macro (write def self.published = where(published: true) instead).
Implementation: validations and callbacks are runtime data keyed by model name, since Spinel cannot define methods at runtime.
Instance Attribute Summary collapse
-
#id ⇒ Integer
readonly
The primary key,
0until the record is first saved.
Class Method Summary collapse
-
.after_create {|record| ... } ⇒ nil
Runs in #save of a new record, after the INSERT (the record has its id).
-
.after_destroy {|record| ... } ⇒ nil
Runs in #destroy after the DELETE.
-
.after_save {|record| ... } ⇒ nil
Runs in #save after the row is written and after
after_create/after_update. -
.after_update {|record| ... } ⇒ nil
Runs in #save of a persisted record, after the UPDATE.
-
.before_create {|record| ... } ⇒ nil
Runs in #save of a new record, after
before_save, before the INSERT. -
.before_destroy {|record| ... } ⇒ nil
Runs in #destroy before the DELETE.
-
.before_save {|record| ... } ⇒ nil
Runs in #save after validation passes, before the INSERT or UPDATE.
-
.before_update {|record| ... } ⇒ nil
Runs in #save of a persisted record, after
before_save, before the UPDATE. - .before_validation {|record| ... } ⇒ nil
- .validates(attr, presence: false, length: nil, allow_blank: false) ⇒ nil
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
Records are equal when they are the same model and the same saved row.
-
#as_json ⇒ Hash{String => Object}
#attributes, ready for JSON: a Time becomes its ISO 8601 String (
"2026-10-05T09:00:00Z"); booleans, numbers, Strings and nil stay as they are (Cast.to_sql would turn booleans into 1/0). -
#attributes ⇒ Hash{String => Object}
Every column, keyed by its SQL name,
"id"first, with typed values. -
#destroy ⇒ Boolean
Runs Model.before_destroy, deletes the row, runs Model.after_destroy.
- #errors ⇒ Errors
-
#new_record? ⇒ Boolean
The opposite of #persisted?.
-
#persisted? ⇒ Boolean
True once the record has been saved or was loaded from the database, false after #destroy.
-
#reload ⇒ self
Re-reads every column from the database.
-
#save ⇒ Boolean
Validates, then runs before_save, before_create/before_update, writes the row, and runs after_create/after_update and after_save – the ActiveRecord order.
-
#save! ⇒ true
Like #save, but raises instead of returning false.
-
#to_json ⇒ String
#as_json as a JSON String, e.g.
-
#to_param ⇒ String
The id as a String, what route helpers put in a URL (
"0"for a new record). -
#update(attrs) ⇒ Boolean
Assigns the attributes (cast to the column types, unknown keys ignored) and saves.
-
#valid? ⇒ Boolean
Clears #errors, runs the Model.before_validation callbacks, then every rule declared with Model.validates.
Instance Attribute Details
#id ⇒ Integer (readonly)
The primary key, 0 until the record is first saved.
199 200 201 |
# File 'cybertrain/model.rb', line 199 def id @id end |
Class Method Details
.after_create {|record| ... } ⇒ nil
Runs in #save of a new record, after the INSERT (the record has its id).
135 136 137 138 139 140 |
# File 'cybertrain/model.rb', line 135 def self.after_create(&block) = add_model_callback("after_create", block) # Runs in {#save} of a persisted record, after `before_save`, before the # UPDATE. # @yieldparam record [Model] # @return [nil] # @api public |
.after_destroy {|record| ... } ⇒ nil
Runs in #destroy after the DELETE.
159 |
# File 'cybertrain/model.rb', line 159 def self.after_destroy(&block) = add_model_callback("after_destroy", block) |
.after_save {|record| ... } ⇒ nil
Runs in #save after the row is written and after after_create /
after_update.
125 126 127 128 129 |
# File 'cybertrain/model.rb', line 125 def self.after_save(&block) = add_model_callback("after_save", block) # Runs in {#save} of a new record, after `before_save`, before the INSERT. # @yieldparam record [Model] # @return [nil] # @api public |
.after_update {|record| ... } ⇒ nil
Runs in #save of a persisted record, after the UPDATE.
146 147 148 149 150 151 152 153 |
# File 'cybertrain/model.rb', line 146 def self.after_update(&block) = add_model_callback("after_update", block) # Runs in {#destroy} before the DELETE. There is no `dependent:`; delete # children here: # @example # before_destroy { |article| Comment.where(article_id: article.id).delete_all } # @yieldparam record [Model] # @return [nil] # @api public |
.before_create {|record| ... } ⇒ nil
Runs in #save of a new record, after before_save, before the INSERT.
130 131 132 133 134 |
# File 'cybertrain/model.rb', line 130 def self.before_create(&block) = add_model_callback("before_create", block) # Runs in {#save} of a new record, after the INSERT (the record has its id). # @yieldparam record [Model] # @return [nil] # @api public |
.before_destroy {|record| ... } ⇒ nil
Runs in #destroy before the DELETE. There is no dependent:; delete
children here:
154 155 156 157 158 |
# File 'cybertrain/model.rb', line 154 def self.before_destroy(&block) = add_model_callback("before_destroy", block) # Runs in {#destroy} after the DELETE. # @yieldparam record [Model] # @return [nil] # @api public |
.before_save {|record| ... } ⇒ nil
Runs in #save after validation passes, before the INSERT or UPDATE.
119 120 121 122 123 124 |
# File 'cybertrain/model.rb', line 119 def self.before_save(&block) = add_model_callback("before_save", block) # Runs in {#save} after the row is written and after `after_create` / # `after_update`. # @yieldparam record [Model] # @return [nil] # @api public |
.before_update {|record| ... } ⇒ nil
Runs in #save of a persisted record, after before_save, before the
UPDATE.
141 142 143 144 145 |
# File 'cybertrain/model.rb', line 141 def self.before_update(&block) = add_model_callback("before_update", block) # Runs in {#save} of a persisted record, after the UPDATE. # @yieldparam record [Model] # @return [nil] # @api public |
.before_validation {|record| ... } ⇒ nil
114 115 116 117 118 |
# File 'cybertrain/model.rb', line 114 def self.before_validation(&block) = add_model_callback("before_validation", block) # Runs in {#save} after validation passes, before the INSERT or UPDATE. # @yieldparam record [Model] # @return [nil] # @api public |
.validates(attr, presence: false, length: nil, allow_blank: false) ⇒ nil
Declares validations for one attribute, run by #valid? (and so by
#save) in declaration order. Only presence and length exist.
presence: trueadds"can't be blank"when the value isnilor a String that is empty afterstrip.falseand0are present.length: { minimum: n, maximum: m }(either key) adds"is too short (minimum is n characters)"or"is too long (maximum is m characters)". Anilvalue is left topresence; other keys (is:,in:) are ignored.allow_blank: trueskips the length rule for a blank value. It does not apply topresence.
For anything else (uniqueness, formats, cross-field rules), add errors
yourself in a before_validation block: valid? clears the errors,
runs that block, then the declared rules.
83 84 85 86 87 88 89 90 91 92 |
# File 'cybertrain/model.rb', line 83 def self.validates(attr, presence: false, length: nil, allow_blank: false) list = (VALIDATORS[self.name] ||= Array.new(0) { Validator.new(:base, :presence) }) list << Validator.new(attr, :presence) if presence unless length.nil? minimum = length.key?(:minimum) ? length[:minimum] : -1 maximum = length.key?(:maximum) ? length[:maximum] : -1 list << Validator.new(attr, :length, minimum, maximum, allow_blank) end nil end |
Instance Method Details
#==(other) ⇒ Boolean
Records are equal when they are the same model and the same saved row. Two unsaved records are never equal, not even a record to itself.
360 361 362 363 |
# File 'cybertrain/model.rb', line 360 def ==(other) return false unless other.is_a?(Model) @id != 0 && @id == other.id && model_name == other.model_name end |
#as_json ⇒ Hash{String => Object}
#attributes, ready for JSON: a Time becomes its ISO 8601 String
("2026-10-05T09:00:00Z"); booleans, numbers, Strings and nil
stay as they are (Cast.to_sql would turn booleans into 1/0).
383 384 385 386 387 388 389 390 391 392 |
# File 'cybertrain/model.rb', line 383 def as_json out = {} attributes.each do |k, v| out[k] = case v when Time then Cast.iso8601(v) else v end end out end |
#attributes ⇒ Hash{String => Object}
Every column, keyed by its SQL name, "id" first, with typed values.
370 371 372 373 374 375 376 |
# File 'cybertrain/model.rb', line 370 def attributes attrs = { "id" => read_attribute(:id) } self.class.column_names.each do |column| attrs[column] = read_attribute(column.to_sym) unless column == "id" end attrs end |
#destroy ⇒ Boolean
Runs before_destroy, deletes the row, runs after_destroy. The record keeps its attributes and id but is no longer #persisted?.
A record that was never saved has no row: destroy then runs no callbacks, sends no DELETE and returns false.
330 331 332 333 334 335 336 337 338 339 340 |
# File 'cybertrain/model.rb', line 330 def destroy return false unless @persisted run_callbacks("before_destroy") sql = "DELETE FROM #{Relation.quote_ident(self.class.table_name)} WHERE #{Relation.quote_ident("id")} = ?" id = @id Cybertrain::DB.with { |c| c.execute(sql, [id]) } @persisted = false run_callbacks("after_destroy") true end |
#errors ⇒ Errors
204 205 206 207 208 209 210 211 |
# File 'cybertrain/model.rb', line 204 def errors e = @errors return e unless e.nil? e = Errors.new @errors = e e end |
#new_record? ⇒ Boolean
Returns the opposite of #persisted?.
241 242 243 |
# File 'cybertrain/model.rb', line 241 def new_record? !@persisted end |
#persisted? ⇒ Boolean
Returns true once the record has been saved or was loaded from the database, false after #destroy.
235 236 237 |
# File 'cybertrain/model.rb', line 235 def persisted? @persisted end |
#reload ⇒ self
Re-reads every column from the database.
346 347 348 349 350 351 352 353 |
# File 'cybertrain/model.rb', line 346 def reload sql = "SELECT * FROM #{Relation.quote_ident(self.class.table_name)} WHERE #{Relation.quote_ident("id")} = ?" id = @id found = Cybertrain::DB.with { |c| c.execute_models(sql, [id]) } raise RecordNotFound, "Couldn't find #{model_name} with id=#{id}" if found.empty? load_row(found[0]) self end |
#save ⇒ Boolean
Validates, then runs before_save, before_create/before_update, writes the row, and runs after_create/after_update and after_save – the ActiveRecord order. Returns false (and writes nothing) when invalid.
A new record is INSERTed (created_at / updated_at are filled in when
the table has them and they are nil) and gets its #id; a persisted one
is UPDATEd, every column, with updated_at set to now. Timestamps are
UTC, to the second.
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 |
# File 'cybertrain/model.rb', line 284 def save return false unless valid? run_callbacks("before_save") if @persisted run_callbacks("before_update") update_row run_callbacks("after_update") else run_callbacks("before_create") insert_row run_callbacks("after_create") end run_callbacks("after_save") true end |
#save! ⇒ true
Like #save, but raises instead of returning false.
305 306 307 308 |
# File 'cybertrain/model.rb', line 305 def save! raise RecordInvalid, "Validation failed: #{errors..join(", ")}" unless save true end |
#to_json ⇒ String
#as_json as a JSON String, e.g. for render json: @article.to_json.
397 398 399 |
# File 'cybertrain/model.rb', line 397 def to_json JSON.generate(as_json) end |
#to_param ⇒ String
The id as a String, what route helpers put in a URL ("0" for a new
record).
249 250 251 |
# File 'cybertrain/model.rb', line 249 def to_param @id.to_s end |
#update(attrs) ⇒ Boolean
Assigns the attributes (cast to the column types, unknown keys ignored) and saves.
318 319 320 321 |
# File 'cybertrain/model.rb', line 318 def update(attrs) assign_attributes(attrs) save end |
#valid? ⇒ Boolean
Clears #errors, runs the before_validation callbacks, then every rule declared with validates.
257 258 259 260 261 262 |
# File 'cybertrain/model.rb', line 257 def valid? errors.clear run_callbacks("before_validation") Model.validators_for(model_name).each { |v| v.validate(self) } errors.empty? end |