Ruby on Rails Module 2 – Active Record: models and the database
Active Record models and CRUD
Follow one Active Record row through create, read, update and delete, and pick the save method that fits a change in a Rails 8.1 app.
What you will learn
- Create, read, update and delete records with Active Record and read the SQL each call runs
- Choose between save, save!, update, update_columns, update_all, destroy and delete for a given change
- Use typed attributes, type casting on assignment and dirty tracking to track and inspect changes
Before you start
What you’ll learn · Before you start
What you will learn
- Create, read, update and delete records with Active Record and read the SQL each call runs
- Choose between save, save!, update, update_columns, update_all, destroy and delete for a given change
- Use typed attributes, type casting on assignment and dirty tracking to track and inspect changes
Before you start
On this page
In Kaksha, the coaching-centre app this track builds, one row of the batches table is a Class 10 Science class with
24 seats and a fee of 15,00,000 paise, which is Rs 15,000. Writing Batch.find(7) reads it in one SQL query, but
writing it back is a choice: the Rails validations guide lists six methods that run the model’s validations and
nineteen that skip them, so one copy-paste can leave a batch with a negative fee in the database.
This lesson follows one row through its lifetime: how a column of a table becomes a typed attribute, which read method
raises and which returns nil, and which save method runs the model’s validations. Every example is a
standalone Active Record: Rails' object-relational mapper: one Ruby class holds the rules, queries and lifecycle of one database table, and the table's columns become typed attributes of the class. script with an in-memory SQLite database, so the only moving
parts are the model and the SQL you can watch each call run.
A column of a table becomes a typed attribute
Active Record is Rails’ object-relational mapper. One class holds the rules and queries for one table: a model’s name
tells it which table to use, so Batch reaches batches. The Batch class used throughout this lesson needs no column
list in Ruby.
The script’s schema has name (string, not null), fee_paise (integer, not null, default 0), seats (integer, not
null, default 30) and timestamps, and the model is one line:
# A full CRUD tour of one Active Record model, standalone (no Rails app).
# The in-memory SQLite database lives only for the run; a schema builds the
# batches table, and the Batch class maps to it with no configuration.
# A small subscriber on sql.active_record prints only the SQL the model runs,
# so we see what each call does to the database.
require "active_record"
require "logger"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
# Keep schema setup, transactions and ar_internal_metadata out of the trace:
# just the SQL that CRUD on batches runs.
ActiveSupport::Notifications.subscribe("sql.active_record") do |event|
sql = event.payload[:sql]
next if event.payload[:name].to_s =~ /SCHEMA|TRANSACTION/
next if sql.start_with?("CREATE", "PRAGMA", "SELECT sqlite", "SELECT sql")
next if sql.include?("ar_internal_metadata")
puts " SQL: #{sql.gsub(/\s+/, ' ').strip}"
end
ActiveRecord::Schema.verbose = false
ActiveRecord::Schema.define do
create_table :batches do |t|
t.string :name, null: false
t.integer :fee_paise, null: false, default: 0
t.integer :seats, null: false, default: 30
t.timestamps
end
end
class Batch < ActiveRecord::Base; end
def section(title)
puts
puts "== #{title} =="
end
section "Create"
science = Batch.create!(name: "Class 10 Science", fee_paise: 15_00_000, seats: 24)
puts " New row: id=#{science.id}, name=#{science.name.inspect}"
# A second row for the find/where section below.
Batch.create!(name: "Class 10 Maths", fee_paise: 12_50_000, seats: 20)
section "Read: find, find_by, where"
puts " find(#{science.id}) -> #{Batch.find(science.id).name.inspect}"
begin
Batch.find(9999)
rescue ActiveRecord::RecordNotFound => e
puts " find(9999) raised: ActiveRecord::RecordNotFound (#{e.message[0, 48]}...)"
end
puts " find_by(name: 'Nothing') -> #{Batch.find_by(name: "Nothing").inspect}"
rel = Batch.where("fee_paise > ?", 13_00_000)
puts " where(...) class before use: #{rel.class.name} (nothing has run yet)"
puts " .to_a: #{rel.to_a.map(&:name).inspect}"
puts " first (no order): #{Batch.first.name.inspect} (ordered by primary key)"
puts " last (no order): #{Batch.last.name.inspect}"
section "Type casting on assignment"
science.seats = "22" # a string from params
puts " seats = \"22\" became #{science.seats.inspect} of #{science.seats.class.name}"
puts " changed? #{science.changed?} ; changes: #{science.changes.inspect}"
section "Update"
science.update!(fee_paise: 16_00_000)
puts " fee_paise now Rs #{science.fee_paise / 100}, saved_change? #{science.saved_change_to_fee_paise?}"
puts " previous_changes keys: #{science.previous_changes.keys.sort.inspect}"
section "Delete"
science.destroy
puts " destroyed? #{science.destroyed?} ; count after destroy: #{Batch.count}" Output
== Create ==
SQL: INSERT INTO "batches" ("name", "fee_paise", "seats", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) RETURNING "id"
New row: id=1, name="Class 10 Science"
SQL: INSERT INTO "batches" ("name", "fee_paise", "seats", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) RETURNING "id"
== Read: find, find_by, where ==
SQL: SELECT "batches".* FROM "batches" WHERE "batches"."id" = ? LIMIT ?
find(1) -> "Class 10 Science"
SQL: SELECT "batches".* FROM "batches" WHERE "batches"."id" = ? LIMIT ?
find(9999) raised: ActiveRecord::RecordNotFound (Couldn't find Batch with 'id'=9999...)
SQL: SELECT "batches".* FROM "batches" WHERE "batches"."name" = ? LIMIT ?
find_by(name: 'Nothing') -> nil
where(...) class before use: ActiveRecord::Relation (nothing has run yet)
SQL: SELECT "batches".* FROM "batches" WHERE (fee_paise > ?)
.to_a: ["Class 10 Science"]
SQL: SELECT "batches".* FROM "batches" ORDER BY "batches"."id" ASC LIMIT ?
first (no order): "Class 10 Science" (ordered by primary key)
SQL: SELECT "batches".* FROM "batches" ORDER BY "batches"."id" DESC LIMIT ?
last (no order): "Class 10 Maths"
== Type casting on assignment ==
seats = "22" became 22 of Integer
changed? true ; changes: {"seats" => [24, 22]}
== Update ==
SQL: UPDATE "batches" SET "fee_paise" = ?, "seats" = ?, "updated_at" = ? WHERE "batches"."id" = ?
fee_paise now Rs 16000, saved_change? true
previous_changes keys: ["fee_paise", "seats", "updated_at"]
== Delete ==
SQL: DELETE FROM "batches" WHERE "batches"."id" = ?
SQL: SELECT COUNT(*) FROM "batches"
destroyed? true ; count after destroy: 1
Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby crud_tour.rb
Open each section of the output. In Create, Batch.create! built an INSERT that writes every column; Active Record
asked the database for the table’s columns the first time the model needed them, so no mapping file is needed. In Read, find
and find_by each ran one SELECT with LIMIT 1, and where built a Relation on the client. In Update, one UPDATE
set fee_paise and seats and the timestamp; in Delete, one DELETE ended the row.
Now the two assignments. Setting science.seats = "22" read back as 22 of class Integer: a column
type casting: Converting a value to the type a column expects when it is assigned to an attribute, so a string from a request becomes the Integer, Date or Boolean the schema holds. the string the moment it was assigned, with the same type the column holds.
This is why params[:seats], which comes in as a String from a form, becomes a usable Integer as soon as it reaches
the model: the model reads from the schema, not from the request.
Each assignment also showed up in dirty tracking: Active Record's record of which attributes a model has changed since it was loaded or last saved, read with changed?, changes, saved_change_to_<name>? and previous_changes.: changed? was true, changes
returned {"seats" => [24, 22]} (the old value and the new), and after the update saved_change_to_fee_paise? was
true. These predicates let callbacks and audit code ask what moved in the last save without a second query.
New Rails 8.1 apps hide most attributes in inspect
The generator writes config.active_record.attributes_for_inspect = [:id] in config/environments/production.rb.
In the production console, batch.inspect then prints only the id and the row’s class, so a leaked attribute cannot
reach a screenshot of a shoulder-surfed session. Development keeps every attribute visible.
Reading one row and reading many
Three method families read rows, and they answer a missing row in three different ways. The step through below walks each one from the Ruby call on the left to the SQL on the right, using the queries the example above printed:
The drawing shows the whole scene; its 9 steps are listed below.
Every step as text
- find(1): the row is there. Batch.find(1) runs one query with the id and LIMIT 1. The database returns the one row and Active Record builds it into a Batch object.
- find returned the Batch. The call hands back the Batch object. find always looks for one specific row.
- find(9999): no row. The same shape of query runs with an id that no row has. The database returns nothing.
- find raises RecordNotFound. A missing row is a problem for find. It raises ActiveRecord::RecordNotFound, which Rails controllers turn into a 404 page by default.
- find_by on a missing name. find_by runs the same shape of query on any attribute. It can also return no row.
- find_by returned nil. For find_by a missing row is not an error. The call returns nil, and the caller decides what to do with it.
- where: nothing has run yet. Batch.where("fee_paise > ?", 1300000) builds a Relation on the client. No SQL has run yet, so the condition can be built up piece by piece before it costs anything.
- Reading the Relation runs it. As soon as the Relation is read with .to_a, .each, .count or any method that needs the rows, Active Record runs one SELECT and builds every row into a Batch object.
- The rows come back. The database returns every row that matches. The order is not promised, so add .order(:name) when the order matters to you.
find(id) is for one specific record. If no row has that id, it raises ActiveRecord::RecordNotFound, and Rails’
default rescue turns that into a 404 response, so controllers can call find(params[:id]) without an if for the
missing case. find_by(attribute: value) is for the general case: it also runs one SELECT ... LIMIT 1, but it
returns nil when no row matches, which is correct when the row may not exist and the caller can decide what to do.
where(...) is different again. The script printed ActiveRecord::Relation for Batch.where("fee_paise > ?", 1_300_000) before the Relation was read. No SQL ran yet: a Relation is a plan the client builds up and only
executes when a method asks for the rows (to_a, each, count, find_each and many more). This is what lets
chained scopes stay cheap: Batch.where(name: "Class 10").where("fee_paise > ?", 1_300_000).order(:name) is still one
query at the end.
Two shorter calls close the family. Batch.first and Batch.last run SELECT ... ORDER BY batches.id ASC LIMIT 1
and ... DESC LIMIT 1: when no order is given, they order by the primary key, which is why first returned Science,
the batch created first, and last returned Maths. Add .order(:name) or any other order when the order of ids is
not the one you want.
Key idea
find raises RecordNotFound, find_by returns nil, and where returns a lazy Relation; first and last
order by the primary key when no order is given.
Checkpoint
Check: Reading rows with find, find_by and where
Writing changes with validations
Validations get a lesson of their own later in this module, so Batch here has just two rules: name must be present
and fee_paise must be a positive integer. The next example tries every save method on the Batch and prints what
each one did:
# Which save method runs validations? Which touches updated_at? Standalone
# Active Record script, in-memory SQLite. Four columns: name, fee_paise,
# seats, timestamps. Two validations: name must be present and fee_paise
# must be a positive integer. The script tries each save method on the
# same model and prints what each did.
require "active_record"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Schema.verbose = false
ActiveRecord::Schema.define do
create_table :batches do |t|
t.string :name, null: false
t.integer :fee_paise, null: false, default: 0
t.integer :seats, null: false, default: 30
t.timestamps
end
end
class Batch < ActiveRecord::Base
validates :name, presence: true
validates :fee_paise, numericality: { only_integer: true, greater_than: 0 }
end
def section(title)
puts
puts "== #{title} =="
end
science = Batch.create!(name: "Class 10 Science", fee_paise: 15_00_000, seats: 24)
start_timestamp = science.updated_at
section "save returns false on an invalid record"
science.name = ""
science.fee_paise = -100
puts " save -> #{science.save.inspect}"
puts " errors.full_messages: #{science.errors.full_messages.inspect}"
# Reset for the next step.
science.reload
section "save! raises ActiveRecord::RecordInvalid"
science.fee_paise = -5
begin
science.save!
rescue ActiveRecord::RecordInvalid => e
puts " raised: ActiveRecord::RecordInvalid"
puts " message: #{e.message}"
end
science.reload
section "update runs validations and touches updated_at"
sleep 0.01 # make sure a new timestamp differs from the first
ok = science.update(fee_paise: 15_50_000)
puts " update -> #{ok.inspect} ; validations ran: errors.any? #{science.errors.any?}"
puts " updated_at changed by update: #{science.updated_at != start_timestamp}"
after_update = science.updated_at
section "update_columns skips validations and keeps updated_at"
science.update_columns(fee_paise: -1, seats: 999)
puts " valid? after update_columns: #{science.valid?.inspect} (fee_paise -1 would fail validation)"
puts " updated_at changed by update_columns: #{science.updated_at != after_update}"
puts " reload shows the row in the database: fee_paise=#{science.reload.fee_paise}"
science.update!(fee_paise: 15_50_000) # repair for the next step
before_update_all = science.reload.updated_at
section "update_all also skips validations and keeps updated_at"
# Give every batch an invalid fee in one SQL round-trip.
sleep 0.01
Batch.update_all(fee_paise: -42)
science.reload
puts " all rows now have fee_paise=#{Batch.distinct.pluck(:fee_paise).inspect}"
puts " updated_at touched by update_all? #{science.updated_at != before_update_all}" Output
== save returns false on an invalid record == save -> false errors.full_messages: ["Name can't be blank", "Fee paise must be greater than 0"] == save! raises ActiveRecord::RecordInvalid == raised: ActiveRecord::RecordInvalid message: Validation failed: Fee paise must be greater than 0 == update runs validations and touches updated_at == update -> true ; validations ran: errors.any? false updated_at changed by update: true == update_columns skips validations and keeps updated_at == valid? after update_columns: false (fee_paise -1 would fail validation) updated_at changed by update_columns: false reload shows the row in the database: fee_paise=-1 == update_all also skips validations and keeps updated_at == all rows now have fee_paise=[-42] updated_at touched by update_all? false
Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby save_vs_update_columns.rb
save is the baseline. It assigns nothing: you set attributes, then call save. On an invalid record it returns
false and fills errors.full_messages, so a controller can render the form again with the messages. save! is the
same call that raises ActiveRecord::RecordInvalid instead, which is right in a job or a script where a failure must
roll back the surrounding transaction. The example reproduced both: save returned false with two messages, and
save! raised RecordInvalid with the fee rule that failed in its message.
update(fee_paise: 15_50_000) wraps the two in one call: it assigns, validates, saves and returns true or false.
update! raises in the same way as save!. Both touch updated_at, so a cache keyed on it or an audit log notices
the row has moved. These three methods also run the before_save, after_save, before_update and after_update
callbacks, so any code that keeps a counter cache, invalidates a view fragment or logs a change still runs for every
change.
The Persistence API describes these three as saves that validate first; the next two are documented as writes that skip validations.
Writing without validations, and when that is right
update_columns and update_all write to the database without running validations, without running callbacks and
without changing updated_at. They are a different kind of operation. The example used update_columns(fee_paise: -1, seats: 999) on the same batch: valid? right after was false, the row kept its old updated_at, and
reload showed the fee really became -1. On update_all the same was true for every row the Relation covered,
and the update_all API page says why: it sends a single UPDATE statement to the database and builds no Batch object
for any row.
Which is right depends on where the value comes from and what else the change touches. Answer the two questions and the table rules the methods out:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Method | Runs validations | Runs callbacks | Touches updated_at | On failure |
|---|---|---|---|---|
| save | Good yes: the record must be valid before any SQL runs | Good yes: before_save, after_save and the create or update callbacks run | Good yes: both timestamps on a new row, updated_at on a change | Mixed returns false and fills errors.full_messages |
| save! | Good yes: the record must be valid before any SQL runs | Good yes: the save callbacks run | Good yes, as save does | Mixed raises ActiveRecord::RecordInvalid, which rolls back the transaction |
| update(attributes) | Good yes: assigns the attributes, then saves | Good yes: the save callbacks run | Good yes: a save, so updated_at changes | Mixed returns false and fills errors (update! raises) |
| update_columns(attributes) | Poor no: writes straight to the row, even when the record is invalid | Poor no: before_save and after_save do not run | Poor no: updated_at keeps its value until a later save | Mixed raises on a database error, such as a check constraint |
| update_all(attributes) | Poor no: one UPDATE for every row the Relation covers | Poor no: never loads a row, never runs model code | Poor no: updated_at keeps its value for every row | Good returns the count of rows the database updated |
How the answers decide
- “Where does the value you want to save come from?” A form or API request a user filled in: rules out update_columns(attributes) and update_all(attributes). A user may send a value the validations would reject; a method that skips them lets a bad row reach the database.
- “Must updated_at change (so a cache or an audit log knows the row moved)?” Yes, every change the row records: rules out update_columns(attributes) and update_all(attributes). update_columns and update_all keep updated_at as it was, so a cache that trusts it would still show the old row.
- “Where does the value you want to save come from?” A one-off change on every row that matches: adds a caveat to save, save! and update(attributes). For a change on many rows at once, update_all makes one SQL round-trip; the others load every row into Ruby first.
In short: for a value a user sent (a form, an API request, a webhook that is not fully trusted), use save, save!
or update so validations check it. For a repair of data your own code owns, where you have already decided the
value is good, update_columns is the right tool on one row and update_all on many. For a change that must leave
updated_at as it is (think of a background migration that fixes a typo in name without pretending the row has
just been updated), update_columns and update_all are the only choices.
Writing user input with update_columns to go faster
A controller that reads params[:fee_paise] and calls batch.update_columns(fee_paise: params[:fee_paise].to_i)
will accept a negative number because no validation ran, and will not change updated_at, so a cache keyed on it
still shows the old fee. The right tool for a request from a user is update.
Checkpoint
Check: Writing changes that skip validations
destroy loads the row, delete does not
Removing a row has the same two families. destroy loads the record, runs before_destroy and after_destroy,
follows the dependent: rules of its associations and then issues the DELETE. delete runs one DELETE on that id,
with no row loaded and no callback invoked. The example’s last section shows destroy ending with one
DELETE FROM "batches" WHERE "batches"."id" = ? and destroyed? returning true.
On a collection the same split holds, with the same shape of SQL:
| Call | Loads rows? | Runs callbacks? | Follows dependent: rules? | SQL |
|---|---|---|---|---|
batch.destroy |
the one row | yes | yes | one DELETE after the callbacks |
batch.delete |
no | no | no | one DELETE |
Batch.where(archived: true).destroy_all |
each matching row | yes, once per row | yes, per row | one SELECT and N DELETEs |
Batch.where(archived: true).delete_all |
no | no | no | one DELETE with the WHERE clause |
The difference is often the difference between a safe removal and a surprise. When a Batch has_many :students, dependent: :destroy, calling batch.destroy deletes the students first and then the batch; calling batch.delete
leaves the students in the table, pointing at a batch that no longer exists. With no foreign key on
students.batch_id, nothing refuses. With one, which the migrations lesson adds, the database refuses the DELETE and
Active Record raises ActiveRecord::InvalidForeignKey.
Checkpoint
Check: Removing rows with destroy and delete
Practise: write the two methods a Batch needs
Copy save_vs_update_columns.rb into a new file. It already opens an in-memory SQLite database and defines a Batch
with a required name and a fee above zero. Add a last_class_on date and a nullable archived_at datetime to its
create_table, then build the two methods below and run the file with ruby after each step.
Batch.archive_stale!(cutoff)setsarchived_attoTime.currenton every batch whose last class came beforecutoffand that is not archived yet, and returns how many it archived. Each archived batch must pass its validations and get a newupdated_at, so pick the write method with the save-method table in mind.Batch#raise_fee!(paise)addspaise, which may be negative, tofee_paiseand saves. A change that would leave the fee at zero or below raisesActiveRecord::RecordInvalidand leaves the row as it was; a good change movesupdated_at.- Check both with
puts. Create three batches with different last-class dates, archive with a cutoff that falls between them and print the count. Then callraise_fee!(-20_00_000)on a fee of 15,00,000 paise inside abeginandrescue, and printBatch.find(id).fee_paiseto confirm that the database still holds 1500000.
When both methods behave, the self-check goes back over every section:
Practice that adapts to you
Practise this lesson
10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: how do save and save! differ, and which case does each fit?
Both save the record if it is valid, run the same before_save and after_save callbacks, and touch updated_at.
They differ in what happens on an invalid record. save returns false and fills errors.full_messages, so a
controller can render the form again with the messages; the surrounding transaction continues. save! raises
ActiveRecord::RecordInvalid, which rolls back the transaction, so a job or a script stops at the first bad row
instead of carrying on with a half-finished change. Use save in a controller that reads from a form (the view
reads the errors), and save! in a background job, a seed, a migration or an inner method that must not swallow a
failure. The same pair exists for update and update!, and for create and create!, with the same meaning.
Two questions in the module bank build on this lesson. Decide your own answer first, then open each one:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A controller receives a batch fee from an admin's form. A nightly job normalises every batch (trims whitespace in name, rebuilds a derived field). Pick the persistence method for each case, and defend each choice against the four methods you did not pick.
-
Mid to senior
A Batch has_many :students, dependent: :destroy. A developer writes batch.delete to make an admin page faster. What does the shortcut do that batch.destroy would not, and how do you make sure the shortcut never runs in production?
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
A form sends "22" for the integer column seats. What does the model hold after assignment?
The Integer 22. Active Record reads the column types from the database and casts each value the moment it is assigned to the attribute.
-
What do find, find_by and where each give back when nothing matches?
find raises ActiveRecord::RecordNotFound, which Rails turns into a 404. find_by returns nil. where returns an empty, lazy relation that sends no SQL until something asks for its rows.
-
How do save and save! behave on an invalid record, and where does each belong?
save returns false and fills errors, so a controller can show the form again with the messages. save! raises ActiveRecord::RecordInvalid, so a job, a seed or a script stops at the first bad row. Both run validations and callbacks and touch updated_at.
-
What do update_columns and update_all leave out, and when is that acceptable?
They skip validations and callbacks and leave updated_at untouched. Use them only on data your own code controls, never on values that came from a user.
-
How does destroy differ from delete on a record?
destroy loads the row first and runs callbacks and the dependent: rules of its associations. delete sends one DELETE with none of that, so it suits only a row with no associations and nothing to clean up.
-
Which methods tell you what changed on a record before a save, and which after it?
Before the save, changed? and changes. After it, saved_change_to_<attribute>? and previous_changes.
Key takeaways
- Active Record reads a table’s columns from the database and gives the model a typed getter and setter for each one,
so
science.seats = "22"becomes the Integer 22 the moment it is assigned. findraisesActiveRecord::RecordNotFound(which Rails turns into a 404),find_byreturnsnil, andwherereturns a lazy Relation that runs no SQL until a method asks for its rows.save,save!andupdaterun validations and callbacks and touchupdated_at;savereturnsfalseon an invalid record andsave!raisesActiveRecord::RecordInvalid.update_columnsandupdate_allskip validations and callbacks and keepupdated_atas it was, so they belong on data your own code already owns, never on input from a user.destroyloads the row and runs callbacks anddependent:rules;deleteissues one DELETE with none of that, so it only fits a row with no associations and no bookkeeping to run.- Dirty tracking tells code what moved:
changed?andchangesbefore a save,saved_change_to_*?andprevious_changesafter one.
Terms in this lesson
- Active Record
- Rails' object-relational mapper: one Ruby class holds the rules, queries and lifecycle of one database table, and the table's columns become typed attributes of the class.
- attribute
- A getter and setter on an Active Record model that reads and writes one column of its table, with the column's type and the model's rules around it.
- CRUD
- The four basic data operations on a record: create (insert a row), read (fetch rows), update (change columns of a row) and delete (remove a row).
- dirty tracking
- Active Record's record of which attributes a model has changed since it was loaded or last saved, read with changed?, changes, saved_change_to_<name>? and previous_changes.
- type casting
- Converting a value to the type a column expects when it is assigned to an attribute, so a string from a request becomes the Integer, Date or Boolean the schema holds.
References
- Active Record Basics (Ruby on Rails)
- Active Record Validations (Ruby on Rails)
- ActiveRecord::Persistence (Ruby on Rails)
- Active Record Query Interface (Ruby on Rails)
- ActiveModel::Dirty (Ruby on Rails)
- Ruby on Rails 8.1 release notes (Ruby on Rails)
- ActiveRecord::AttributeMethods (Ruby on Rails)
- ActiveRecord::Relation (Ruby on Rails)
- New-app production.rb template (Rails 8.1.4 source) (Ruby on Rails)
Report a problem with this lesson
Kept only in this browser. Your Learn progress