Zenn
🚀

Query Serviceとは

2025/03/25に公開

Daily Blogging94日目

複数のリソースにまたがるデータの取得ロジックはどこに置けばいいんだろう

Query Service

DDDにおいて、特定のユースケースで使用する複数のリソースにまたがる複雑なデータの取得ロジックはQuery Serivceを利用するらしい。
CQRSのデータ参照部分を担う。

Repositoryを使用する場合、リソースごとのRepositoryを呼んでそのあとでデータを適宜加工する。みたいな処理が必要
これが面倒臭い
コードは複雑になり、別々に取得してくるのでクエリのパフォーマンスも悪くなる可能性がある。

Repositoryの場合

# User のリポジトリ
class UserRepository
  def find_by_id(user_id)
    User.find(user_id) # 単純な取得
  end
end

# Order のリポジトリ
class OrderRepository
  def find_by_user_id(user_id)
    Order.where(user_id: user_id).order(created_at: :desc).to_a
  end
end

これらをユースケース層で呼び出す。

class UserOrderApplicationService
  def initialize(user_repository = UserRepository.new, order_repository = OrderRepository.new)
    @user_repository = user_repository
    @order_repository = order_repository
  end

  def fetch_user_orders(user_id)
    user = @user_repository.find_by_id(user_id) # ユーザー情報取得
    orders = @order_repository.find_by_user_id(user_id) # 注文一覧取得

    {
      user_id: user.id,
      user_name: user.name,
      orders: orders.map do |order|
        {
          order_id: order.id,
          total_price: order.total_price,
          order_date: order.created_at
        }
      end
    }
  end
end

Query Serviceを使うと

こんな感じ

class UserOrderQueryService
  def initialize(db_connection = ActiveRecord::Base.connection)
    @db_connection = db_connection
  end

  def fetch_user_orders(user_id)
    sql = <<-SQL
      SELECT users.id AS user_id, users.name AS user_name, 
             orders.id AS order_id, orders.total_price, orders.created_at AS order_date
      FROM users
      LEFT JOIN orders ON users.id = orders.user_id
      WHERE users.id = ?
      ORDER BY orders.created_at DESC
    SQL

    result = @db_connection.exec_query(sql, "SQL", [[nil, user_id]])

    orders = result.map do |row|
      OrderReadModel.new(row["order_id"], row["total_price"], row["order_date"])
    end

    first_row = result.first
    UserOrderReadModel.new(first_row["user_id"], first_row["user_name"], orders)
  end
end

Data Transfer Object

class UserOrderReadModel
  attr_reader :user_id, :user_name, :orders

  def initialize(user_id, user_name, orders)
    @user_id = user_id
    @user_name = user_name
    @orders = orders
  end
end

class OrderReadModel
  attr_reader :order_id, :total_price, :order_date

  def initialize(order_id, total_price, order_date)
    @order_id = order_id
    @total_price = total_price
    @order_date = order_date
  end
end

呼び出し元はこう

class UserOrderApplicationService
  def initialize(user_order_query_service = UserOrderQueryService.new)
    @user_order_query_service = user_order_query_service
  end

  def fetch_user_orders(user_id)
    @user_order_query_service.fetch_user_orders(user_id)
  end
end

シンプル!

Discussion

ログインするとコメントできます