← Back to list

query-object
by GangWooLee
⭐ 0🍴 0📅 Jan 25, 2026
SKILL.md
name: query-object description: 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".
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
Score
Total Score
50/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon