Features Guide
This guide highlights the features that are implemented in Crecto today and how to use them safely.
Schema Definition
Models extend Crecto::Model and describe their table structure inside a schema block. Supported field types include the Crystal primitives listed in src/crecto/schema.cr, plus JSON, UUID, nullable variants, and arrays of those types.
class Product < Crecto::Model
schema "products" do
field :name, String
field :price, Float64
field :active, Bool, default: true
field :metadata, Json?
field :sku, UUID?
belongs_to :category
has_many :inventory_entries, InventoryEntry
end
validate_required [:name, :price]
endAssociations support:
belongs_to :categoryhas_many :inventory_entries, InventoryEntryhas_one :profilehas_many :tags, Tag, through: :inventory_entries
Many-to-many helpers are not generated automatically; using has_many ... through: is the recommended pattern.
Validations and Changesets
Validations are declared on the model class and run when you build a changeset.
class User < Crecto::Model
schema "users" do
field :name, String
field :email, String
field :age, Int32?
end
validate_required [:name, :email]
validate_format :email, /^[^@\s]+@[^@\s]+\.[^@\s]+$/
validate_length :name, min: 2
unique_constraint :email
end
user = User.new
changeset = User.changeset(user)
changeset.valid? # => false
changeset.errors # => [{"name", "is required"}, {"email", "is required"}]Keyword initialization uses the same casting pipeline, so User.new(name: "Ada") assigns attributes before you build a changeset.
Per-record validations run every time a changeset is instantiated. Mutating repository calls (insert, update, delete) require a valid changeset.
CRUD Operations
Query = Crecto::Repo::Query
# Create
user = User.new.tap do |u|
u.name = "Sandi"
u.email = "[email protected]"
end
changeset = User.changeset(user)
insert_result = Repo.insert(changeset)
# Update
if insert_result.valid?
saved_user = insert_result.instance
saved_user.name = "Sandi Metz"
update_changeset = User.changeset(saved_user)
Repo.update(update_changeset)
end
# Delete
Repo.delete(User.changeset(user))
# Fetch
Repo.get(User, 1)
Repo.get_by(User, email: "[email protected]")
Repo.all(User, Query.where(active: true).order_by("name ASC"))Remember that Repo.insert/update return Changeset objects. Read operations return model instances directly.
Query Builder Tips
Crecto::Repo::Query supports where, or_where, string fragments with parameters, ordering, limiting, joining, preloading, and grouping.
recent_posts = Query
.where(published: true)
.where("published_at > ?", [Time.utc - 30.days])
.order_by("published_at DESC")
.limit(10)
Repo.all(Post, recent_posts.preload(:author))To preload multiple associations:
Repo.all(User,
Query.preload(:posts, Query.where(published: true))
.preload(:profile)
)Bulk Operations
Use Repo.insert_all to insert many records efficiently.
records = (1..3).map do |n|
{name: "Batch User #{n}", email: "batch#{n}@example.com"}
end
result = Repo.insert_all(User, records)
result.successful_count # number of inserted records
result.failed_count # zero if all succeeded
result.errors # details for records that failed validation/DB checksRepo.update_all and Repo.delete_all accept a Query to target matching rows:
inactive = Query.where(active: false)
Repo.update_all(User, inactive, {archived_at: Time.utc})
Repo.delete_all(User, Query.where("archived_at < ?", [Time.utc - 90.days]))Transactions
Group operations with Crecto::Multi or use a live transaction when you need immediate feedback.
multi = Crecto::Multi.new
multi.insert(User.changeset(user))
multi.insert(Profile.changeset(profile))
result = Repo.transaction(multi)
if result.errors.empty?
puts "Batch committed"
else
pp result.errors
endRepo.transaction! do |tx|
tx.insert!(User.changeset(user))
tx.insert!(Profile.changeset(profile))
endLive transactions yield the same changeset objects returned by the repository. Rolling back is automatic when an exception is raised inside the block.
Raw SQL Escape Hatch
When the query builder is too limiting, reach for Repo.query or Repo.raw_exec.
Repo.query("UPDATE users SET last_login = ? WHERE id = ?", [Time.utc, 1])
Repo.raw_query("SELECT COUNT(*) FROM users") do |rs|
rs.each { puts rs.read(Int64) }
endThese helpers expect you to close the result set when you work with the lower-level DB::ResultSet API directly.