Rails models without the whole app
Installing the pieces
ActiveRecord is a gem on its own: a script can use it without Rails, a router or a view. The database driver is a second gem, and SQLite is the one that needs no server:
gem install activerecord sqlite3
8 gems installed
These commands ran on the host in the ruby:3.4 image: ActiveRecord depends on bigdecimal, which builds a C extension, and this container has no compiler. With a toolchain the install is one line and the script below runs as written.
A migration
A migration is a class with a change method that describes the table, and migrate(:up) applies it. The version in the class name is the Rails version the migration was written for:
require "active_record"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: "db/development.sqlite3")
class CreateTodos < ActiveRecord::Migration[8.0]
def change
create_table :todos do |t|
t.string :title, null: false
t.boolean :done, default: false
t.timestamps
end
end
end
CreateTodos.new.migrate(:up)
t.timestamps adds created_at and updated_at, which ActiveRecord fills on save. null: false and the default are part of the schema, not of the model; the database enforces them for every writer.
The migration, the model and the queries are one file, db.rb, in that order: connection, table, class, then the calls. Before the run, create the folder the database lives in. The SQLite adapter does not create it, and its own FileUtils.mkdir_p call fails with uninitialized constant ...::FileUtils when the folder is missing:
mkdir db
The model
The model is a class named after the table, singular, and it inherits from ActiveRecord::Base. The validation is a declaration, and valid? is what a caller checks before a save:
class Todo < ActiveRecord::Base
validates :title, presence: true
end
create! raises when the validation fails; create returns an unsaved object with errors. A web layer uses the second, because the errors belong on the form, not in a 500.
Queries and validations
The query methods return relations that are lazy until they are enumerated, and pluck asks only for the columns:
Todo.create!(title: "write the lesson")
Todo.create!(title: "ship it", done: true)
p Todo.count
p Todo.where(done: false).pluck(:title)
todo = Todo.new
p todo.valid?
p todo.errors[:title]
Run it on the host:
ruby db.rb
== CreateTodos: migrating ==================================================== -- create_table(:todos) -> 0.1717s == CreateTodos: migrated (0.1718s) =========================================== 2 ["write the lesson"] false ["can't be blank"]
The migration runs once, then the four answers: two rows, the one that is not done, an invalid new record and the validation message. The error text comes from the validation, and errors.full_messages adds the attribute name for a person to read.
where, order, limit and joins chain in any order and produce one SQL statement. Reading the log with ActiveRecord::Base.logger = Logger.new($stdout) shows the statement, which is the fastest way to learn what a chain costs.
What to watch for
A standalone script must require the adapter and
call establish_connection before the first query.
A model defined before the connection works until it touches the
database, then fails with a message that names the missing connection.
update_all and delete_all
skip validations and callbacks. Use them for a migration or a cleanup
where that is the point, and use the instance methods when the model
rules must apply.
Next: Rails controllers and views
Article author: Arthur Isaev