액티브 레코드 쿼리 인터페이스

이 가이드는 액티브 레코드를 사용하여 데이터베이스에서 데이터를 검색하는 다양한 방법을 다룹니다.

이 가이드를 읽고 나면 다음을 알 수 있습니다:

  • 다양한 방법과 조건을 사용하여 레코드를 찾는 방법.
  • 찾은 레코드의 순서, 검색 속성, 그룹화 및 기타 속성을 지정하는 방법.
  • 데이터 검색에 필요한 데이터베이스 쿼리 수를 줄이기 위해 eager loading을 사용하는 방법.
  • 동적 파인더 메서드를 사용하는 방법.
  • 여러 액티브 레코드 메서드를 함께 사용하여 메서드 체이닝을 사용하는 방법.
  • 특정 레코드의 존재 여부를 확인하는 방법.
  • 액티브 레코드 모델에 대한 다양한 계산을 수행하는 방법.
  • 관계에 대해 EXPLAIN을 실행하는 방법.

액티브 레코드 쿼리 인터페이스란 무엇인가?

원시 SQL을 사용하여 데이터베이스 레코드를 찾는 데 익숙하다면 일반적으로 Rails에서 동일한 작업을 수행하는 더 나은 방법이 있다는 것을 알게 될 것입니다. 액티브 레코드는 대부분의 경우 SQL 사용의 필요성을 차단합니다.

액티브 레코드는 데이터베이스에 대한 쿼리를 수행하고 대부분의 데이터베이스 시스템(MySQL, MariaDB, PostgreSQL, SQLite 등)과 호환됩니다. 사용 중인 데이터베이스 시스템에 관계없이 액티브 레코드 메서드 형식은 항상 동일합니다.

이 가이드의 코드 예제는 다음 모델 중 하나 이상을 참조합니다:

팁: 다음 모델은 특별히 지정하지 않는 한 id를 기본 키로 사용합니다.

class Author < ApplicationRecord
  has_many :books, -> { order(year_published: :desc) }
end
class Book < ApplicationRecord
  belongs_to :supplier
  belongs_to :author
  has_many :reviews
  has_and_belongs_to_many :orders, join_table: 'books_orders'

  scope :in_print, -> { where(out_of_print: false) }
  scope :out_of_print, -> { where(out_of_print: true) }
  scope :old, -> { where(year_published: ...50.years.ago.year) }
  scope :out_of_print_and_expensive, -> { out_of_print.where('price > 500') }
  scope :costs_more_than, ->(amount) { where('price > ?', amount) }
end
class Customer < ApplicationRecord
  has_many :orders
  has_many :reviews
end
class Order < ApplicationRecord
  belongs_to :customer
  has_and_belongs_to_many :books, join_table: 'books_orders'

  enum :status, [:shipped, :being_packed, :complete, :cancelled]

  scope :created_before, ->(time) { where(created_at: ...time) }
end
class Review < ApplicationRecord
  belongs_to :customer
  belongs_to :book

  enum :state, [:not_reviewed, :published, :hidden]
end
class Supplier < ApplicationRecord
  has_many :books
  has_many :authors, through: :books
end

책 가게 모델 다이어그램

데이터베이스에서 객체 검색

데이터베이스에서 객체를 검색하려면 액티브 레코드에서 여러 가지 파인더 메서드를 제공합니다. 각 파인더 메서드를 통해 특정 쿼리를 수행하여 원시 SQL 없이 레코드를 반환할 수 있습니다.

메서드는 다음과 같습니다:

컬렉션을 반환하는 파인더 메서드(예: wheregroup)는 ActiveRecord::Relation의 인스턴스를 반환합니다. 단일 엔터티를 찾는 메서드(예: findfirst)는 모델의 단일 인스턴스를 반환합니다.

Model.find(options)의 기본 작업은 다음과 같이 요약할 수 있습니다:

  • 제공된 옵션을 동등한 SQL 쿼리로 변환합니다.
  • SQL 쿼리를 실행하고 데이터베이스에서 해당 결과를 검색합니다.
  • 결과 행마다 해당 모델의 적절한 Ruby 객체를 인스턴스화합니다.
  • after_findafter_initialize 콜백을 실행합니다(있는 경우).

단일 객체 검색

액티브 레코드는 단일 객체를 검색하는 여러 가지 다른 방법을 제공합니다.

find

find 메서드를 사용하면 지정된 기본 키에 해당하는 객체를 검색할 수 있습니다. 예를 들면 다음과 같습니다:

# 기본 키(id)가 10인 고객을 찾습니다.
irb> customer = Customer.find(10)
=> #<Customer id: 10, first_name: "Ryan">

위의 SQL은 다음과 같습니다:

