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