query-object
Generate query objects for complex database queries. Use when queries are complex, reusable, need filtering/sorting, or says "complex query", "search", "filter posts", "advanced search", "query builder", "scope is too complex".
安装与使用
请根据来源仓库和你的 AI 工具说明安装此技能。先阅读技能文件和依赖要求,再添加到工作流。
查看来源 ↗使用指南技能说明
Query Object Generator
Extract complex database queries into reusable query objects for better organization and performance.
Quick Start
Task Progress (copy and check off):
- [ ] 1. Identify complex query to extract
- [ ] 2. Create query class
- [ ] 3. Move query logic
- [ ] 4. Add chainable methods
- [ ] 5. Use in controller
- [ ] 6. Test query performance
When to Use Query Objects
✅ Good candidates:
- Complex WHERE conditions
- Multiple JOINs
- Reusable search/filter logic
- Queries with many optional filters
- Performance-critical queries
- Queries spanning multiple models
❌ Don't use for:
- Simple
Model.where(status: :active) - One-time queries
- Basic associations
Query Structure
app/queries/
├── application_query.rb # Base class
├── posts_query.rb
├── users_query.rb
├── job_posts_query.rb
└── search/
├── post_search_query.rb
└── user_search_query.rb
Base Query Class
# app/queries/application_query.rb
class ApplicationQuery
def initialize(relation = nil)
@relation = relation
end
def call
@relation
end
alias_method :resolve, :call
private
attr_reader :relation
end
Basic Query Template
# app/queries/example_query.rb
class ExampleQuery < ApplicationQuery
def initialize(relation = Model.all)
super(relation)
end
def call
@relation
.where(conditions)
.joins(associations)
.order(ordering)
end
private
def conditions
# WHERE conditions
end
def associations
# JOIN logic
end
def ordering
{ created_at: :desc }
end
end
Usage:
# In controller
@results = ExampleQuery.new.call
# Or with relation
@results = ExampleQuery.new(Model.active).call
Common Patterns
1. Posts Search Query
# app/queries/posts_query.rb
class PostsQuery < ApplicationQuery
def initialize(relation = Post.includes(:user))
super(relation)
@filters = {}
end
def call
apply_filters
@relation
end
# Chainable filter methods
def search(term)
return self if term.blank?
@filters[:search] = term
self
end
def by_status(status)
return self if status.blank?
@filters[:status] = status
self
end
def by_user(user_id)
return self if user_id.blank?
@filters[:user_id] = user_id
self
end
def published_only
@filters[:published] = true
self
end
def recent
@filters[:recent] = true
self
end
def popular(min_likes = 10)
@filters[:popular] = min_likes
self
end
private
def apply_filters
apply_search_filter
apply_status_filter
apply_user_filter
apply_published_filter
apply_recent_filter
apply_popular_filter
end
def apply_search_filter
return unless @filters[:search]
term = @filters[:search]
@relation = @relation.where(
"title LIKE ? OR content LIKE ?",
"%#{term}%",
"%#{term}%"
)
end
def apply_status_filter
return unless @filters[:status]
@relation = @relation.where(status: @filters[:status])
end
def apply_user_filter
return unless @filters[:user_id]
@relation = @relation.where(user_id: @filters[:user_id])
end
def apply_published_filter
return unless @filters[:published]
@relation = @relation.where(status: :published)
end
def apply_recent_filter
return unless @filters[:recent]
@relation = @relation.where("created_at > ?", 7.days.ago)
.order(created_at: :desc)
end
def apply_popular_filter
return unless @filters[:popular]
@relation = @relation.where("likes_count >= ?", @filters[:popular])
.order(likes_count: :desc)
end
end
Controller Usage:
class PostsController < ApplicationController
def index
@posts = PostsQuery.new
.search(params[:q])
.by_status(params[:status])
.published_only
.recent
.call
.page(params[:page])
end
end
2. Advanced Search Query
# app/queries/search/post_search_query.rb
module Search
class PostSearchQuery < ApplicationQuery
def initialize(params = {})
super(Post.includes(:user))
@params = params
end
def call
@relation = filter_by_keyword
@relation = filter_by_category
@relation = filter_by_date_range
@relation = filter_by_likes
@relation = sort_results
@relation
end
private
def filter_by_keyword
keyword = @params[:keyword]
return @relation if keyword.blank?
@relation.where(
"LOWER(title) LIKE :term OR LOWER(content) LIKE :term",
term: "%#{keyword.downcase}%"
)
end
def filter_by_category
category = @params[:category]
return @relation if category.blank?
@relation.where(category: category)
end
def filter_by_date_range
start_date = @params[:start_date]
end_date = @params[:end_date]
return @relation if start_date.blank? || end_date.blank?
@relation.where(created_at: start_date..end_date)
end
def filter_by_likes
min_likes = @params[:min_likes]&.to_i || 0
@relation.where("likes_count >= ?", min_likes)
end
def sort_results
sort_by = @params[:sort_by] || "recent"
case sort_by
when "popular"
@relation.order(likes_count: :desc, created_at: :desc)
when "commented"
@relation.order(comments_count: :desc, created_at: :desc)
else # recent
@relation.order(created_at: :desc)
end
end
end
end
3. Job Posts Filter Query
# app/queries/job_posts_query.rb
class JobPostsQuery < ApplicationQuery
VALID_CATEGORIES = %w[development design planning marketing].freeze
VALID_TYPES = %w[short_term long_term].freeze
VALID_STATUSES = %w[open closed].freeze
def initialize(relation = JobPost.includes(:user))
super(relation)
end
def by_category(category)
return self unless VALID_CATEGORIES.include?(category)
@relation = @relation.where(category: category)
self
end
def by_project_type(type)
return self unless VALID_TYPES.include?(type)
@relation = @relation.where(project_type: type)
self
end
def by_status(status)
return self unless VALID_STATUSES.include?(status)
@relation = @relation.where(status: status)
self
end
def open_only
@relation = @relation.where(status: :open)
self
end
def with_budget
@relation = @relation.where.not(budget: nil)
self
end
def budget_range(min, max)
return self if min.blank? || max.blank?
@relation = @relation.where(
"CAST(REPLACE(REPLACE(budget, '만원', ''), ',', '') AS INTEGER) BETWEEN ? AND ?",
min,
max
)
self
end
def recent(days = 7)
@relation = @relation.where("created_at > ?", days.days.ago)
self
end
def popular(min_views = 100)
@relation = @relation.where("views_count >= ?", min_views)
self
end
def call
@relation.order(created_at: :desc)
end
end
4. User Analytics Query
# app/queries/user_analytics_query.rb
class UserAnalyticsQuery < ApplicationQuery
def initialize(relation = User.all)
super(relation)
end
def active_users(days = 30)
@relation = @relation.where("last_sign_in_at > ?", days.days.ago)
self
end
def with_posts
@relation = @relation.where("posts_count > 0")
self
end
def top_contributors(limit = 10)
@relation = @relation.order(posts_count: :desc).limit(limit)
self
end
def by_role(role_title)
return self if role_title.blank?
@relation = @relation.where(role_title: role_title)
self
end
def joined_between(start_date, end_date)
@relation = @relation.where(created_at: start_date..end_date)
self
end
def call
@relation
end
end
Hash-Based Parameters
# app/queries/flexible_query.rb
class FlexibleQuery < ApplicationQuery
def initialize(relation = Model.all, filters = {})
super(relation)
@filters = filters
end
def call
@relation = apply_filters(@relation, @filters)
@relation
end
private
def apply_filters(relation, filters)
filters.each do |key, value|
next if value.blank?
relation = case key.to_sym
when :status
relation.where(status: value)
when :user_id
relation.where(user_id: value)
when :search
relation.where("title LIKE ?", "%#{value}%")
when :min_date
relation.where("created_at >= ?", value)
when :max_date
relation.where("created_at <= ?", value)
else
relation
end
end
relation
end
end
Usage:
filters = {
status: params[:status],
user_id: params[:user_id],
search: params[:q],
min_date: params[:from]
}
@results = FlexibleQuery.new(Post.all, filters).call
Performance Optimization
Eager Loading
class OptimizedPostsQuery < ApplicationQuery
def initialize
super(Post.includes(:user, :comments, :likes))
end
def call
@relation.order(created_at: :desc)
end
end
Select Specific Columns
class LightweightPostsQuery < ApplicationQuery
def call
@relation.select(:id, :title, :created_at, :user_id, :likes_count)
end
end
Use Counter Cache
class PopularPostsQuery < ApplicationQuery
def call
# Uses counter_cache, no COUNT query
@relation.where("likes_count > 10")
.order(likes_count: :desc)
end
end
Batch Processing
class BatchQuery < ApplicationQuery
def find_each_batch(batch_size = 1000)
@relation.find_each(batch_size: batch_size) do |record|
yield record
end
end
end
Testing
# test/queries/posts_query_test.rb
require "test_helper"
class PostsQueryTest < ActiveSupport::TestCase
test "filters by search term" do
post1 = create(:post, title: "Ruby on Rails")
post2 = create(:post, title: "Python Django")
post3 = create(:post, title: "Rails Tutorial")
results = PostsQuery.new.search("Rails").call
assert_includes results, post1
assert_includes results, post3
assert_not_includes results, post2
end
test "filters by status" do
draft = create(:post, status: :draft)
published = create(:post, status: :published)
results = PostsQuery.new.by_status(:published).call
assert_includes results, published
assert_not_includes results, draft
end
test "chains multiple filters" do
user = create(:user)
post1 = create(:post, user: user, status: :published, likes_count: 20)
post2 = create(:post, user: user, status: :draft)
post3 = create(:post, status: :published, likes_count: 5)
results = PostsQuery.new
.by_user(user.id)
.published_only
.popular(10)
.call
assert_includes results, post1
assert_not_includes results, post2
assert_not_includes results, post3
end
test "returns correct SQL query" do
query = PostsQuery.new.search("test").by_status(:published)
sql = query.call.to_sql
assert_match /title LIKE/, sql
assert_match /status/, sql
end
end
Best Practices
-
Chainable Methods: Return
selffor method chaining - Default Includes: Eager load associations by default
- Validation: Validate filter parameters
- Performance: Use indexes, counter caches, select columns
- Immutability: Don't modify input relation
- Explicitness: Clear method names (by_status, not filter)
- Testing: Test each filter and combinations
Integration Examples
Controller with Pagination
class PostsController < ApplicationController
def index
@posts = PostsQuery.new
.search(params[:q])
.by_status(params[:status])
.recent
.call
.page(params[:page])
.per(20)
end
end
Background Job
class AnalyticsJob < ApplicationJob
def perform
users = UserAnalyticsQuery.new
.active_users(30)
.with_posts
.call
users.find_each do |user|
calculate_stats(user)
end
end
end
API Endpoint
module Api
module V1
class PostsController < BaseController
def index
@posts = PostsQuery.new
.by_status(params[:status])
.recent
.call
.limit(50)
render json: { data: @posts.map { |p| post_json(p) } }
end
end
end
end
Checklist
- Query class created in app/queries/
- Inherits from ApplicationQuery
- Chainable filter methods
- Eager loading configured
- Returns relation (not array)
- Tested with various filters
- Performance optimized
- Used in controller/service
使用示例
查看来源仓库中的使用示例与配置说明。
查看来源 ↗来源历史
- 收录时间
- 2026-01-20
- 索引更新时间
- 2026-01-26
- Branch
- main
收录与同步日期属于本站记录。源码修改历史请查看来源仓库。
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!