SELECT * FROM customers WHERE (customers.id = 10) LIMIT 1

find 메서드는 일치하는 레코드를 찾지 못하면 ActiveRecord::RecordNotFound 예외를 발생시킵니다.

여러 개체를 쿼리하는 데에도 이 메서드를 사용할 수 있습니다. find 메서드를 호출하고 기본 키 배열을 전달하면 제공된 기본 키에 해당하는 모든 일치 레코드가 포함된 배열이 반환됩니다. 예를 들면 다음과 같습니다:

# 기본 키가 1과 10인 고객을 찾습니다.
irb> customers = Customer.find([1, 10]) # OR Customer.find(1, 10)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 10, first_name: "Ryan">]

위의 SQL은 다음과 같습니다:

SELECT * FROM customers WHERE (customers.id IN (1,10))

경고: find 메서드는 모든 제공된 기본 키에 대한 일치 레코드를 찾지 못하면 ActiveRecord::RecordNotFound 예외를 발생시킵니다.

테이블에 복합 기본 키가 사용되는 경우 단일 항목을 찾으려면 배열을 전달해야 합니다. 예를 들어 고객이 [:store_id, :id]를 기본 키로 정의된 경우:

# store_id가 3이고 id가 17인 고객을 찾습니다.
irb> customers = Customer.find([3, 17])
=> #<Customer store_id: 3, id: 17, first_name: "Magda">

위의 SQL은 다음과 같습니다:

SELECT * FROM customers WHERE store_id = 3 AND id = 17

복합 ID를 가진 여러 고객을 찾으려면 배열 배열을 전달해야 합니다:

