1 Active Record 쿼리 인터페이스란 무엇인가?
데이터베이스 레코드를 찾기 위해 raw SQL을 사용하는 데 익숙하다면, Rails에서 동일한 작업을 수행하는 더 나은 방법이 있다는 것을 알게 될 것입니다. Active Record는 대부분의 경우 SQL을 사용할 필요성으로부터 여러분을 보호합니다.
Active Record는 데이터베이스에 대한 쿼리를 대신 수행하며 MySQL, MariaDB, PostgreSQL, SQLite를 포함한 대부분의 데이터베이스 시스템과 호환됩니다. 어떤 데이터베이스 시스템을 사용하든 Active Record 메서드 형식은 항상 동일합니다.
이 가이드의 코드 예제들은 다음 모델들 중 하나 이상을 참조할 것입니다:
다음의 모든 모델은 따로 지정하지 않는 한 id
를 primary key로 사용합니다.
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) } # 50년 이상 된 책
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
2 데이터베이스에서 객체 검색하기
데이터베이스에서 객체를 검색하기 위해 Active Record는 여러 finder 메서드를 제공합니다. 각 finder 메서드를 사용하면 raw SQL을 작성하지 않고도 데이터베이스에서 특정 쿼리를 수행할 수 있도록 인자를 전달할 수 있습니다.
제공되는 메서드는 다음과 같습니다:
annotate
find
create_with
distinct
eager_load
extending
extract_associated
from
group
having
includes
joins
left_outer_joins
limit
lock
none
offset
optimizer_hints
order
preload
readonly
references
reorder
reselect
regroup
reverse_order
select
where
where
와 group
같이 컬렉션을 반환하는 finder 메서드는 ActiveRecord::Relation
인스턴스를 반환합니다. find
와 first
같이 단일 엔티티를 찾는 메서드는 해당 모델의 단일 인스턴스를 반환합니다.
Model.find(options)
의 주요 동작은 다음과 같이 요약할 수 있습니다:
- 제공된 options를 동등한 SQL 쿼리로 변환합니다.
- SQL 쿼리를 실행하고 데이터베이스에서 해당하는 결과를 검색합니다.
- 결과로 나온 각 행에 대해 적절한 모델의 Ruby 객체를 인스턴스화합니다.
- 있다면
after_find
와after_initialize
콜백을 실행합니다.
2.1 단일 객체 조회하기
Active Record는 단일 객체를 조회하기 위한 여러가지 방법을 제공합니다.
2.1.1 find
find
메서드를 사용하면 지정된 primary key 에 해당하는 객체를 조회할 수 있으며, 추가로 지정한 옵션들과 매칭됩니다. 예를 들면:
# primary key(id)가 10인 customer를 찾습니다.
irb> customer = Customer.find(10)
=> #<Customer id: 10, first_name: "Ryan">
위 내용의 SQL 표현은 다음과 같습니다:
SELECT * FROM customers WHERE (customers.id = 10) LIMIT 1
find
메서드는 매칭되는 레코드를 찾지 못할 경우 ActiveRecord::RecordNotFound
exception을 발생시킵니다.
이 메서드를 사용하여 여러 객체를 조회할 수도 있습니다. find
메서드를 호출하고 primary key 배열을 전달하면 됩니다. 반환값은 제공된 primary keys에 매칭되는 모든 레코드를 포함하는 배열입니다. 예를 들면:
# primary key가 1과 10인 customer를 찾습니다.
irb> customers = Customer.find([1, 10]) # 또는 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))
모든 제공된 primary key에 대해 일치하는 레코드를 찾을 수 없는 경우 find
메서드는 ActiveRecord::RecordNotFound
exception을 발생시킵니다.
테이블이 composite primary key를 사용하는 경우, 단일 항목을 찾기 위해 find에 배열을 전달해야 합니다. 예를 들어, customers가 [:store_id, :id]
를 primary key로 정의된 경우:
# store_id가 3이고 id가 17인 customer를 찾음
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를 가진 여러 customer를 찾으려면, 배열의 배열을 전달해야 합니다:
# primary key가 [1, 8] 및 [7, 15]인 customer를 찾습니다.
irb> customers = Customer.find([[1, 8], [7, 15]]) # 또는 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)
2.1.2 take
take
메서드는 어떤 암시적인 ordering 없이 record를 검색합니다. 예를 들어:
irb> customer = Customer.take
=> #<Customer id: 1, first_name: "Lifo">
위의 SQL 등가식은 다음과 같습니다:
SELECT * FROM customers LIMIT 1
take
메소드는 레코드가 발견되지 않으면 nil
을 반환하며 예외가 발생하지 않습니다.
take
메소드에 숫자 인자를 전달하여 해당 개수만큼의 결과를 반환받을 수 있습니다. 예를 들어
irb> customers = Customer.take(2)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 220, first_name: "Sara">]
위 코드와 동등한 SQL은 다음과 같습니다:
SELECT * FROM customers LIMIT 2
take!
메서드는 take
와 정확히 동일하게 동작하지만, 일치하는 레코드를 찾지 못할 경우 ActiveRecord::RecordNotFound
를 발생시킵니다.
검색된 레코드는 데이터베이스 엔진에 따라 다를 수 있습니다.
2.1.3 first
first
메서드는 primary key로 정렬된(기본값) 첫 번째 레코드를 찾습니다. 예시:
irb> customer = Customer.first
=> #<Customer id: 1, first_name: "Lifo">
위 코드와 동일한 SQL은 다음과 같습니다:
SELECT * FROM customers ORDER BY customers.id ASC LIMIT 1
first
메서드는 일치하는 레코드가 없을 경우 nil
을 반환하며 예외가 발생하지 않습니다.
만약 default scope에 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
복합 primary key를 가진 model들은 정렬시 전체 복합 primary key를 사용합니다.
예를 들어, customers가 [:store_id, :id]
를 primary key로 정의되어 있다면:
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
를 사용해 정렬된 컬렉션에서 first
는 order
에서 지정한 속성으로 정렬된 첫 번째 레코드를 반환합니다.
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
를 발생시킨다는 점이 다릅니다.
2.1.4 last
last
메서드는 primary key로 정렬된(기본값) 마지막 레코드를 찾습니다. 예를 들면:
irb> customer = Customer.last
=> #<Customer id: 221, first_name: "Russel">
위의 내용에 대한 SQL 표현식은 다음과 같습니다:
SELECT * FROM customers ORDER BY customers.id DESC LIMIT 1
last
메서드는 일치하는 레코드가 없을 경우 nil
을 반환하며 예외가 발생하지 않습니다.
복합 primary key를 가진 모델은 전체 복합 primary key를 정렬에 사용합니다.
예를 들어, customers가 [:store_id, :id]
를 primary key로 정의된 경우:
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
default scope에 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">]
위의 Rails 코드와 동등한 SQL 쿼리는 다음과 같습니다:
SELECT * FROM customers ORDER BY customers.id DESC LIMIT 3
order
를 사용하여 정렬된 컬렉션에서, last
는 order
에 명시된 속성으로 정렬된 레코드 중 마지막 레코드를 반환합니다.
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
를 발생시킨다는 점이 다릅니다.
2.1.5 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
where 조건과 일치하는 record 하나를 가져옵니다.
위의 SQL 표현식은 다음과 같습니다:
SELECT * FROM customers WHERE (customers.first_name = 'Lifo') LIMIT 1
위의 SQL에는 ORDER BY
가 없다는 점에 유의하세요. find_by
조건이 여러 레코드와 일치할 수 있는 경우, 결정론적 결과를 보장하기 위해 order를 적용해야 합니다.
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!
일치하는 레코드를 찾지 못하면 ActiveRecord::RecordNotFound
예외를 발생시킵니다.
2.1.5.1 :id
를 사용한 조건
find_by
와 where
와 같은 메서드에서 조건을 지정할 때, id
의 사용은 모델의 :id
속성과 매칭됩니다. 이는 전달된 ID가 primary key 값이어야 하는 find
와는 다릅니다.
:id
가 primary key가 아닌 모델(예: 복합 primary key 모델)에서 find_by(id:)
를 사용할 때는 주의가 필요합니다. 예를 들어, customers가 [:store_id, :id]
를 primary key로 정의된 경우:
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]
을 가진 단일 레코드를 검색하려고 할 수 있지만, Active Record는 :id
컬럼이 5 또는 10인 레코드를 검색하게 되어 잘못된 레코드를 반환할 수 있습니다.
id_value
메서드는 find_by
나 where
같은 finder 메서드에서 사용하기 위해 레코드의 :id
컬럼 값을 가져오는 데 사용할 수 있습니다. 아래 예시를 참조하세요:
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">
2.2 다수의 객체를 배치로 가져오기
많은 레코드를 순회해야 하는 경우가 있습니다. 예를 들어 많은 고객들에게 뉴스레터를 보내거나 데이터를 내보낼 때입니다.
이는 직관적으로 보일 수 있습니다:
# 테이블이 큰 경우 너무 많은 메모리를 소비할 수 있습니다.
Customer.all.each do |customer|
NewsMailer.weekly(customer).deliver_now
end
하지만 이 방식은 테이블 크기가 증가할수록 점점 더 비실용적이 됩니다. Customer.all.each
는 Active Record에게 전체 테이블을 한 번에 가져와서, 각 행마다 model 객체를 생성하고, 그 model 객체들의 전체 배열을 메모리에 유지하도록 지시하기 때문입니다. 실제로 레코드 수가 많은 경우, 전체 컬렉션이 사용 가능한 메모리 양을 초과할 수 있습니다.
Rails는 이 문제를 해결하기 위해 레코드를 메모리 친화적인 batch로 나누어 처리하는 두 가지 메서드를 제공합니다. 첫 번째 메서드인 find_each
는 레코드의 batch를 가져온 다음 각 레코드를 model로서 개별적으로 블록에 yield합니다. 두 번째 메서드인 find_in_batches
는 레코드의 batch를 가져온 다음 전체 batch를 model 배열로 블록에 yield합니다.
find_each
와 find_in_batches
메서드는 한 번에 메모리에 모두 담을 수 없는 많은 수의 레코드를 batch 처리하는 데 사용하기 위한 것입니다. 단순히 천 개 정도의 레코드를 순회해야 한다면 일반적인 find 메서드가 선호되는 옵션입니다.
2.2.1 find_each
find_each
메서드는 레코드를 batch로 가져온 다음 각 레코드를 블록에 yield합니다. 다음 예시에서 find_each
는 1000개씩 batch로 customer를 가져와서 하나씩 블록에 yield합니다:
Customer.find_each do |customer|
NewsMailer.weekly(customer).deliver_now
end
위 코드를 통해 모든 고객에게 뉴스레터를 발송합니다.
이 처리는 모든 레코드가 처리될 때까지 필요에 따라 더 많은 batch를 가져오면서 반복됩니다.
find_each
는 위에서 본 것처럼 model 클래스에서 동작하며, relation에서도 동작합니다:
Customer.where(weekly_subscriber: true).find_each do |customer|
NewsMailer.weekly(customer).deliver_now
end
주간 구독자인 고객들에게 이메일을 보내는 코드입니다.
정렬이 없는 한, 메서드가 내부적으로 순서를 강제해야 하기 때문입니다.
수신자에 정렬이 있는 경우 동작은 config.active_record.error_on_ignored_order
플래그에 따라 달라집니다. true인 경우 ArgumentError
가 발생하고, 그렇지 않으면 정렬이 무시되고 경고가 발생하며 이것이 기본값입니다. 이는 아래에서 설명하는 :error_on_ignore
옵션으로 재정의할 수 있습니다.
2.2.1.1 find_each
의 옵션들
:batch_size
:batch_size
옵션을 사용하면 블록에 개별적으로 전달되기 전에 각 배치에서 검색할 레코드 수를 지정할 수 있습니다. 예를 들어 5000개 단위로 레코드를 검색하려면:
Customer.find_each(batch_size: 5000) do |customer|
NewsMailer.weekly(customer).deliver_now
end
해당 코드는 한 번에 5000개의 record를 가져와서 모든 customer에게 뉴스레터를 보냅니다.
:start
기본적으로 레코드는 primary key의 오름차순으로 가져옵니다. :start
옵션을 사용하면 가장 낮은 ID가 필요한 ID가 아닐 때 시퀀스의 첫 번째 ID를 구성할 수 있습니다. 예를 들어, 마지막으로 처리된 ID를 체크포인트로 저장했다면 중단된 배치 프로세스를 다시 시작하고자 할 때 유용할 수 있습니다.
예를 들어, primary key가 2000부터 시작하는 고객에게만 뉴스레터를 보내려면 다음과 같이 합니다:
Customer.find_each(start: 2000) do |customer|
NewsMailer.weekly(customer).deliver_now
end
:finish
:start
옵션과 비슷하게, :finish
는 가장 높은 ID가 필요한 ID가 아닌 경우 시퀀스의 마지막 ID를 설정할 수 있게 해줍니다.
이는 예를 들어 :start
와 :finish
를 기반으로 레코드의 일부를 사용하여 배치 프로세스를 실행하고 싶을 때 유용할 수 있습니다.
예를 들어, primary key가 2000부터 10000까지인 고객들에게만 뉴스레터를 보내려면:
Customer.find_each(start: 2000, finish: 10000) do |customer|
NewsMailer.weekly(customer).deliver_now
end
다른 예시로는 여러 worker가 동일한 처리 queue를 처리하도록 하고 싶은 경우입니다. 각 worker에게 적절한 :start
와 :finish
옵션을 설정하여 각각 10000개의 레코드를 처리하도록 할 수 있습니다.
:error_on_ignore
relation에 order가 있을 때 error를 발생시킬지 여부를 지정하기 위해 애플리케이션 config를 재정의합니다.
:order
primary key order를 지정합니다(:asc
또는 :desc
일 수 있음). 기본값은 :asc
입니다.
Customer.find_each(order: :desc) do |customer|
NewsMailer.weekly(customer).deliver_now
end
2.2.2 find_in_batches
find_in_batches
는 둘 다 레코드 배치를 검색하므로 find_each
와 유사합니다. 차이점은 find_in_batches
가 개별적으로가 아닌 모델의 배열로 배치 를 블록에 전달한다는 것입니다. 다음 예제는 한 번에 최대 1000명의 customers 배열을 제공된 블록에 전달하며, 마지막 블록에는 남아있는 customers가 포함됩니다:
# add_customers에 한 번에 1000명의 고객 배열을 전달합니다.
Customer.find_in_batches do |customers|
export.add_customers(customers)
end
find_in_batches
는 위에서 본 것처럼 model 클래스에서 작동하며, relation에서도 작동합니다:
# add_customers에 한번에 1000명의 최근 활동한 고객들의 배열을 전달합니다.
Customer.recently_active.find_in_batches do |customers|
export.add_customers(customers)
end
내부적으로 순서를 강제해야 하므로 정렬이 없는 한에서만 가능합니다.
2.2.2.1 find_in_batches
의 옵션들
find_in_batches
메서드는 find_each
와 동일한 옵션을 받습니다:
:batch_size
find_each
와 마찬가지로, batch_size
는 각 그룹에서 검색할 레코드의 수를 지정합니다. 예를 들어, 2500개의 레코드를 배치로 검색하려면 다음과 같이 지정할 수 있습니다:
Customer.find_in_batches(batch_size: 2500) do |customers|
export.add_customers(customers)
end
:start
start
옵션은 레코드를 선택할 시작 ID를 지정할 수 있습니다. 앞서 언급했듯이, 기본적으로 레코드는 primary key의 오름차순으로 가져옵니다. 예를 들어, ID: 5000부터 시작해서 2500개씩 고객 레코드를 가져오려면 다음 코드를 사용할 수 있습니다:
Customer.find_in_batches(batch_size: 2500, start: 5000) do |customers|
export.add_customers(customers)
end
:start
옵션을 통해 레코드를 검색할 시작 id를 지정할 수 있습니다. 이는 이전 배치 처리가 특정 id에서 중단되었을 때 유용합니다.
:finish
finish
옵션은 검색할 레코드의 마지막 ID를 지정할 수 있습니다. 아래 코드는 ID가 7000인 customer까지 customer를 batch로 검색하는 사례를 보여줍니다:
Customer.find_in_batches(finish: 7000) do |customers|
export.add_customers(customers)
end
:error_on_ignore
error_on_ignore
옵션은 relation에 특정 순서가 존재할 때 에러를 발생시킬지 여부를 지정하기 위해 애플리케이션 설정을 재정의합니다.
3 Conditions
where
메서드는 반환되는 레코드를 제한하는 조건을 지정할 수 있게 해주며, SQL 문의 WHERE
부분을 나타냅니다. 조건은 문자열, 배열 또는 해시로 지정할 수 있습니다.
3.1 Pure String Conditions
find에 조건을 추가하고 싶다면, Book.where("title = 'Introduction to Algorithms'")
와 같이 지정할 수 있습니다. 이는 title
필드 값이 'Introduction to Algorithms'인 모든 책을 찾을 것입니다.
순수 문자열로 직접 조건을 만드는 것은 SQL injection 공격에 취약할 수 있습니다. 예를 들어, Book.where("title LIKE '%#{params[:title]}%'")
는 안전하지 않습니다. 다음 섹션에서 array를 사용한 조건 처리의 선호되는 방법을 확인하세요.
3.2 Array 조건들
만약 타이틀이 어딘가의 인수로 변경될 수 있다면 어떻게 될까요? find는 다음과 같은 형태를 가질 것입니다:
Book.where("title = ?", params[:title])
Active Record는 첫 번째 인수를 조건 문자열로 사용하며 추가 인수는 조건문에서 물음표 (?)
를 대체합니다.
여러 개의 조건을 지정하고 싶다면:
Book.where("title = ? AND out_of_print = ?", params[:title], false)
이 예시에서 첫 번째 물음표는 params[:title]
값으로 대체되고, 두 번째 물음표는 adapter에 따라 다른 SQL의 false
표현으로 대체됩니다.
아래의 코드가 더 선호됩니다:
Book.where("title = ?", params[:title])
"이 코드로:" 로 번역됩니다.
Book.where("title = #{params[:title]}")
절대로 이런 코드를 사용하지 마세요. 이는 params[:title] 값이 SQL에 직접 주입되기 때문에 SQL 인젝션 공격에 취약합니다.
인수 안전성 때문입니다. 변수를 직접 조건 문자열에 넣으면 변수가 데이터베이스에 있는 그대로 전달됩니다. 이는 악의적인 의도를 가진 사용자로부터 직접 받은 이스케이프되지 않은 변수가 될 수 있다는 것을 의미합니다. 이렇게 하면 사용자가 데이터베이스를 악용할 수 있다는 것을 알게 되면 데이터베이스에 거의 모든 것을 할 수 있기 때문에 전체 데이터베이스가 위험해집니다. 절대로 인수를 조건 문자열 안에 직접 넣지 마세요.
SQL injection의 위험성에 대한 자세한 정보는 Ruby on Rails Security Guide를 참조하세요.
3.2.1 Placeholder 조건
params의 (?)
대체 스타일과 유사하게, 조건 문자열에 키를 지정하고 이에 대응하는 키/값 해시를 함께 지정할 수 있습니다:
Book.where("created_at >= :start_date AND created_at <= :end_date",
{ start_date: params[:start_date], end_date: params[:end_date] })
많은 수의 가변 조건이 있을 때 더욱 명확한 가독성을 제공합니다.
3.2.2 LIKE
를 사용하는 조건
condition 인자가 SQL injection을 방지하기 위해 자동으로 이스케이프(escape)되지만, SQL LIKE
의 와일드카드(예: %
와 _
)는 이스케이프되지 않습니다. 이는 sanitize되지 않은 값이 인자로 사용될 때 예기치 않은 동작을 일으킬 수 있습니다. 예를 들어:
Book.where("title LIKE ?", params[:title] + "%")
위 코드에서는 사용자가 지정한 문자열로 시작하는 제목을 매칭하려고 합니다. 하지만 params[:title]
에 포함된 %
나 _
는 와일드카드로 처리되어 예상치 않은 쿼리 결과를 초래할 수 있습니다. 이로 인해 데이터베이스가 의도한 index를 사용하지 못하게 되어 쿼리가 훨씬 느려질 수 있는 상황도 발생할 수 있습니다.
이러한 문제를 피하려면 sanitize_sql_like
를 사용하여 인자의 관련 부분에서 와일드카드 문자를 이스케이프 처리하세요:
Book.where("title LIKE ?",
Book.sanitize_sql_like(params[:title]) + "%")
3.3 Hash 조건
Active Record는 조건문의 가독성을 높일 수 있는 hash 조건을 전달할 수도 있습니다. Hash 조건을 사용하면 검색하려는 필드를 key로, 그에 대한 검색 조건을 value로 하는 hash를 전달합니다:
Hash 조건에서는 동등성, 범위, 부분집합 검사만 가능합니다.
3.3.1 동등성 조건
Book.where(out_of_print: true)
이렇게 하면 다음과 같은 SQL이 생성됩니다:
SELECT * FROM books WHERE (books.out_of_print = 1)
필드 이름은 문자열(string)로도 지정할 수 있습니다:
Book.where("out_of_print" => true)
belongs_to 관계에서는 Active Record 객체가 값으로 사용되는 경우 모델을 지정하기 위해 association key를 사용할 수 있습니다. 이 방법은 polymorphic 관계에서도 동작합니다.
author = Author.first
Book.where(author: author)
Author.joins(:books).where(books: { author: author })
Hash 조건은 key가 컬럼의 배열이고 value가 튜플의 배열인 튜플과 유사한 문법으로도 지정할 수 있습니다:
Book.where([:author_id, :id] => [[15, 1], [15, 2]])
이 구문은 테이블이 composite primary key를 사용하는 relation을 쿼리할 때 유용할 수 있습니다:
class Book < ApplicationRecord
self.primary_key = [:author_id, :id]
end
Book.where(Book.primary_key => [[2, 1], [3, 1]])
이 코드는 composite primary key(복합 기본키)로 구성된 author_id
와 id
를 가진 Book 모델의 예시입니다. Book.where
쿼리는 author_id
가 2이고 id
가 1인 책과, author_id
가 3이고 id
가 1인 책을 찾습니다.
3.3.2 Range Conditions
Range condition은 BETWEEN
SQL 표현식을 사용해 필드가 주어진 값 범위에 있는지 확인합니다. Range condition은 Ruby의 range object를 사용하여 생성됩니다:
Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)
이는 다음 SQL을 생성합니다:
SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
이는 Array Conditions를 사용하는 것과 동등합니다.
Client.where(created_at: ['2008-12-21 00:00:00', '2008-12-22 00:00:00'])
Range condition이 BETWEEN
을 사용하는 반면, array condition은 IN
을 사용합니다. 이 차이점은 시간 값을 비교할 때 중요할 수 있습니다.
객체가 속성을 받아들일 수 있을 때(responded_at과 같은), Range
는 SQL 표현식에서 정의된 형태대로 속성이 NULL
을 포함하는지 확인하는데 사용될 수 있습니다:
Student.where(response_time: nil..Float::INFINITY)
이는 다음 SQL을 생성합니다:
SELECT * FROM students WHERE (students.response_time <= INFINITY OR students.response_time IS NULL)
Book.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)
Book을 생성한 시간이 어제 자정부터 오늘 자정 사이인 레코드를 찾습니다.
이것은 SQL의 BETWEEN
구문을 사용하여 어제 생성된 모든 books를 찾을 것입니다:
SELECT * FROM books WHERE (books.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
Array Conditions에서 예시로 든 보다 짧은 구문을 보여줍니다.
시작과 끝이 없는 range가 지원되며, 이는 미만/초과 조건을 만드는 데 사용할 수 있습니다.
Book.where(created_at: (Time.now.midnight - 1.day)..)
어제 자정 이후에 생성된 모든 책을 가져옵니다
이는 다음과 같은 SQL을 생성할 것입니다:
SELECT * FROM books WHERE books.created_at >= '2008-12-21 00:00:00'
3.3.3 서브셋 조건
만약 IN
표현식을 사용하여 레코드를 찾고 싶다면 conditions 해시에 배열을 전달할 수 있습니다:
Customer.where(orders_count: [1, 3, 5])
orders_count가 1, 3 또는 5인 고객을 찾습니다.
이 코드는 다음과 같은 SQL을 생성할 것입니다:
SELECT * FROM customers WHERE (customers.orders_count IN (1,3,5))
3.4 NOT 조건
NOT
SQL 쿼리는 where.not
을 사용하여 구성할 수 있습니다:
Customer.where.not(orders_count: [1, 3, 5])
orders_count가 1, 3, 5가 아닌 Customer를 찾습니다.
다시 말해서, 이 쿼리는 where
를 인수 없이 호출한 다음 즉시 not
을 체이닝하여 where
조건을 전달하는 방식으로 생성할 수 있습니다. 이는 이와 같은 SQL을 생성할 것입니다:
SELECT * FROM customers WHERE (customers.orders_count NOT IN (1,3,5))
만약 쿼리가 nullable 컬럼에 대해 nil
이 아닌 값을 가진 hash 조건을 가지고 있다면, nullable 컬럼에 nil
값을 가진 레코드는 반환되지 않을 것입니다. 예를 들어:
Customer.create!(nullable_country: nil)
Customer.where.not(nullable_country: "UK")
# => []
# 하지만
Customer.create!(nullable_country: "UK")
Customer.where.not(nullable_country: nil)
# => [#<Customer id: 2, nullable_country: "UK">]
3.5 OR 조건
두 relation간의 OR
조건은 첫번째 relation에 [or
][]를 호출하고 두번째 relation을 인자로 전달하여 생성할 수 있습니다.
Customer.where(last_name: "Smith").or(Customer.where(orders_count: [1, 3, 5]))
SELECT * FROM customers WHERE (customers.last_name = 'Smith' OR customers.orders_count IN (1,3,5))
3.6 AND 조건
AND
조건은 where
조건들을 연결하여 만들 수 있습니다.
Customer.where(last_name: "Smith").where(orders_count: [1, 3, 5])
Customer에서 last_name이 "Smith"이며 orders_count가 1, 3, 5 중 하나인 레코드를 조회합니다.
SELECT * FROM customers WHERE customers.last_name = 'Smith' AND customers.orders_count IN (1,3,5)
관계들 간의 논리적 교집합을 위한 AND
조건은 첫 번째 relation에서 [and
][]를 호출하고 두 번째 것을 인자로 전달하여 만들 수 있습니다.
Customer.where(id: [1, 2]).and(Customer.where(id: [2, 3]))
SELECT * FROM customers WHERE (customers.id IN (1, 2) AND customers.id IN (2, 3))
4 Ordering
데이터베이스로부터 특정한 순서로 레코드를 검색하려면 order
메소드를 사용할 수 있습니다.
예를 들어, 레코드 세트를 가져올 때 테이블의 created_at
필드를 기준으로 오름차순으로 정렬하려면:
Book.order(:created_at)
# 또는
Book.order("created_at")
ASC
또는 DESC
도 지정할 수 있습니다:
Book.order(created_at: :desc)
# 또는
Book.order(created_at: :asc)
# 또는
Book.order("created_at DESC")
# 또는
Book.order("created_at ASC")
또는 여러 필드로 정렬할 수 있습니다:
Book.order(title: :asc, created_at: :desc)
# 또는
Book.order(:title, created_at: :desc)
# 또는
Book.order("title ASC, created_at DESC")
# 또는
Book.order("title ASC", "created_at DESC")
여러 번 order
를 호출하고자 한다면, 뒤이어 나오는 order들은 첫 번째 order에 추가됩니다:
irb> Book.order("title ASC").order("created_at DESC")
SELECT * FROM books ORDER BY title ASC, created_at DESC
조인된 테이블에서도 정렬할 수 있습니다.
Book.includes(:author).order(books: { print_year: :desc }, authors: { name: :asc })
# 또는
Book.includes(:author).order("books.print_year desc", "authors.name asc")
대부분의 데이터베이스 시스템에서 select
, pluck
, ids
와 같은 메서드를 사용하여 결과 세트에서 distinct
로 필드를 선택할 때, order
절에서 사용된 필드가 select 목록에 포함되어 있지 않으면 order
메서드는 ActiveRecord::StatementInvalid
예외를 발생시킵니다. 결과 세트에서 필드를 선택하는 방법은 다음 섹션을 참조하세요.
5 Selecting Specific Fields
기본적으로 Model.find
는 select *
를 사용하여 결과 세트의 모든 필드를 선택합니다.
결과 세트에서 필드의 하위 집합만 선택하려면, select
메서드를 통해 하위 집합을 지정할 수 있습니다.
예를 들어, isbn
과 out_of_print
컬럼만 선택하려면:
Book.select(:isbn, :out_of_print)
# 또는
Book.select("isbn, out_of_print")
이 find 호출에서 사용하는 SQL 쿼리는 다음과 비슷할 것입니다:
SELECT isbn, out_of_print FROM books
이는 선택한 필드로만 모델 객체를 초기화한다는 의미이므로 주의가 필요합니다. 초기화된 레코드에 없는 필드에 접근하려고 하면 다음과 같은 오류가 발생합니다:
ActiveModel::MissingAttributeError: Book 모델에서 '<attribute>' 속성이 누락되었습니다
여기서 <attribute>
는 요청한 속성입니다. id
메서드는 ActiveRecord::MissingAttributeError
를 발생시키지 않으므로, 연관관계를 다룰 때 주의해야 합니다. 연관관계가 제대로 작동하려면 id
메서드가 필요하기 때문입니다.
특정 필드의 고유한 값마다 하나의 레코드만 가져오고 싶다면, distinct
를 사용할 수 있습니다:
Customer.select(:last_name).distinct
에서는 중복되지 않은 last_name들을 검색합니다.
이는 다음과 같은 SQL을 생성할 것입니다:
SELECT DISTINCT last_name FROM customers
uniqueness 제약조건을 제거할 수도 있습니다:
# 유니크한 last_names를 반환
query = Customer.select(:last_name).distinct
# 중복이 있더라도 모든 last_names를 반환
query.distinct(false)
6 Limit과 Offset
Model.find
에 의해 발생한 SQL에 LIMIT
을 적용하기 위해서는, relation에 limit
와 offset
메서드를 사용하여 LIMIT
를 지정할 수 있습니다.
limit
를 사용하여 검색할 레코드 수를 지정하고, offset
을 사용하여 레코드를 반환하기 전에 건너뛸 레코드 수를 지정할 수 있습니다. 예를 들면
Customer.limit(5)
offset이 지정되지 않았으므로 테이블의 처음부터 최대 5명의 customers를 반환할 것입니다. 실행되는 SQL은 다음과 같습니다:
SELECT * FROM customers LIMIT 5
offset
을 추가하기
Customer.limit(5).offset(30)
Customer를 30번째부터 5개만 골라냅니다.
31번째부터 시작하는 최대 5명의 customer를 반환할 것입니다. SQL은 다음과 같습니다:
SELECT * FROM customers LIMIT 5 OFFSET 30
7 Grouping
SQL finder에서 GROUP BY
절을 적용하려면 group
메소드를 사용할 수 있습니다.
예를 들어, order가 생성된 날짜들을 collection으로 찾으려면:
Order.select("created_at").group("created_at")
이는 데이터베이스에 order가 있는 각 날짜에 대해 하나의 Order
객체를 반환할 것입니다.
실행되는 SQL은 다음과 같습니다:
SELECT created_at
FROM orders
GROUP BY created_at
7.1 Grouped Items의 합계
단일 쿼리에서 grouped items의 합계를 구하려면, group
뒤에 count
를 호출하세요.
irb> Order.group(:status).count
=> {"being_packed"=>7, "shipped"=>12} # 포장 중인 주문이 7개, 배송된 주문이 12개
다음과 같은 SQL이 실행될 것입니다:
SELECT COUNT (*) AS count_all, status AS status
FROM orders
GROUP BY status
7.2 HAVING 조건절
SQL은 GROUP BY
필드에 조건을 지정하기 위해 HAVING
절을 사용합니다. Model.find
에서 실행되는 SQL에 having
메서드를 추가하여 HAVING
절을 추가할 수 있습니다.
예시:
Order.select("created_at을 ordered_date으로, sum(total)을 total_price로").
group("created_at").having("sum(total) > ?", 200)
실행될 SQL은 다음과 같을 것입니다:
SELECT created_at as ordered_date, sum(total) as total_price
FROM orders
GROUP BY created_at
HAVING sum(total) > 200
이는 주문된 날짜별로 그룹화되고 총액이 $200를 초과하는 각 order 객체의 날짜와 총 가격을 반환합니다.
반환된 각 order 객체의 total_price
에는 다음과 같이 접근할 수 있습니다:
big_orders = Order.select("created_at, sum(total) as total_price")
.group("created_at")
.having("sum(total) > ?", 200)
big_orders[0].total_price
# 첫 번째 Order 객체의 total price를 반환합니다
8 Conditions 재정의
8.1 unscope
unscope
메서드를 사용하여 제거할 특정 조건을 지정할 수 있습니다. 예를 들면:
Book.where("id > 100").limit(20).order("id desc").unscope(:order)
.unscope
를 사용하여 이전에 적용된 특정 scope를 제거할 수 있습니다. 예를 들어 위의 경우에는 order
scope를 제거합니다.
실행될 SQL은 다음과 같습니다:
SELECT * FROM books WHERE id > 100 LIMIT 20
-- unscope를 사용하지 않은 원래의 쿼리
SELECT * FROM books WHERE id > 100 ORDER BY id desc LIMIT 20
where절의 특정 조건만 제거할 수도 있습니다. 예를 들어 다음과 같이 where절에서 id
조건을 제거할 수 있습니다:
Book.where(id: 10, out_of_print: false).unscope(where: :id)
# books.* FROM books에서 out_of_print = 0인 레코드 SELECT
unscope
를 사용한 relation은 그것이 병합되는 어떤 relation에도 영향을 미칩니다:
Book.order("id desc").merge(Book.unscope(:order))
# books 테이블에서 모든 컬럼을 조회
unscope
는 이전에 추가된 특정한 scope를 제거합니다. 예를 들면:
Post.where('id > 10').limit(20).order('id asc').unscope(:order)
생성된 SQL:
SELECT "posts".* FROM "posts" WHERE (id > 10) LIMIT 20
# `order` 부분이 제거되었음을 주목하세요
또한 where
를 unscope할 때는, 특정 조건들을 지정할 수 있습니다:
Post.where(id: 10, trashed: false).unscope(where: :id)
# SELECT "posts".* FROM "posts" WHERE trashed = false
8.2 only
only
메서드를 사용해서 조건을 오버라이드할 수도 있습니다. 예를 들면:
Book.where("id > 10").limit(20).order("id desc").only(:order, :where)
이 메서드 체인에 대해 only
는 :order
와 :where
구절만을 유지하고 다른 모든 구절을 제거할 것입니다. 결과적으로 limit
구절은 제거되어 아래와 같은 쿼리가 생성됩니다:
실행될 SQL:
SELECT * FROM books WHERE id > 10 ORDER BY id DESC
-- only가 없는 원본 쿼리
SELECT * FROM books WHERE id > 10 ORDER BY id DESC LIMIT 20
8.3 reselect
reselect
메서드는 기존의 select 구문을 재정의합니다. 예시:
Book.select(:title, :isbn).reselect(:created_at)
select로 선택된 속성들을 덮어쓰고 새로운 속성들을 선택합니다.
실행되는 SQL:
SELECT books.created_at FROM books
reselect
절이 사용되지 않는 경우와 비교해보세요:
Book.select(:title, :isbn).select(:created_at)
실행되는 SQL은 다음과 같습니다:
SELECT books.title, books.isbn, books.created_at FROM books
8.4 reorder
reorder
메서드는 기본 scope 정렬을 오버라이드합니다. 예를 들어 클래스 정의에 다음이 포함된 경우:
class Author < ApplicationRecord
has_many :books, -> { order(year_published: :desc) }
end
그리고 다음을 실행하세요:
Author.find(10).books
번역할 문장이 없는 순수 Ruby 코드 예시입니다.
실행될 SQL문은 다음과 같습니다:
SELECT * FROM authors WHERE id = 10 LIMIT 1
SELECT * FROM books WHERE author_id = 10 ORDER BY year_published DESC
reorder
절을 사용하여 책을 정렬하는 방식을 다르게 지정할 수 있습니다:
Author.find(10).books.reorder("year_published ASC")
실행될 SQL 쿼리:
SELECT * FROM authors WHERE id = 10 LIMIT 1
SELECT * FROM books WHERE author_id = 10 ORDER BY year_published ASC
8.5 reverse_order
reverse_order
메서드는 정렬 조건이 지정된 경우 순서를 반대로 바꿉니다.
Book.where("author_id > 10").order(:year_published).reverse_order
where 절로 필터링한 결과를 year_published로 정렬한 뒤 reverse_order로 순서를 뒤집습니다.
실행될 SQL 쿼리:
SELECT * FROM books WHERE author_id > 10 ORDER BY year_published DESC
쿼리에 ordering 절이 지정되어 있지 않은 경우, reverse_order
는 primary key를 기준으로 역순으로 정렬합니다.
Book.where("author_id > 10").reverse_order
reverse_order
는 정렬 순서를 뒤집습니다. 단, 모델에 순서를 지정하지 않은 경우 기본 키로 역순 정렬을 수행합니다.
실행될 SQL문:
SELECT * FROM books WHERE author_id > 10 ORDER BY books.id DESC
reverse_order
메소드는 아무런 인자도 받지 않습니다.
8.6 rewhere
rewhere
메서드는 기존의 명명된 where
조건을 덮어씁니다. 예를 들어:
Book.where(out_of_print: true).rewhere(out_of_print: false)
절을 완전히 덮어씁니다. rewhere는 where와는 달리 동일한 조건에 대한 기존의 조건 절을 제거한 후 새로운 조건을 추가합니다.
실행될 SQL:
SELECT * FROM books WHERE out_of_print = 0
rewhere
절을 사용하지 않으면 where 절들이 AND로 결합됩니다:
Book.where(out_of_print: true).where(out_of_print: false)
실행되는 SQL문은 다음과 같습니다:
SELECT * FROM books WHERE out_of_print = 1 AND out_of_print = 0
rewhere
는 기존 조건을 새로운 조건으로 대체할 수 있습니다. 예를 들어 rewhere
는 사용자 검색이나 필터링을 수행할 때 유용할 수 있습니다:
Article.where(trashed: true).rewhere(trashed: false)
처음 구성된 where(trashed: true)
는 무시되고 대신 where(trashed: false)
가 사용됩니다.
8.7 regroup
regroup
메서드는 기존의 명명된 group
조건을 재정의합니다. 예를 들어:
Book.group(:author).regroup(:id)
:author로 그룹화된 쿼리를 :id로 재그룹화합니다.
실행될 SQL 쿼리:
SELECT * FROM books GROUP BY id
만약 regroup
절을 사용하지 않으면, 모든 group 절들이 함께 결합됩니다.
Book.group(:author).group(:id)
- 여러 컬럼의 그룹화는
group(:column1, :column2)
혹은 여러group
호출로 지정할 수 있습니다.
실행될 SQL은 다음과 같습니다:
SELECT * FROM books GROUP BY author, id
9 Null Relation
none
메서드는 레코드가 없는 연결 가능한 relation을 반환합니다. 반환된 relation에 연결된 이후의 모든 조건들은 계속해서 빈 relation을 생성합니다. 이는 메서드나 scope에 대한 응답이 0개의 결과를 반환할 수 있는 시나리오에서 연결 가능한 응답이 필요할 때 유용합니다.
Book.none # 빈 Relation을 반환하고 쿼리를 실행하지 않습니다.
# 아래의 highlighted_reviews 메서드는 항상 Relation을 반환하도록 기대됩니다.
Book.first.highlighted_reviews.average(:rating)
# => 책의 평균 평점을 반환합니다
class Book
# 리뷰가 5개 이상이면 리뷰들을 반환하고,
# 그렇지 않으면 아직 리뷰가 없는 책으로 간주합니다
def highlighted_reviews
if reviews.count > 5
reviews
else
Review.none # 아직 최소 임계값을 충족하지 않음
end
end
end
10 Readonly 객체
Active Record는 relation에 readonly
메소드를 제공하여 반환된 객체들의 수정을 명시적으로 금지할 수 있습니다. readonly 레코드를 변경하려는 모든 시도는 성공하지 못하고 ActiveRecord::ReadOnlyRecord
예외가 발생합니다.
customer = Customer.readonly.first
customer.visits += 1
customer.save # ActiveRecord::ReadOnlyRecord 에러를 발생시킴
customer
가 명시적으로 readonly 객체로 설정되었기 때문에, 위의 코드는 visits 값을 업데이트하여 customer.save
를 호출할 때 ActiveRecord::ReadOnlyRecord
예외를 발생시킬 것입니다.
11 레코드 업데이트를 위한 잠금
잠금은 데이터베이스의 레코드를 업데이트할 때 경쟁 상태를 방지하고 원자적 업데이트를 보장하는 데 도움이 됩니다.
Active Record는 두 가지 잠금 메커니즘을 제공합니다:
- Optimistic Locking
- Pessimistic Locking
11.1 낙관적 잠금(Optimistic Locking)
낙관적 잠금은 여러 사용자가 동시에 같은 레코드를 편집할 수 있게 하며, 데이터 충돌이 최소한으로 발생한다고 가정합니다. 이는 레코드가 열린 이후 다른 프로세스가 해당 레코드를 변경했는지 확인하는 방식으로 동작합니다. 만약 변경이 발생했다면 ActiveRecord::StaleObjectError
예외가 발생하고 업데이트는 무시됩니다.
낙관적 잠금 컬럼
낙관적 잠금을 사용하려면 테이블에 integer 타입의 lock_version
컬럼이 있어야 합니다. 레코드가 업데이트될 때마다 Active Record는 lock_version
컬럼의 값을 증가시킵니다. 만약 업데이트 요청의 lock_version
필드 값이 데이터베이스의 현재 lock_version
컬럼 값보다 낮다면, 해당 업데이트 요청은 ActiveRecord::StaleObjectError
와 함께 실패하게 됩니다.
예시:
c1 = Customer.find(1)
c2 = Customer.find(1)
c1.first_name = "Sandra"
c1.save
c2.first_name = "Michael"
c2.save # ActiveRecord::StaleObjectError를 발생시킴
그런 다음 exception을 rescue하고 rollback하거나, merge하거나, 또는 충돌을 해결하는 데 필요한 비즈니스 로직을 적용하는 등의 방법으로 충돌을 처리할 책임이 있습니다.
이 동작은 ActiveRecord::Base.lock_optimistically = false
를 설정하여 끌 수 있습니다.
lock_version
컬럼의 이름을 오버라이드하려면, ActiveRecord::Base
는 locking_column
이라는 클래스 속성을 제공합니다:
class Customer < ApplicationRecord
self.locking_column = :lock_customer_column
end
위 코드는 Customer 모델의 optimistic locking에서 사용될 column을 변경하는 예시입니다. 일반적으로 lock_version이 사용되지만 새로운 column을 사용하도록 지정할 수 있습니다.
11.2 Pessimistic Locking
Pessimistic locking은 기본 데이터베이스가 제공하는 locking 메커니즘을 사용합니다. relation을 구축할 때 lock
을 사용하면 선택된 행에 대한 배타적 lock을 얻습니다. lock
을 사용하는 relation은 일반적으로 deadlock 상태를 방지하기 위해 transaction 안에 래핑됩니다.
예시:
Book.transaction do
book = Book.lock.first
book.title = "Algorithms, second edition"
book.save!
end
MySQL 백엔드에서 위의 세션은 다음과 같은 SQL을 생성합니다:
SQL (0.2ms) BEGIN
Book Load (0.3ms) SELECT * FROM books LIMIT 1 FOR UPDATE
Book Update (0.4ms) UPDATE books SET updated_at = '2009-02-07 18:05:56', title = 'Algorithms, second edition' WHERE id = 1
SQL (0.8ms) COMMIT
lock
메서드에 raw SQL을 전달하여 다양한 유형의 lock을 허용할 수 있습니다. 예를 들어, MySQL에는 LOCK IN SHARE MODE
라는 표현식이 있어 record를 잠그면서도 다른 query가 읽을 수 있도록 허용합니다. 이 표현식을 사용하려면 lock 옵션으로 전달하면 됩니다:
Book.transaction do
book = Book.lock("LOCK IN SHARE MODE").find(1)
book.increment!(:views)
end
이 코드는 데이터베이스의 SHARE MODE 락을 사용하여 읽기 트랜잭션을 수행합니다. ID가 1인 book 레코드를 검색하고 views 카운터를 증가시킵니다.
lock
메소드에 전달하는 raw SQL을 데이터베이스가 지원해야 합니다.
만약 이미 모델의 인스턴스가 있다면, 다음 코드를 사용하여 트랜잭션을 시작하고 lock을 한 번에 획득할 수 있습니다:
book = Book.first
book.with_lock do
# 이 블록은 트랜잭션 내에서 호출되며,
# book은 이미 lock 상태입니다.
book.increment!(:views)
end
12 Joining Tables
Active Record는 결과 SQL에서 JOIN
절을 지정하기 위한 두 가지 finder method를 제공합니다: joins
와 left_outer_joins
입니다.
joins
는 INNER JOIN
또는 사용자 정의 쿼리에 사용되어야 하는 반면,
left_outer_joins
는 LEFT OUTER JOIN
을 사용하는 쿼리에 사용됩니다.
12.1 joins
joins
메서드를 사용하는 방법은 여러 가지가 있습니다.
12.1.1 SQL 문자열 조각 사용하기
joins
에 JOIN
절을 지정하는 raw SQL을 직접 제공할 수 있습니다:
Author.joins("INNER JOIN books ON books.author_id = authors.id AND books.out_of_print = FALSE")
Author.joins를 사용하여 SQL INNER JOIN을 직접 지정할 수 있습니다.
이는 다음과 같은 SQL을 생성할 것입니다:
SELECT authors.* FROM authors INNER JOIN books ON books.author_id = authors.id AND books.out_of_print = FALSE
12.1.2 Array/Hash로 Named Associations 사용하기
Active Record를 사용하면 모델에 정의된 associations의 이름을 joins
메서드를 사용할 때 해당 associations에 대한 JOIN
절을 지정하는 단축키로 사용할 수 있습니다.
다음의 모든 예시는 INNER JOIN
을 사용하여 예상되는 join 쿼리를 생성합니다:
12.1.2.1 단일 Association 조인하기
Book.joins(:reviews)
Book 모델과 관련된 reviews를 joins 합니다.
이는 다음과 같이 출력됩니다:
SELECT books.* FROM books
INNER JOIN reviews ON reviews.book_id = books.id
또는 영어로 표현하면: "리뷰가 있는 모든 책들에 대한 Book 객체를 반환". 한 책에 여러 개의 리뷰가 있는 경우 중복된 책들이 표시된다는 점에 유의하세요. 중복되지 않은 책들을 원한다면 Book.joins(:reviews).distinct
를 사용할 수 있습니다.
12.1.3 여러 Association 조인하기
Book.joins(:author, :reviews)
Book과 author, reviews를 join합니다.
이는 다음과 같이 출력됩니다:
SELECT books.* FROM books
INNER JOIN authors ON authors.id = books.author_id
INNER JOIN reviews ON reviews.book_id = books.id
또는 영어로는: "작성자가 있고 리뷰가 하나 이상 있는 모든 책을 반환하라"는 의미입니다. 다시 말하지만, 여러 개의 리뷰가 있는 책은 여러 번 표시될 것입니다.
12.1.3.1 중첩된 Association 조인하기 (단일 레벨)
Book.joins(reviews: :customer)
reviews를 거쳐서 customer까지 연결되는 중첩 join을 수행합니다.
이렇게 생성됩니다:
SELECT books.* FROM books
INNER JOIN reviews ON reviews.book_id = books.id
INNER JOIN customers ON customers.id = reviews.customer_id
또는 영어로: "고객의 리뷰가 있는 모든 책을 반환한다."
12.1.3.2 중첩된 Association 조인하기 (다중 레벨)
Author.joins(books: [{ reviews: { customer: :orders } }, :supplier])
Active Record 쿼리 인터페이스
Active Record는 데이터베이스의 데이터에 액세스할 수 있게 해주는 풍부한 API를 제공합니다. 이 가이드에서는 다음 주제를 다룹니다:
- 단건 객체를 찾는 방법
- 객체들의 집합을 찾는 방법
- 검색을 제한하는 방법
- 데이터베이스 집계를 수행하는 방법
- Table Association을 사용하는 방법
- INNER JOIN과 LEFT OUTER JOIN을 사용하는 방법
- Eager loading을 통해 쿼리 횟수를 최소화하는 방법
- 데이터베이스에서 여러 객체들을 조작하기 위해 Scopes를 사용하는 방법
이 가이드에서 설명하는 예제들은 다음 모델들을 사용할 것입니다:
class Author < ApplicationRecord
has_many :books
has_many :authors_books
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
다음과 같이 생성됩니다:
SELECT authors.* FROM authors
INNER JOIN books ON books.author_id = authors.id
INNER JOIN reviews ON reviews.book_id = books.id
INNER JOIN customers ON customers.id = reviews.customer_id
INNER JOIN orders ON orders.customer_id = customers.id
INNER JOIN suppliers ON suppliers.id = books.supplier_id
또는 영어로는 "책 리뷰가 있고 또한 고객이 주문한 저자들과, 그 책들의 공급업체를 반환합니다."
12.1.4 Join된 테이블에 대한 조건 지정하기
join된 테이블에 대한 조건은 일반적인 Array와 String 조건을 사용하여 지정할 수 있습니다. Hash 조건은 join된 테이블의 조건을 지정하기 위한 특별한 문법을 제공합니다:
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).where("orders.created_at" => time_range).distinct
어제 자정부터 오늘 자정 사이에 주문한 고객들을 모두 찾습니다.
이것은 created_at
을 비교하기 위해 SQL의 BETWEEN
표현식을 사용하여 어제 생성된 orders를 가진 모든 customers를 찾습니다.
대안으로 더 깔끔한 문법은 hash 조건을 중첩하는 것입니다:
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).where(orders: { created_at: time_range }).distinct
어제 자정부터 오늘 자정까지의 기간 동안 주문한 고객들을 찾는 예제입니다. distinct
는 중복된 고객을 제거하는데 사용됩니다.
더 복잡한 조건이나 기존의 named scope를 재사용하기 위해서는 merge
를 사용할 수 있습니다. 먼저 Order
모델에 새로운 named scope를 추가해보겠습니다:
class Order < ApplicationRecord
belongs_to :customer
scope :created_in_time_range, ->(time_range) {
where(created_at: time_range)
}
end
위 코드는 주문 시간의 범위를 지정하는 scope를 정의합니다.
이제 merge
를 사용하여 created_in_time_range
scope를 병합할 수 있습니다:
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).merge(Order.created_in_time_range(time_range)).distinct
time_range가 설정된 기간 동안 주문한 고객들을 중복없이 조회합니다.
이는 SQL BETWEEN
표현식을 사용하여, 어제 생성된 주문을 가진 모든 customer를 찾습니다.
12.2 left_outer_joins
관련 레코드가 있든 없든 레코드 세트를 선택하고 싶다면 left_outer_joins
메서드를 사용할 수 있습니다.
Customer.left_outer_joins(:reviews).distinct.select("customers.*, COUNT(reviews.*) AS reviews_count").group("customers.id")
다음과 같이 생성됩니다:
SELECT DISTINCT customers.*, COUNT(reviews.*) AS reviews_count FROM customers
LEFT OUTER JOIN reviews ON reviews.customer_id = customers.id GROUP BY customers.id
"모든 customer를 반환하되, review가 있든 없든 상관없이 각각의 review 수를 함께 반환합니다"
12.3 where.associated
와 where.missing
associated
와 missing
쿼리 메소드는 association의 존재 여부를 기반으로 레코드 세트를 선택할 수 있게 해줍니다.
where.associated
사용법:
Customer.where.associated(:reviews)
Customer 모델에 연결된 reviews를 가진 레코드만 조회합니다.
출력:
SELECT customers.* FROM customers
INNER JOIN reviews ON reviews.customer_id = customers.id
WHERE reviews.id IS NOT NULL
이는 "적어도 하나의 리뷰를 남긴 모든 고객들을 반환하라"는 의미입니다.
where.missing
을 사용하려면:
Customer.where.missing(:reviews)
reviews가 없는 Customer를 조회합니다.
Produces:
(번역할 내용이 없어서 번역을 제공할 수 없습니다. 번역이 필요한 본문을 제공해 주시면 번역해드리겠습니다.)
SELECT customers.* FROM customers
LEFT OUTER JOIN reviews ON reviews.customer_id = customers.id
WHERE reviews.id IS NULL
이는 "리뷰를 하나도 작성하지 않은 모든 고객들을 반환한다"는 의미입니다.
13 Eager Loading Associations
Eager loading은 Model.find
로 반환된 객체들의 연관 레코드들을 가능한 적은 쿼리로 로딩하는 메커니즘입니다.
13.1 N + 1 쿼리 문제
다음과 같이 10개의 books를 찾아서 각각의 authors의 last_name을 출력하는 코드를 살펴보겠습니다:
books = Book.limit(10)
books.each do |book|
puts book.author.last_name
end
이 코드는 처음 보면 괜찮아 보입니다. 하지만 실행되는 쿼리의 총 개수에 문제가 있습니다. 위 코드는 1(10개의 책을 찾기 위해) + 10(각 책마다 author를 로드하기 위해 하나씩) = 11개의 쿼리를 총 실행합니다.
13.1.1 N + 1 쿼리 문제의 해결책
Active Record는 로드될 모든 association을 미리 지정할 수 있도록 해줍니다.
사용 가능한 메서드들은 다음과 같습니다:
13.2 includes
includes
를 사용하면, Active Record는 지정된 모든 association들이 가능한 최소한의 쿼리를 사용하여 로드되도록 보장합니다.
위의 경우를 includes
메서드를 사용하여 다시 살펴보면, Book.limit(10)
을 수정하여 authors를 eager load할 수 있습니다:
books = Book.includes(:author).limit(10)
books.each do |book|
puts book.author.last_name
end
위 코드는 원래의 경우처럼 11개의 쿼리가 아닌 단 2개의 쿼리만 실행할 것입니다:
SELECT books.* FROM books LIMIT 10
SELECT authors.* FROM authors
WHERE authors.id IN (1,2,3,4,5,6,7,8,9,10)
13.2.1 다수의 Association을 Eager Loading하기
Active Record는 includes
메소드를 사용할 때 배열, 해시, 또는 배열/해시의 중첩된 해시를 통해 하나의 Model.find
호출로 원하는 만큼의 association을 eager load할 수 있습니다.
13.2.1.1 다수 Association의 배열
Customer.includes(:orders, :reviews)
각각의 customers와 연관된 orders와 reviews를 모두 로드합니다.
13.2.1.2 Nested Associations Hash
Customer.includes(orders: { books: [:supplier, :author] }).find(1)
이 코드는 주어진 customer의 관련된 orders records, books records, supplier records 그리고 author records들을 모두 동시에 로드합니다.
이것은 id가 1인 고객을 찾고, 관련된 모든 orders, orders와 관련된 모든 books, 그리고 각 books의 author와 supplier를 eager load합니다.
13.2.2 Eager Load된 Association에 조건 지정하기
Active Record는 joins
처럼 eager load된 association에도 조건을 지정할 수 있게 해주지만, 권장되는 방법은 joins를 사용하는 것입니다.
하지만 꼭 해야한다면, 일반적인 방법으로 where
를 사용할 수 있습니다.
Author.includes(:books).where(books: { out_of_print: true })
books가 절판(out_of_print)된 Author를 찾는 where 조건을 가진 eager loading을 수행합니다.
이는 joins
메서드가 INNER JOIN
함수를 사용하여 쿼리를 생성하는 것과 달리, LEFT OUTER JOIN
이 포함된 쿼리를 생성합니다.
SELECT authors.id AS t0_r0, ... books.updated_at AS t1_r5 FROM authors LEFT OUTER JOIN books ON books.author_id = authors.id WHERE (books.out_of_print = 1)
where
조건이 없다면 일반적인 두 개의 쿼리가 생성될 것입니다.
이와 같이 where
를 사용하는 것은 Hash를 전달할 때만 작동합니다. SQL
fragments의 경우 joined 테이블을 강제하기 위해 references
를 사용해야 합니다:
Author.includes(:books).where("books.out_of_print = true").references(:books)
includes
와 함께 조건을 사용하기 위해 references
를 명시적으로 호출해야 하는 경우입니다. 이렇게 하면 Active Record는 LEFT OUTER JOIN을 사용하고, 책들이 절판된 모든 저자들을 찾을 것입니다.
이 includes
쿼리에서 어떤 저자도 책을 가지고 있지 않은 경우에도 모든 저자가 로드됩니다. joins
(INNER JOIN)를 사용하면 조인 조건이 반드시 일치해야 하며, 그렇지 않으면 어떤 레코드도 반환되지 않습니다.
조인의 일부로 association이 eager load되는 경우, custom select 절의 필드들은 로드된 모델에 존재하지 않습니다. 이는 그 필드들이 부모 레코드에 나타나야 하는지 자식 레코드에 나타나야 하는지가 모호하기 때문입니다.
13.3 preload
preload
를 사용하면 Active Record가 지정된 각 association을 association당 하나의 쿼리를 사용하여 로드합니다.
N + 1 쿼리 문제로 돌아가서, authors를 preload하도록 Book.limit(10)
을 다시 작성할 수 있습니다:
books = Book.preload(:author).limit(10)
books.each do |book|
puts book.author.last_name
end
위 코드는 원래의 11개 쿼리와는 달리 단 2개의 쿼리만 실행할 것입니다:
SELECT books.* FROM books LIMIT 10
SELECT authors.* FROM authors
WHERE authors.id IN (1,2,3,4,5,6,7,8,9,10)
preload
메소드는 배열, 해시, 또는 배열/해시의 중첩된 해시를 includes
메소드와 동일한 방식으로 사용하여 하나의 Model.find
호출로 여러 association을 로드할 수 있습니다. 하지만 includes
메소드와 달리 preload된 association에 대한 조건을 지정할 수는 없습니다.
13.4 eager_load
eager_load
를 사용하면 Active Record는 LEFT OUTER JOIN
을 사용하여 지정된 모든 association을 로드합니다.
N + 1이 발생했던 케이스를 eager_load
메서드를 사용하여 다시 살펴보면, authors에 대한 Book.limit(10)
을 다음과 같이 다시 작성할 수 있습니다:
books = Book.eager_load(:author).limit(10)
books.each do |book|
puts book.author.last_name
end
위 코드는 원본의 11개의 쿼리와 달리 단 1개의 쿼리만 실행합니다:
SELECT "books"."id" AS t0_r0, "books"."title" AS t0_r1, ... FROM "books"
LEFT OUTER JOIN "authors" ON "authors"."id" = "books"."author_id"
LIMIT 10
eager_load
메서드는 includes
메서드와 동일한 방식으로 배열, 해시, 또는 배열/해시의 중첩 해시를 사용하여 단일 Model.find
호출로 원하는 만큼의 association을 로드할 수 있습니다. 또한 includes
메서드처럼 eager load되는 association에 대한 조건을 지정할 수 있습니다.
13.5 strict_loading
Eager loading은 N + 1 쿼리를 방지할 수 있지만 일부 association들은 여전히 lazy loading될 수 있습니다. 어떤 association도 lazy loading되지 않도록 하기 위해서는 strict_loading
을 활성화할 수 있습니다.
relation에서 strict loading 모드를 활성화하면, 레코드가 어떤 association이라도 lazy loading 하려고 시도할 때 ActiveRecord::StrictLoadingViolationError
가 발생합니다:
user = User.strict_loading.first
user.address.city # ActiveRecord::StrictLoadingViolationError를 발생시킵니다
user.comments.to_a # ActiveRecord::StrictLoadingViolationError를 발생시킵니다
모든 relation에 대해 활성화하려면 config.active_record.strict_loading_by_default
플래그를 true
로 변경하세요.
대신 위반 사항을 logger로 전송하려면 config.active_record.action_on_strict_loading_violation
을 :log
로 변경하세요.
13.6 strict_loading!
strict_loading!
를 호출하여 레코드 자체에서 strict loading을 활성화할 수 있습니다:
user = User.first
user.strict_loading!
user.address.city # ActiveRecord::StrictLoadingViolationError를 발생시킴
user.comments.to_a # ActiveRecord::StrictLoadingViolationError를 발생시킴
strict_loading!
은 :mode
인자도 받습니다. 이를 :n_plus_one_only
로 설정하면 N + 1 쿼리를 발생시키는 association이 lazy loading될 때만 에러가 발생합니다.
user.strict_loading!(mode: :n_plus_one_only)
user.address.city # => "Tatooine"
user.comments.to_a # => [#<Comment:0x00...]
user.comments.first.likes.to_a # ActiveRecord::StrictLoadingViolationError를 발생시킵니다
13.7 연관관계에서의 strict_loading
옵션
strict_loading
옵션을 제공하여 단일 연관관계에 대해 strict loading을 활성화할 수 있습니다:
class Author < ApplicationRecord
has_many :books, strict_loading: true
end
Scopes
Scope를 사용하면 자주 사용하는 쿼리를 정의하여 association 객체나 모델의 메서드 호출로 참조할 수 있습니다. 이러한 scope를 사용하면 where
, joins
, includes
와 같은 이전에 다룬 모든 메서드를 사용할 수 있습니다. 모든 scope 본문은 추가 메서드(다른 scope와 같은)를 호출할 수 있도록 ActiveRecord::Relation
또는 nil
을 반환해야 합니다.
간단한 scope를 정의하려면 클래스 내부에서 scope
메서드를 사용하고, 이 scope가 호출될 때 실행하고자 하는 쿼리를 전달합니다:
class Book < ApplicationRecord
scope :out_of_print, -> { where(out_of_print: true) }
end
이 out_of_print
scope를 호출하려면 클래스에서 호출할 수 있습니다:
irb> Book.out_of_print
=> #<ActiveRecord::Relation> # 절판된 모든 책들
Book
객체로 구성된 association에서도 가능합니다:
irb> author = Author.first
irb> author.books.out_of_print
=> #<ActiveRecord::Relation> # author의 절판된 모든 book들
스코프는 스코프 내에서도 체이닝이 가능합니다:
class Book < ApplicationRecord
scope :out_of_print, -> { where(out_of_print: true) }
scope :out_of_print_and_expensive, -> { out_of_print.where("price > 500") }
end
13.8 인자 전달하기
scope에 인자를 전달할 수 있습니다:
class Book < ApplicationRecord
scope :costs_more_than, ->(amount) { where("price > ?", amount) }
end
scope를 클래스 메서드처럼 호출하세요:
irb> Book.costs_more_than(100.10)
하지만 이것은 class method로 제공받을 수 있는 기능을 단순히 중복하는 것입니다.
class Book < ApplicationRecord
def self.costs_more_than(amount)
where("price > ?", amount)
end
end
이러한 메소드들은 association 객체에서 여전히 접근할 수 있습니다:
irb> author.books.costs_more_than(100.10)
13.9 조건문 사용하기
scope에서는 조건문을 활용할 수 있습니다:
class Order < ApplicationRecord
scope :created_before, ->(time) { where(created_at: ...time) if time.present? }
end
time이 present?
일 때만 쿼리를 조건부로 실행하는 scope를 정의합니다.
다른 예시들과 같이, 이것은 class method처럼 동작할 것입니다.
class Order < ApplicationRecord
def self.created_before(time)
where(created_at: ...time) if time.present? # time이 존재하는 경우 해당 시간 이전에 생성된 주문들을 반환합니다
end
end
하지만 한 가지 중요한 주의사항이 있습니다: scope는 조건문이 false
로 평가되더라도 항상 ActiveRecord::Relation
객체를 반환하는 반면, class method는 nil
을 반환합니다. 이는 조건문이 포함된 class method들을 체이닝할 때, 조건문 중 하나라도 false
를 반환하면 NoMethodError
가 발생할 수 있습니다.
13.10 Default Scope 적용하기
모델의 모든 쿼리에 scope를 적용하고 싶다면, 모델 내부에서 default_scope
메소드를 사용할 수 있습니다.
class Book < ApplicationRecord
default_scope { where(out_of_print: false) }
end
이 model에 대해 query가 실행되면 SQL query는 아래와 같은 형태가 됩니다:
SELECT * FROM books WHERE (out_of_print = false)
만약 default scope로 더 복잡한 작업을 해야 한다면, 이를 클래스 메소드로 정의할 수 있습니다:
class Book < ApplicationRecord
def self.default_scope
# ActiveRecord::Relation을 반환해야 합니다.
end
end
default_scope
는 scope 인자가 Hash
로 주어질 때 레코드를 생성/빌드하는 동안에도 적용됩니다. 레코드를 업데이트하는 동안에는 적용되지 않습니다. 예시:
class Book < ApplicationRecord
default_scope { where(out_of_print: false) }
end
irb> Book.new
=> #<Book id: nil, out_of_print: false>
irb> Book.unscoped.new
=> #<Book id: nil, out_of_print: nil>
주의하세요. Array
형식으로 지정된 default_scope
쿼리 인수는 기본 속성 할당을 위한 Hash
로 변환될 수 없습니다. 예를 들면:
class Book < ApplicationRecord
default_scope { where("out_of_print = ?", false) }
end
irb> Book.new
=> #<Book id: nil, out_of_print: nil>
13.11 Scope의 병합
where
절과 마찬가지로 scope는 AND
조건을 사용하여 병합됩니다.
class Book < ApplicationRecord
scope :in_print, -> { where(out_of_print: false) } # 절판되지 않은 도서
scope :out_of_print, -> { where(out_of_print: true) } # 절판된 도서
scope :recent, -> { where(year_published: 50.years.ago.year..) } # 최근 50년 이내 출판된 도서
scope :old, -> { where(year_published: ...50.years.ago.year) } # 50년 이전에 출판된 도서
end
irb> Book.out_of_print.old
SELECT books.* FROM books WHERE books.out_of_print = 'true' AND books.year_published < 1969
scope
와 where
조건을 섞어서 사용할 수 있으며, 최종 SQL에서는 모든 조건들이 AND
로 연결됩니다.
irb> Book.in_print.where(price: ...100)
SELECT books.* FROM books WHERE books.out_of_print = 'false' AND books.price < 100
만약 마지막 where
절이 우선순위를 갖도록 하고 싶다면 merge
를 사용할 수 있습니다.
irb> Book.in_print.merge(Book.out_of_print)
SELECT books.* FROM books WHERE books.out_of_print = true
한 가지 중요한 주의사항은 default_scope
가 scope
와 where
조건문에서 앞에 추가된다는 것입니다.
class Book < ApplicationRecord
# 50년 전부터 현재까지 출판된 도서만 기본으로 조회합니다
default_scope { where(year_published: 50.years.ago.year..) }
# 절판되지 않은 도서만 조회합니다
scope :in_print, -> { where(out_of_print: false) }
# 절판된 도서만 조회합니다
scope :out_of_print, -> { where(out_of_print: true) }
end
irb> Book.all
SELECT books.* FROM books WHERE (year_published >= 1969)
irb> Book.in_print
SELECT books.* FROM books WHERE (year_published >= 1969) AND books.out_of_print = false
irb> Book.where('price > 50')
SELECT books.* FROM books WHERE (year_published >= 1969) AND (price > 50)
위에서 볼 수 있듯이 default_scope
는 scope
와 where
조건 모두에 병합됩니다.
13.12 모든 Scoping 제거하기
어떤 이유로든 scoping을 제거하고 싶다면 [unscoped
][] 메서드를 사용할 수 있습니다. 이는 특히 모델에 default_scope
이 지정되어 있고 특정 쿼리에서는 이를 적용하고 싶지 않을 때 유용합니다.
Book.unscoped를 실행하고 즉시 레코드를 로드합니다.
이 메서드는 모든 scope를 제거하고 테이블에 대해 일반적인 query를 수행합니다.
irb> Book.unscoped.all
SELECT books.* FROM books
irb> Book.where(out_of_print: true).unscoped.all
SELECT books.* FROM books
unscoped
는 블록을 받을 수도 있습니다:
irb> Book.unscoped { Book.out_of_print }
SELECT books.* FROM books WHERE books.out_of_print = true
14 Dynamic Finders
테이블에서 정의한 모든 필드(속성이라고도 함)에 대해 Active Record는 finder 메서드를 제공합니다. 예를 들어 Customer
모델에 first_name
이라는 필드가 있다면, Active Record는 자동으로 find_by_first_name
인스턴스 메서드를 제공합니다. Customer
모델에 locked
필드도 있다면, find_by_locked
메서드도 함께 제공됩니다.
레코드를 찾지 못했을 때 ActiveRecord::RecordNotFound
에러를 발생시키려면 dynamic finder 끝에 느낌표(!
)를 지정할 수 있습니다. 예: Customer.find_by_first_name!("Ryan")
first_name
과 orders_count
모두로 찾고 싶다면, 필드 사이에 "and
"를 입력하여 이러한 finder들을 연결할 수 있습니다. 예: Customer.find_by_first_name_and_orders_count("Ryan", 5)
.
15 Enums
enum을 사용하면 속성에 대한 값들의 Array를 정의하고 이름으로 참조할 수 있습니다. 데이터베이스에 실제로 저장되는 값은 값들 중 하나에 매핑된 정수입니다.
enum을 선언하면 다음과 같은 것들이 생성됩니다:
- enum 값을 가지고 있거나 가지고 있지 않은 모든 객체를 찾는 데 사용할 수 있는 scope
- 객체가 특정 enum 값을 가지고 있는지 확인하는 데 사용할 수 있는 인스턴스 메서드
- 객체의 enum 값을 변경하는 데 사용할 수 있는 인스턴스 메서드
enum의 가능한 모든 값에 대해 위의 것들이 생성됩니다.
예를 들어, 다음과 같은 enum
선언이 있다고 가정해봅시다:
class Order < ApplicationRecord
enum :status, [:shipped, :being_packaged, :complete, :cancelled]
end
이러한 scopes는 자동으로 생성되며 특정 status
값을 가진 또는 가지지 않은 모든 객체를 찾는 데 사용할 수 있습니다:
irb> Order.shipped
=> #<ActiveRecord::Relation> # status == :shipped인 모든 orders
irb> Order.not_shipped
=> #<ActiveRecord::Relation> # status != :shipped인 모든 orders
이러한 인스턴스 메서드들은 자동으로 생성되며 모델이 status
enum에 대해 해당 값을 가지고 있는지 확인합니다:
irb> order = Order.shipped.first
irb> order.shipped?
=> true
irb> order.complete?
=> false
이러한 인스턴스 메서드들은 자동으로 생성되며, 먼저 status
값을 지정된 값으로 업데이트한 다음 상태가 해당 값으로 성공적으로 설정되었는지 여부를 확인합니다:
irb> order = Order.first
irb> order.shipped!
UPDATE "orders" SET "status" = ?, "updated_at" = ? WHERE "orders"."id" = ? [["status", 0], ["updated_at", "2019-01-24 07:13:08.524320"], ["id", 1]]
=> true
enum에 대한 전체 문서는 여기에서 찾을 수 있습니다.
16 Method Chaining 이해하기
Active Record 패턴은 Method Chaining을 구현하며, 이를 통해 여러 Active Record 메서드를 간단하고 명확한 방식으로 함께 사용할 수 있습니다.
이전에 호출된 메서드가 all
, where
, joins
와 같은 ActiveRecord::Relation
을 반환할 때 구문에서 메서드를 체이닝할 수 있습니다. 단일 객체를 반환하는 메서드(단일 객체 검색 섹션 참조)는 구문의 마지막에 있어야 합니다.
아래에 몇 가지 예시가 있습니다. 이 가이드는 모든 가능성을 다루지는 않고 예시로 몇 가지만 다룰 것입니다. Active Record 메서드가 호출될 때 쿼리는 즉시 생성되어 데이터베이스로 전송되지 않습니다. 쿼리는 실제로 데이터가 필요할 때만 전송됩니다. 그래서 아래의 각 예시는 단일 쿼리를 생성합니다.
16.1 여러 테이블에서 필터링된 데이터 가져오기
Customer
.select("customers.id, customers.last_name, reviews.body")
.joins(:reviews)
.where("reviews.created_at > ?", 1.week.ago)
1주일 전 이후에 작성된 review가 있는 customer의 id와 last_name, 그리고 review의 body를 조회합니다.
결과는 다음과 같이 나타날 것입니다:
SELECT customers.id, customers.last_name, reviews.body
FROM customers
INNER JOIN reviews
ON reviews.customer_id = customers.id
WHERE (reviews.created_at > '2019-01-08')
16.2 여러 테이블로부터 특정 데이터 검색하기
Book
.select("books.id, books.title, authors.first_name")
.joins(:author)
.find_by(title: "Abstraction and Specification in Program Development")
이 query는 books 테이블에서 id와 title을 선택하고 authors 테이블에서 first_name을 선택하며, author association을 통해 join을 하고, 제목이 "Abstraction and Specification in Program Development"인 책을 찾습니다.
위 내용은 다음과 같이 생성됩니다:
SELECT books.id, books.title, authors.first_name
FROM books
INNER JOIN authors
ON authors.id = books.author_id
WHERE books.title = $1 [["title", "Abstraction and Specification in Program Development"]]
LIMIT 1
만약 쿼리가 여러 레코드와 일치하는 경우 find_by
는 첫번째 것만 가져오고 나머지는 무시합니다(위의 LIMIT 1
구문 참조).
17 새 객체 찾기 또는 생성하기
레코드를 찾거나 존재하지 않을 경우 생성해야 하는 경우가 흔합니다. find_or_create_by
와 find_or_create_by!
메서드를 사용하여 이 작업을 수행할 수 있습니다.
17.1 find_or_create_by
find_or_create_by
메서드는 지정된 속성을 가진 레코드가 존재하는지 확인합니다. 존재하지 않으면 create
가 호출됩니다. 예시를 살펴보겠습니다.
"Andy"라는 이름의 고객을 찾고, 없다면 새로 생성하고 싶다고 가정해봅시다. 다음과 같이 실행할 수 있습니다:
irb> Customer.find_or_create_by(first_name: 'Andy')
=> #<Customer id: 5, first_name: "Andy", last_name: nil, title: nil, visits: 0, orders_count: nil, lock_version: 0, created_at: "2019-01-17 07:06:45", updated_at: "2019-01-17 07:06:45">
이 메서드로 생성되는 SQL은 다음과 같습니다:
customers에서 (customers.first_name = 'Andy')인 모든 컬럼을 LIMIT 1로 선택
BEGIN
customers 테이블에 (created_at, first_name, locked, orders_count, updated_at) 컬럼에 ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57') 값을 삽입
COMMIT
find_or_create_by
는 이미 존재하는 레코드나 새로운 레코드를 반환합니다. 이 경우 Andy라는 이름을 가진 customer가 없었기 때문에 새로운 레코드가 생성되어 반환됩니다.
새로운 레코드가 데이터베이스에 저장되지 않을 수 있습니다. 이는 create
와 마찬가지로 validation을 통과했는지 여부에 따라 달라집니다.
새로운 레코드를 생성할 때 'locked' 속성을 false
로 설정하고 싶지만, 이를 쿼리에 포함시키고 싶지 않다고 가정해봅시다. 즉, "Andy"라는 이름의 customer를 찾거나, 해당 customer가 존재하지 않을 경우 잠기지 않은 상태의 "Andy"라는 이름의 customer를 생성하고 싶습니다.
이를 달성하는 방법에는 두 가지가 있습니다. 첫 번째는 create_with
를 사용하는 것입니다:
Customer.create_with(locked: false).find_or_create_by(first_name: "Andy")
Customer를 찾을 때나 생성할 때 locked
를 false
로 설정하려면 create_with
로 이 기본값을 지정할 수 있습니다.
두 번째 방법은 block을 사용하는 것입니다:
Customer.find_or_create_by(first_name: "Andy") do |c|
c.locked = false
end
find_or_create_by
와 블록을 함께 사용할 수 있습니다. 주어진 속성을 가진 레코드를 찾거나 새 레코드를 만들 때 블록이 실행됩니다.
이 block은 customer가 생성될 때만 실행됩니다. 두 번째로 이 코드를 실행할 때는 block이 무시됩니다.
17.2 find_or_create_by!
find_or_create_by!
를 사용하여 새 레코드가 유효하지 않은 경우 예외를 발생시킬 수도 있습니다. 이 가이드에서는 validation에 대해 다루지 않지만, 잠시 다음을 추가한다고 가정해보겠습니다.
validates :orders_count, presence: true
orders_count가 존재하고 nil이 아닌지 검증합니다.
Customer
모델에 포함됩니다. orders_count
를 전달하지 않고 새로운 Customer
를 생성하려고 시도하면, 레코드는 유효하지 않게 되고 예외가 발생할 것입니다:
irb> Customer.find_or_create_by!(first_name: 'Andy')
ActiveRecord::RecordInvalid: 유효성 검사 실패: Orders count는 비워둘 수 없습니다
17.3 find_or_initialize_by
[find_or_initialize_by
][] 메서드는 find_or_create_by
와 비슷하게 작동하지만 create
대신 new
를 호출합니다. 이는 새로운 model 인스턴스가 메모리에 생성되지만 데이터베이스에는 저장되지 않는다는 것을 의미합니다. find_or_create_by
예제를 이어서, 'Nina'라는 이름의 customer를 찾아보겠습니다:
irb> nina = Customer.find_or_initialize_by(first_name: 'Nina')
=> #<Customer id: nil, first_name: "Nina", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
irb> nina.persisted? # 데이터베이스에 저장되었는지 확인
=> false
irb> nina.new_record? # 새로운 레코드인지 확인
=> true
객체가 아직 데이터베이스에 저장되지 않았기 때문에, 다음과 같은 SQL이 생성됩니다:
SELECT * FROM customers WHERE (customers.first_name = 'Nina') LIMIT 1
데이터베이스에 저장하고 싶을 때는, 단순히 save
를 호출하세요:
irb> nina.save
=> true
18 SQL로 찾기
테이블에서 레코드를 찾기 위해 직접 SQL을 사용하고 싶다면 find_by_sql
를 사용할 수 있습니다. find_by_sql
메서드는 기본 쿼리가 단일 레코드만 반환하더라도 객체 배열을 반환합니다. 예를 들어 다음과 같은 쿼리를 실행할 수 있습니다:
irb> Customer.find_by_sql("SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id ORDER BY customers.created_at desc")
=> [#<Customer id: 1, first_name: "Lucas" ...>, #<Customer id: 2, first_name: "Jan" ...>, ...]
find_by_sql
은 데이터베이스에 커스텀 쿼리를 실행하고 인스턴스화된 객체를 검색하는 간단한 방법을 제공합니다.
18.1 select_all
find_by_sql
에는 [lease_connection.select_all
][]이라는 유사한 메서드가 있습니다. select_all
은 find_by_sql
처럼 사용자 정의 SQL을 사용하여 데이터베이스에서 객체를 검색하지만, 인스턴스화하지는 않습니다. 이 메서드는 ActiveRecord::Result
클래스의 인스턴스를 반환하며, 이 객체에서 to_a
를 호출하면 각 해시가 레코드를 나타내는 해시 배열을 반환합니다.
irb> Customer.lease_connection.select_all("SELECT first_name, created_at FROM customers WHERE id = '1'").to_a
=> [{"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"}, {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}]
18.2 pluck
pluck
는 현재 relation에서 지정된 칼럼(들)의 값을 가져오는 데 사용할 수 있습니다. 인자로 칼럼 이름 목록을 받아서 지정된 칼럼들의 값을 해당 데이터 타입으로 배열로 반환합니다.
irb> Book.where(out_of_print: true).pluck(:id)
SELECT id FROM books WHERE out_of_print = true
=> [1, 2, 3]
irb> Order.distinct.pluck(:status)
SELECT DISTINCT status FROM orders
=> ["shipped", "being_packed", "cancelled"]
irb> Customer.pluck(:id, :first_name)
SELECT customers.id, customers.first_name FROM customers
=> [[1, "David"], [2, "Fran"], [3, "Jose"]]
pluck
는 다음과 같은 코드를 대체할 수 있게 해줍니다:
Customer.select(:id).map { |c| c.id }
# 또는
Customer.select(:id).map(&:id)
# 또는
Customer.select(:id, :first_name).map { |c| [c.id, c.first_name] }
with: 키워드를 사용하여 데이터베이스를 채우는 데 사용할 객체를 명시할 수 있습니다. 이 객체는
create` 메소드에 전달되는 어떤 것이든 될 수 있습니다. 데이터베이스에 넣을 속성을 가진 해시부터 객체 그 자체까지 모두 가능합니다.
마찬가지로 create
메소드에 블록을 넘길 수도 있습니다:
# calls Book.create with a block
Book.destroy_all
book = Book.create do |book|
book.name = "Blink"
book.title = "Blink"
book.author = "Malcolm Gladwell"
end
다음 코드는 모든 대상에서 동일한 블록을 호출합니다:
5.times do
Book.create do |book|
book.name = "Blink"
book.title = "Blink"
book.author = "Malcolm Gladwell"
end
end
Customer.pluck(:id)
# 또는
Customer.pluck(:id, :first_name)
select
와 달리 pluck
은 데이터베이스 결과를 ActiveRecord
객체를 생성하지 않고 직접 Ruby Array
로 변환합니다. 이는 대규모이거나 자주 실행되는 쿼리의 경우 성능이 더 좋을 수 있습니다. 하지만 모델 메서드의 오버라이드는 사용할 수 없습니다. 예를 들어:
class Customer < ApplicationRecord
def name
"나는 #{first_name}입니다"
end
end
irb> Customer.select(:first_name).map &:name
=> ["나는 David입니다", "나는 Jeremy입니다", "나는 Jose입니다"]
irb> Customer.pluck(:first_name)
=> ["David", "Jeremy", "Jose"]
단일 테이블의 필드만 쿼리할 필요는 없으며, 여러 테이블을 함께 쿼리할 수도 있습니다.
irb> Order.joins(:customer, :books).pluck("orders.created_at, customers.email, books.title")
게다가 select
나 다른 Relation
scope들과는 달리 pluck
은 즉시 쿼리를 실행하며, 따라서 추가적인 scope들과 체이닝할 수 없습니다. 하지만 이전에 이미 구성된 scope들과는 함께 작동할 수 있습니다:
irb> Customer.pluck(:first_name).limit(1)
NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>
irb> Customer.limit(1).pluck(:first_name)
=> ["David"]
pluck
을 사용할 때, relation 객체가 include 값을 포함하고 있다면 쿼리에 필요하지 않더라도 eager loading이 트리거된다는 점을 알아야 합니다. 예를 들면:
irb> assoc = Customer.includes(:reviews)
irb> assoc.pluck(:id)
SELECT "customers"."id" FROM "customers" LEFT OUTER JOIN "reviews" ON "reviews"."id" = "customers"."review_id"
이를 피하는 한 가지 방법은 includes를 unscope
하는 것입니다:
irb> assoc.unscope(:includes).pluck(:id)
18.3 pick
pick
은 현재 relation에서 지정된 컬럼의 값을 선택하는데 사용될 수 있습니다. 컬럼명의 리스트를 인자로 받아서 지정된 컬럼 값들의 첫 번째 행을 해당 데이터 타입과 함께 반환합니다.
pick
은 relation.limit(1).pluck(*column_names).first
의 단축 버전이며, 주로 이미 하나의 행으로 제한된 relation을 가지고 있을 때 유용합니다.
pick
은 다음과 같은 코드를 대체할 수 있게 해줍니다:
Customer.where(id: 1).pluck(:id).first
전치사 "with"은 매우 흔한 용어입니다. 하지만 Ruby에서는 특별한 의미를 가지고 있습니다:
with_options prefix: 'admin' do |admin|
admin.resources :posts
admin.resources :comments
end
위의 패턴은 동일한 설정을 공유하는 리소스 그룹을 정의하는 데 자주 사용됩니다. DRY(같은 것을 반복하지 않는다)에 매우 유용합니다.
이는 다음 코드와 정확히 동일합니다:
resources :posts, prefix: 'admin'
resources :comments, prefix: 'admin'
위의 코드는 다음 매칭되는 URL과 Helper를 생성합니다:
admin_posts GET /admin/posts(.:format) posts#index
POST /admin/posts(.:format) posts#create
new_admin_post GET /admin/posts/new(.:format) posts#new
edit_admin_post GET /admin/posts/:id/edit(.:format) posts#edit
admin_post GET /admin/posts/:id(.:format) posts#show
PATCH/PUT /admin/posts/:id(.:format) posts#update
DELETE /admin/posts/:id(.:format) posts#destroy
Customer.where(id: 1).pick(:id)
18.4 ids
[ids
][]는 테이블의 primary key를 사용하여 relation의 모든 ID를 pluck 하는데 사용할 수 있습니다.
irb> Customer.ids
SELECT id FROM customers
class Customer < ApplicationRecord
self.primary_key = "customer_id"
end
irb> Customer.ids
SELECT customer_id FROM customers
19 Existence of Objects
객체가 존재하는지 단순히 확인하고 싶다면 exists?
라는 메서드를 사용할 수 있습니다. 이 메서드는 find
와 동일한 쿼리를 사용하여 데이터베이스를 조회하지만, 객체나 객체 컬렉션을 반환하는 대신 true
또는 false
를 반환합니다.
Customer.exists?(1)
Customer 레코드의 ID가 1인 레코드가 존재하는지 확인합니다.
exists?
메서드는 여러 값을 받을 수 있지만, 주의할 점은 이러한 레코드들 중 하나라도 존재하면 true
를 반환한다는 것입니다.
Customer.exists?(id: [1, 2, 3])
# 또는
Customer.exists?(first_name: ["Jane", "Sergei"])
모델이나 relation에서 어떤 인수도 없이 exists?
를 사용하는 것도 가능합니다.
Customer.where(first_name: "Ryan").exists?
where 조건절 안에 명시된 조건을 만족하는 record가 하나 이상 존재하는지 확인합니다.
위 코드는 first_name
이 'Ryan'인 customer가 적어도 하나 있으면 true
를 반환하고, 그렇지 않으면 false
를 반환합니다.
exists?
를 호출하는 것은 해당 모델의 테이블에 레코드가 한 개 이상 존재하는 지를 확인하는 가장 단순한 방법입니다.
위의 코드는 customers
테이블이 비어있으면 false
를, 그렇지 않으면 true
를 반환합니다.
모델이나 relation에 대해 존재 여부를 확인하기 위해 any?
와 many?
도 사용할 수 있습니다. many?
는 아이템이 존재하는지 확인하기 위해 SQL count
를 사용합니다.
# model을 통해
Order.any?
# SELECT 1 FROM orders LIMIT 1
Order.many?
# SELECT COUNT(*) FROM (SELECT 1 FROM orders LIMIT 2)
# named scope을 통해
Order.shipped.any?
# SELECT 1 FROM orders WHERE orders.status = 0 LIMIT 1
Order.shipped.many?
# SELECT COUNT(*) FROM (SELECT 1 FROM orders WHERE orders.status = 0 LIMIT 2)
# relation을 통해
Book.where(out_of_print: true).any?
Book.where(out_of_print: true).many?
# association을 통해
Customer.first.orders.any?
Customer.first.orders.many?
20 Calculations
이 섹션의 서두에서는 count
를 예시 메서드로 사용하지만, 설명하는 옵션들은 모든 하위 섹션에 적용됩니다.
모든 계산 메서드는 모델에서 직접 동작합니다:
irb> Customer.count
SELECT COUNT(*) FROM customers
또는 relation에서:
irb> Customer.where(first_name: 'Ryan').count
SELECT COUNT(*) FROM customers WHERE (first_name = 'Ryan')
relation에서 복잡한 계산을 수행하기 위해 다양한 finder 메서드들을 사용할 수 있습니다:
irb> Customer.includes("orders").where(first_name: 'Ryan', orders: { status: 'shipped' }).count
이는 Ryan이라는 first_name을 가진 Customer 중에서 shipped 상태의 orders를 가진 고객의 수를 계산합니다.
실행될 내용:
SELECT COUNT(DISTINCT customers.id) FROM customers
LEFT OUTER JOIN orders ON orders.customer_id = customers.id
WHERE (customers.first_name = 'Ryan' AND orders.status = 0)
Order가 enum status: [ :shipped, :being_packed, :cancelled ]
를 가지고 있다고 가정합니다.
20.1 count
모델 테이블에 있는 레코드의 수를 확인하고 싶다면 Customer.count
를 호출하면 그 숫자가 반환됩니다.
더 구체적으로 데이터베이스에 title이 있는 모든 customer를 찾고 싶다면 Customer.count(:title)
을 사용할 수 있습니다.
옵션에 대해서는 상위 섹션인 Calculations를 참조하세요.
20.2 average
테이블의 특정 숫자의 평균을 보고 싶다면 해당 테이블과 관련된 클래스에서 average
메서드를 호출할 수 있습니다. 이 메서드 호출은 다음과 같습니다:
Order.average("subtotal")
평균값을 구합니다. subtotal 열의 평균값을 반환합니다.
이는 해당 field의 평균값을 나타내는 숫자(3.14159265와 같은 부동소수점 숫자일 수 있음)를 반환합니다.
옵션에 대해서는 상위 섹션인 Calculations를 참조하세요.
20.3 minimum
테이블의 특정 필드에서 최소값을 찾으려면 해당 테이블과 관련된 클래스에서 minimum
메서드를 호출할 수 있습니다. 이 메서드 호출은 다음과 같은 형태입니다:
Order.minimum("subtotal")
주문(Order) 테이블의 subtotal 컬럼의 최솟값을 반환합니다.
옵션에 대해서는 상위 섹션인 Calculations를 참조하세요.
20.4 maximum
테이블의 필드에서 최대값을 찾으려면 테이블과 관련된 class에서 maximum
메서드를 호출할 수 있습니다. 이 메서드 호출은 다음과 같이 사용됩니다:
Order.maximum("subtotal")
subtotal 컬럼의 최대값을 계산합니다.
옵션에 대해서는 상위 섹션인 Calculations을 참조하세요.
20.5 sum
테이블의 모든 레코드에 대한 필드의 합계를 구하려면 해당 테이블과 관련된 클래스에서 sum
메서드를 호출할 수 있습니다. 이 메서드 호출은 다음과 같이 보일 것입니다:
Order.sum("subtotal")
옵션에 대해서는 상위 섹션인 Calculations를 참조하세요.
21 EXPLAIN 실행하기
relation에 대해 explain
을 실행할 수 있습니다. EXPLAIN 출력은 데이터베이스마다 다릅니다.
예를 들어, 다음을 실행하면:
Customer.where(id: 1).joins(:orders).explain
위 쿼리는 Customer
모델과 관련된 orders
테이블을 JOIN 하는 쿼리의 실행 계획을 표시합니다.
MySQL과 MariaDB에서는 다음과 같이 출력될 수 있습니다:
EXPLAIN SELECT `customers`.* FROM `customers` INNER JOIN `orders` ON `orders`.`customer_id` = `customers`.`id` WHERE `customers`.`id` = 1
+----+-------------+------------+-------+---------------+
| id | select_type | table | type | possible_keys |
+----+-------------+------------+-------+---------------+
| 1 | SIMPLE | customers | const | PRIMARY |
| 1 | SIMPLE | orders | ALL | NULL |
+----+-------------+------------+-------+---------------+
+---------+---------+-------+------+-------------+
| key | key_len | ref | rows | Extra |
+---------+---------+-------+------+-------------+
| PRIMARY | 4 | const | 1 | |
| NULL | NULL | NULL | 1 | Using where |
+---------+---------+-------+------+-------------+
2개의 행이 설정됨 (0.00 초)
Active Record는 해당 데이터베이스 셸과 유사한 형태로 예쁘게 출력을 수행합니다. 따라서 PostgreSQL adapter로 동일한 쿼리를 실행하면 다음과 같이 출력됩니다:
EXPLAIN SELECT "customers".* FROM "customers" INNER JOIN "orders" ON "orders"."customer_id" = "customers"."id" WHERE "customers"."id" = $1 [["id", 1]]
실행 계획
------------------------------------------------------------------------------
Nested Loop (비용=4.33..20.85 행=4 너비=164)
-> Index Scan using customers_pkey on customers (비용=0.15..8.17 행=1 너비=164)
Index Cond: (id = '1'::bigint)
-> Bitmap Heap Scan on orders (비용=4.18..12.64 행=4 너비=8)
Recheck Cond: (customer_id = '1'::bigint)
-> Bitmap Index Scan on index_orders_on_customer_id (비용=0.00..4.18 행=4 너비=0)
Index Cond: (customer_id = '1'::bigint)
(7 rows)
Eager loading은 내부적으로 두 개 이상의 쿼리를 트리거할 수 있으며, 일부 쿼리는 이전 쿼리의 결과가 필요할 수 있습니다. 이러한 이유로 explain
은 실제로 쿼리를 실행하고, 그 후에 쿼리 계획을 요청합니다. 예를 들어, 다음을 실행하면:
Customer.where(id: 1).includes(:orders).explain
includes
와 연결된 SQL 쿼리들을 보고 싶으면 explain
을 사용할 수 있습니다. explain은 실행되는 모든 쿼리에 관한 세부 정보를 보여줍니다.
MySQL과 MariaDB의 경우 다음과 같은 결과가 나올 수 있습니다:
EXPLAIN SELECT `customers`.* FROM `customers` WHERE `customers`.`id` = 1
+----+-------------+-----------+-------+---------------+
| id | select_type | table | type | possible_keys |
+----+-------------+-----------+-------+---------------+
| 1 | SIMPLE | customers | const | PRIMARY |
+----+-------------+-----------+-------+---------------+
+---------+---------+-------+------+-------+
| key | key_len | ref | rows | Extra |
+---------+---------+-------+------+-------+
| PRIMARY | 4 | const | 1 | |
+---------+---------+-------+------+-------+
1개의 행이 집합에 있습니다 (0.00 초)
EXPLAIN SELECT `orders`.* FROM `orders` WHERE `orders`.`customer_id` IN (1)
+----+-------------+--------+------+---------------+
| id | select_type | table | type | possible_keys |
+----+-------------+--------+------+---------------+
| 1 | SIMPLE | orders | ALL | NULL |
+----+-------------+--------+------+---------------+
+------+---------+------+------+-------------+
| key | key_len | ref | rows | Extra |
+------+---------+------+------+-------------+
| NULL | NULL | NULL | 1 | Using where |
+------+---------+------+------+-------------+
1개의 행이 집합에 있습니다 (0.00 초)
그리고 PostgreSQL의 경우 다음과 같이 출력될 수 있습니다:
Customer Load (0.3ms) SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 [["id", 1]]
Order Load (0.3ms) SELECT "orders".* FROM "orders" WHERE "orders"."customer_id" = $1 [["customer_id", 1]]
=> EXPLAIN SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 [["id", 1]]
쿼리 계획
----------------------------------------------------------------------------------
인덱스 스캔: customers_pkey를 사용하여 customers 테이블에 대해 (비용=0.15..8.17 rows=1 width=164)
인덱스 조건: (id = '1'::bigint)
(2 rows)
21.1 Explain 옵션
데이터베이스와 adapter가 지원하는 경우(현재 PostgreSQL, MySQL, MariaDB), 더 자세한 분석을 위해 옵션을 전달할 수 있습니다.
PostgreSQL을 사용할 때 다음과 같이 할 수 있습니다:
Customer.where(id: 1).joins(:orders).explain(:analyze, :verbose)
위 쿼리는 주어진 id
를 가진 모든 customer 레코드와 그들의 orders를 검색하고, 사용된 쿼리 실행 계획의 자세한 분석 정보를 표시합니다. :analyze
옵션은 실제 실행 통계를 포함하고, :verbose
옵션은 모든 세부 정보를 출력합니다.
yields:
EXPLAIN (ANALYZE, VERBOSE) SELECT "shop_accounts".* FROM "shop_accounts" INNER JOIN "customers" ON "customers"."id" = "shop_accounts"."customer_id" WHERE "shop_accounts"."id" = $1 [["id", 1]]
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop (비용=0.30..16.37 행=1 너비=24) (실제 시간=0.003..0.004 행=0 반복=1)
출력: shop_accounts.id, shop_accounts.customer_id, shop_accounts.customer_carrier_id
내부 유일: true
-> 인덱스 스캔 using shop_accounts_pkey on public.shop_accounts (비용=0.15..8.17 행=1 너비=24) (실제 시간=0.003..0.003 행=0 반복=1)
출력: shop_accounts.id, shop_accounts.customer_id, shop_accounts.customer_carrier_id
인덱스 조건: (shop_accounts.id = '1'::bigint)
-> 인덱스 전용 스캔 using customers_pkey on public.customers (비용=0.15..8.17 행=1 너비=8) (실행되지 않음)
출력: customers.id
인덱스 조건: (customers.id = shop_accounts.customer_id)
힙 가져오기: 0
계획 시간: 0.063 ms
실행 시간: 0.011 ms
(12 rows)
MySQL 또는 MariaDB를 사용하는 경우 다음과 같습니다:
Customer.where(id: 1).joins(:orders).explain(:analyze)
위 코드는 PostgreSQL의 실제 쿼리 실행 계획과 실제 런타임 통계를 포함한 쿼리 분석을 반환합니다.
yields: 결과를 반환합니다:
ANALYZE SELECT `shop_accounts`.* FROM `shop_accounts` INNER JOIN `customers` ON `customers`.`id` = `shop_accounts`.`customer_id` WHERE `shop_accounts`.`id` = 1
+----+-------------+-------+------+---------------+------+---------+------+------+--------+----------+------------+--------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | r_rows | filtered | r_filtered | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+--------+----------+------------+--------------------------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | const 테이블에서 일치하는 행 없음 |
+----+-------------+-------+------+---------------+------+---------+------+------+--------+----------+------------+--------------------------------+
1 row in set (0.00 sec)
21.2 EXPLAIN 해석하기
EXPLAIN 출력을 해석하는 것은 본 가이드의 범위를 벗어납니다. 다음 링크들이 도움이 될 수 있습니다:
SQLite3: EXPLAIN QUERY PLAN
MySQL: EXPLAIN Output Format
MariaDB: EXPLAIN
PostgreSQL: Using EXPLAIN