Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Rails에서 임의의 순서대로 데이터베이스 레코드 조회하는 방법

Rails에서는 ID만 있으면 데이터베이스에서 여러 레코드를 손쉽게 가져올 수 있습니다.

Person.where(id: [1, 2, 3]).map(&:id) # => [1, 2, 3]

그런데 만약 레코드를 다른 순서로 받아오고 싶다면 어떻게 해야 할까요? 예를 들어 검색 엔진이 관련도가 높은 순서대로 ID를 반환한다고 가정해 봅시다. 그렇다면 그 순서를 그대로 유지한 채 레코드를 가져오려면 어떻게 해야 할까요?

단순히 where를 다시 사용해 볼 수도 있습니다.

Person.where(id: [2, 1, 3]).map(&:id) # => [1, 2, 3]

하지만 결과는 전혀 다르지 않습니다. 데이터베이스는 ID 배열에 담긴 순서가 아니라 자체적인 기준으로 결과를 반환하기 때문입니다. 그렇다면 원하는 순서대로 레코드를 가져오려면 어떻게 해야 할까요?

호환성 있는 방법: CASE 문 활용

Ruby와 마찬가지로 SQL 역시 CASE...WHEN 문을 지원합니다.

자주 쓰이지는 않지만, CASE...WHEN 문은 거의 해시처럼 동작할 수 있습니다. 하나의 값을 다른 값으로 매핑할 수 있는 것입니다.

case :b
when :a then 1
when :b then 2
when :c then 3
end # => 2

이 case 문은 사실 아래 해시와 비슷하게 생겼습니다.

{
  :a => 1,
  :b => 2,
  :c => 3
}[:b] # => 2

즉, 키를 원하는 출력 순서에 매핑하는 방법이 있는 셈입니다. 그리고 데이터베이스는 이 임의의 순서를 기준으로 결과를 정렬해 반환할 수 있습니다.

이를 응용하면 ID와 해당 위치를 CASE 문으로 만들어 SQL의 ORDER BY 절에 넣을 수 있습니다.

예를 들어 객체를 [2, 1, 3] 순서로 반환받고 싶다면 SQL은 다음과 같이 작성할 수 있습니다.

SELECT * FROM people
  WHERE id IN (1, 2, 3)
  ORDER BY CASE id
    WHEN 2 THEN 0
    WHEN 1 THEN 1
    WHEN 3 THEN 2
    ELSE 3 END;

이렇게 하면 레코드가 원하는 순서대로 반환됩니다. CASE 문이 각 ID를 반환되어야 할 순서 값으로 변환해 주기 때문입니다.

물론 이 SQL을 직접 보면 다소 어색해 보일 수 있습니다. 그리고 이런 절을 일일이 손으로 작성하는 것이 얼마나 번거로울지 상상할 수 있을 겁니다.

하지만 손으로 직접 만들 필요는 없습니다. 그래서 Ruby가 있는 것입니다.

lib/extensions/active_record/find_by_ordered_ids.rb
module Extensions::ActiveRecord::FindByOrderedIds
  extend ActiveSupport::Concern
  module ClassMethods
    def find_ordered(ids)
      order_clause = "CASE id "
      ids.each_with_index do |id, index|
        order_clause << sanitize_sql_array(["WHEN ? THEN ? ", id, index])
      end
      order_clause << sanitize_sql_array(["ELSE ? END", ids.length])
      where(id: ids).order(order_clause)
    end
  end
end

ActiveRecord::Base.include(Extensions::ActiveRecord::FindByOrderedIds)

Person.find_ordered([2, 1, 3]) # => [2, 1, 3]

원하던 대로 정확히 동작합니다!

더 깔끔한 방법: MySQL 전용 FIELD() 함수

MySQL을 사용하고 있다면 더 깔끔한 방법이 있습니다. MySQL에는 ORDER BY FIELD라는 특별한 문법이 존재합니다.

SELECT * FROM people
WHERE id IN (1, 2, 3)
ORDER BY FIELD(id, 2, 1, 3);

이 역시 Ruby 코드로 생성할 수 있습니다.

lib/extensions/active_record/find_by_ordered_ids.rb
module Extensions::ActiveRecord::FindByOrderedIds
  extend ActiveSupport::Concern
  module ClassMethods
    def find_ordered(ids)
      sanitized_id_string = ids.map {|id| connection.quote(id)}.join(",")
      where(id: ids).order("FIELD(id, #{sanitized_id_string})")
    end
  end
end

ActiveRecord::Base.include(Extensions::ActiveRecord::FindByOrderedIds)

따라서 MySQL을 사용 중이고 데이터베이스 호환성을 크게 신경 쓰지 않아도 된다면, 이 방법이 좋은 선택입니다. 로그에서 SQL 문을 확인할 때도 훨씬 읽기 편하다는 장점이 있습니다.

마무리

레코드를 특정한 임의의 순서로 표시해야 할 때, 굳이 Ruby에서 정렬할 필요는 없습니다. 간단한 코드 조각 몇 줄이면 충분합니다. 데이터베이스에게 찾기, 정렬, 반환이라는 본래 잘하는 일을 맡기세요. 애플리케이션 코드는 더 단순해지고, 성능도 함께 개선될 것입니다.