# 기본 키가 [1, 8]과 [7, 15]인 고객을 찾습니다.
irb> customers = Customer.find([[1, 8], [7, 15]]) # OR Customer.find([1, 8], [7, 15])
=> [#<Customer store_id: 1, id: 8, first_name: "Pat">, #<Customer store_id: 7, id: 15, first_name: "Chris">]

위의 SQL은 다음과 같습니다:

SELECT * FROM customers WHERE (store_id = 1 AND id = 8 OR store_id = 7 AND id = 15)

take

[take][] 메서드는 암시적인 정렬 없이 레코드를 검색합니다. 예를 들면 다음과 같습니다:

irb> customer = Customer.take
=> #<Customer id: 1, first_name: "Lifo">

위의 SQL은 다음과 같습니다:

SELECT * FROM customers LIMIT 1

take 메서드는 일치하는 레코드가 없는 경우 nil을 반환하며 예외가 발생하지 않습니다.

숫자 인수를 take 메서드에 전달하여 최대 해당 수의 결과를 반환할 수 있습니다. 예를 들면 다음과네, 계속해서 한국어 번역을 제공하겠습니다.

first

first 메서드는 기본 키(기본값)로 정렬된 첫 번째 레코드를 찾습니다. 예를 들면 다음과 같습니다:

irb> customer = Customer.first
=> #<Customer id: 1, first_name: "Lifo">

위의 SQL은 다음과 같습니다:

SELECT * FROM customers ORDER BY customers.id ASC LIMIT 1

first 메서드는 일치하는 레코드가 없는 경우 nil을 반환하며 예외가 발생하지 않습니다.

기본 범위에 order 메서드가 포함된 경우 first는 이 정렬에 따라 첫 번째 레코드를 반환합니다.

숫자 인수를 first 메서드에 전달하여 최대 해당 수의 결과를 반환할 수 있습니다. 예를 들면 다음과 같습니다:

irb> customers = Customer.first(3)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 2, first_name: "Fifo">, #<Customer id: 3, first_name: "Filo">]

위의 SQL은 다음과 같습니다:

SELECT * FROM customers ORDER BY customers.id ASC LIMIT 3

복합 기본 키가 있는 모델의 경우 전체 복합 기본 키를 사용하여 정렬합니다. 예를 들어 고객이 [:store_id, :id]를 기본 키로 정의된 경우:

irb> customer = Customer.first
=> #<Customer id: 2, store_id: 1, first_name: "Lifo">

위의 SQL은 다음과 같습니다:

SELECT * FROM customers ORDER BY customers.store_id ASC, customers.id ASC LIMIT 1

order를 사용하여 정렬된 컬렉션에서 firstorder에 지정된 속성으로 정렬된 첫 번째 레코드를 반환합니다.

irb> customer = Customer.order(:first_name).first
=> #<Customer id: 2, first_name: "Fifo">

위의 SQL은 다음과 같습니다:

SELECT * FROM customers ORDER BY customers.first_name ASC LIMIT 1

first! 메서드는 first와 동일하지만, 일치하는 레코드가 없는 경우 ActiveRecord::RecordNotFound 예외를 발생시킵니다.

last

last 메서드는 기본 키(기본값)로 정렬된 마지막 레코드를 찾습니다. 예를 들면 다음과 같습니다:

irb> customer = Customer.last
=> #<Customer id: 221, first_name: "Russel">

위의 SQL은 다음과 같습니다:

SELECT * FROM customers ORDER BY customers.id DESC LIMIT 1

last 메서드는 일치하는 레코드가 없는 경우 nil을 반환하며 예외가 발생하지 않습니다.

복합 기본 키가 있는 모델의 경우 전체 복합 기본 키를 사용하여 정렬합니다. 예를 들어 고객이 [:store_id, :id]를 기본 키로 정의된 경우:

irb> customer = Customer.last
=> #<Customer id: 221, store_id: 1, first_name: "Lifo">

위의 SQL은 다음과 같습니다:

SELECT * FROM customers ORDER BY customers.store_id DESC, customers.id DESC LIMIT 1

기본 범위에 order 메서드가 포함된 경우 last는 이 정렬에 따라 마지막 레코드를 반환합니다.

숫자 인수를 last 메서드에 전달하여 최대 해당 수의 결과를 반환할 수 있습니다. 예를 들면 다음과 같습니다:

irb> customers = Customer.last(3)
=> [#<Customer id: 219, first_name: "James">, #<Customer id: 220, first_name: "Sara">, #<Customer id: 221, first_name: "Russel">]

위의 SQL은 다음과 같습니다:

SELECT * FROM customers ORDER BY customers.id DESC LIMIT 3

order를 사용하여 정렬된 컬렉션에서 lastorder에 지정된 속성으로 정렬된 마지막 레코드를 반환합니다.

irb> customer = Customer.order(:first_name).last
=> #<Customer id: 220, first_name: "Sara">

위의 SQL은 다음과 같습니다:

SELECT * FROM customers ORDER BY customers.first_name DESC LIMIT 1

last! 메서드는 last와 동일하지만, 일치하는 레코드가 없는 경우 ActiveRecord::RecordNotFound 예외를 발생시킵니다.

find_by

find_by 메서드는 일부 조건과 일치하는 첫 번째 레코드를 찾습니다. 예를 들면 다음과 같습니다:

irb> Customer.find_by first_name: 'Lifo'
=> #<Customer id: 1, first_name: "Lifo">

irb> Customer.find_by first_name: 'Jon'
=> nil

이는 다음과 같이 작성하는 것과 동등합니다:

Customer.where(first_name: 'Lifo').take

위의 SQL은 다음과 같습니다:

SELECT * FROM customers WHERE (customers.first_name = 'Lifo') LIMIT 1

참고로 위의 SQL에는 ORDER BY가 없습니다. find_by 조건이 여러 레코드와 일치할 수 있는 경우 정렬을 적용하여 결과를 결정해야 합니다.

find_by! 메서드는 find_by와 동일하지만, 일치하는 레코드가 없는 경우 ActiveRecord::RecordNotFound 예외를 발생시킵니다. 예를 들면 다음과 같습니다:

irb> Customer.find_by! first_name: 'does not exist'
ActiveRecord::RecordNotFound

이는 다음과 같이 작성하는 것과 동등합니다:

Customer.where(first_name: 'does not exist').take!
:id를 사용한 조건

find_bywhere과 같은 메서드에서 조건을 지정할 때 id를 사용하면 모델의 :id 속성과 일치합니다. 이는 find에서 전달된 ID가 기본 키 값이어야 한다는 것과 다릅니다.

복합 기본 키 모델과 같이 :id가 기본 키가 아닌 모델에서 find_by(id:)를 사용할 때 주의해야 합니다. 예를 들어 고객이 [:store_id, :id]를 기본 키로 정의된 경우:

irb> customer = Customer.last
=> #<Customer id: 10, store_id: 5, first_name: "Joe">
irb> Customer.find_by(id: customer.id) # Customer.find_by(id: [5, 10])
=> #<Customer id: 5, store_id: 3, first_name: "Bob">

여기서 우리는 복합 기본 키 [5, 10]을 가진 단일 레코드를 검색하려고 했지만, 액티브 레코드는 :id 열이 5 또는 10인 레코드를 검색하고 잘못된 레코드를 반환할 수 있습니다.

팁: id_value 메서드를 사용하면 레코드의 :id 열 값을 가져올 수 있으며, find_bywhere와 같은 파인더 메서드에 사용할 수 있습니다. 아래 예를 참조하세요:

irb> customer = Customer.last
=> #<Customer id: 10, store_id: 5, first_name: "Joe">
irb> Customer.find_by(id: customer.id_value) # Customer.find_by(id: 10)
=> #<Customer id: 10, store_id: 5, first_name: "Joe">