Advanced Patterns
The existing codebase already covers a few patterns that help when applications grow beyond basic CRUD. This document focuses on what is implemented today.
Coordinating Work with Multi
Crecto::Multi batches operations that should be executed inside a single transaction. It is best suited for independent changes where you only need to know whether the batch succeeded.
multi = Crecto::Multi.new
multi.insert(User.changeset(user))
multi.insert(Profile.changeset(profile))
multi.update(Post.changeset(post))
result = Repo.transaction(multi)
if result.errors.empty?
puts "Batch committed"
else
pp result.errors
endMulti validates all queued changesets before opening a transaction. If any are invalid, the transaction will be skipped and multi.errors will contain the validation messages.
Immediate Feedback with transaction!
Repo.transaction! yields a Crecto::LiveTransaction, which forwards calls to the repository while reusing the same low-level DB::Transaction.
Repo.transaction! do |tx|
user_result = tx.insert(User.changeset(user))
raise "user invalid" unless user_result.valid?
profile = Profile.new.tap { |p| p.user_id = user_result.instance.id }
profile_result = tx.insert(Profile.changeset(profile))
raise "profile invalid" unless profile_result.valid?
# raise will rollback automatically
endEvery helper on LiveTransaction returns either a changeset or a bulk result—there is no special casing beyond sharing the connection.
Pagination and Batching
Large result sets should be processed in slices. Repo.all accepts limit and offset, so you can build a loop that paginates manually.
Query = Crecto::Repo::Query
offset = 0
batch_size = 500
loop do
batch = Repo.all(User, Query.limit(batch_size).offset(offset))
break if batch.empty?
batch.each { |user| process(user) }
offset += batch_size
endThis approach keeps memory usage predictable even without a streaming cursor API.
Association Preloading
Preloads fetch related rows in follow-up queries and attach them to existing models. Only has_many, has_one, belongs_to, and has_many ... through: associations are supported.
users = Repo.all(User,
Query.preload(:posts, Query.where(published: true))
.preload(:profile)
)
users.each do |user|
puts user.profile?.try(&.bio) if user.profile?
user.posts?.try(&.each) { |post| puts post.title }
endAttempting to access an association that was not preloaded raises Crecto::AssociationNotLoaded.
Bulk Inserts with Error Reporting
Repo.insert_all aggregates validation failures and database errors so you can handle partial success gracefully.
records = [
{name: "Alice", email: "[email protected]"},
{name: "Bob", email: "[email protected]"},
{name: "Invalid", email: "broken"} # fails validation
]
result = Repo.insert_all(User, records)
puts "Inserted #{result.successful_count} / #{result.total_count}"
result.errors.each do |error|
puts "Index #{error.index} failed: #{error.error_message}"
pp error.validation_errors
endThe adapters insert valid records in bulk and fall back to per-record inserts when a database error occurs, mirroring the behaviour in src/crecto/adapters/*_adapter.cr.
These are the patterns supported by the current implementation. Features such as optimistic locking, cursor streaming, or explicit savepoint management are on the roadmap but not yet shipped, so they are intentionally excluded here.