require 'activerecord-import'
class BulkImportPostsService
BATCH_SIZE = 1000
def initialize(csv_file_path)
@csv_file_path = csv_file_path
end
def call
posts = []
imported_count = 0
CSV.foreach(@csv_file_path, headers: true) do |row|
posts << Post.new(
title: row['title'],
body: row['body'],
author_id: row['author_id'],
published_at: row['published_at']
)
if posts.size >= BATCH_SIZE
imported_count += import_batch(posts)
posts = []
end
end
# Import remaining posts
imported_count += import_batch(posts) if posts.any?
Result.success(imported_count: imported_count)
rescue StandardError => e
Rails.logger.error("Bulk import failed: #{e.message}")
Result.failure(error: e.message)
end
private
def import_batch(posts)
Post.import(
posts,
validate: false,
on_duplicate_key_update: {
conflict_target: [:id],
columns: [:title, :body, :updated_at]
}
)
posts.size
end
end
Inserting thousands of records one-by-one is prohibitively slow due to the overhead of individual INSERT statements. The activerecord-import gem provides bulk insert capabilities that compile multiple records into a single multi-row INSERT, dramatically improving throughput. I use this for data imports, batch processing, or seeding test environments. The gem supports validations, callbacks (optionally), and conflict resolution strategies like on_duplicate_key_update. For maximum performance, I disable validations and callbacks when data is already trusted. The trade-off is that bulk operations bypass some ActiveRecord conveniences like after_create callbacks, so I handle those concerns separately if needed. Monitoring insertion rates helps identify when bulk operations are worth the complexity.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
<form data-controller="query-sync" data-action="change->query-sync#apply">
<select name="status" class="rounded border p-2">
<option value="">Any</option>
<option value="open">Open</option>
<option value="closed">Closed</option>
</select>
Filter UI that syncs query params via Stimulus (no front-end router)
Share this code
Here's the card — post it anywhere.