-
[레일즈] Active Record Query Interface 2Ruby on Rails 2021. 4. 26. 20:31
이전 포스팅에서 계속해서 액티브 레코드 쿼리 인터페이스를 알아보겠습니다!!
Ordering
order 메소드를 사용하면 DB에서 특정 순서로 레코드를 반환할 수 있습니다.
예를 들어, 우리가 테이블의 created_at 필드(칼럼)를 기준으로 레코드 집합을 오름차순으로 정렬하려면 아래와 같이 작성하면 됩니다.
Client.order(:created_at) # OR Client.order("created_at")
또한, 다음과 같이 작성해 ASC 또는 DESC 를 사용해 오름차순, 내림차순으로 데이터를 불러올 수 있습니다.
Client.order(created_at: :desc) # OR Client.order(created_at: :asc) # OR Client.order("created_at DESC") # OR Client.order("created_at ASC")
또한, 여러 필드에도 적용할 수 있습니다.
Client.order(orders_count: :asc, created_at: :desc) # OR Client.order(:orders_count, created_at: :desc) # OR Client.order("orders_count ASC, created_at DESC") # OR Client.order("orders_count ASC", "created_at DESC")
Board.order(created_at: :desc) 이전에 만든 프로젝트에 적용해보면 Board.order(created_at: :desc) 를 활용해 최근 작성된 board 가 맨 위로 나오게 할 수도 있습니다.
만약 order를 여러번 적용하려면, 첫번째 order 뒤에 연속적으로 작성하면 됩니다.
Client.order("orders_count ASC").order("created_at DESC") # SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC
참고로 대부분의 DB 시스템에서 select, pluck, id 와 같은 메소드를 사용해 결과 집합과 구분되는 필드를 선택할 때, order 메소드에 사용된 필드가 선택 목록에 포함되지 않으면 ActiveRecord::StatementInvalid 에러가 발생합니다.
결과 집합에서 필드를 select 하기 위해 계속해서 Selecting Specific Fields 를 살펴보겠습니다.
Selecting Specific Fields
디폴트 값으로 Model.find 는 select * 을 사용해 결과 집합에서 모든 필드를 선택합니다.
결과 집합에서 부분집합 필드만 선택하려면, select 메소드를 사용해 부분집합을 선택할 수 있습니다.
형식은 Model.select(:칼럼명, : 칼럼명, ...) 입니다.
예를 들어, viewable_by 와 locked 컬럼만을 선택하려면, 다음과 같이 입력합니다.
Client.select(:viewable_by, :locked) # OR Client.select("viewable_by, locked")
이 호출과 동일한 SQL 쿼리는 다음과 같습니다.
SELECT viewable_by, locked FROM clients
select 는 선택한 필드만으로 모델 객체를 초기화한다는 의미이므로 주의해야 합니다. 초기화된 레코드에 없는 필드에 접근하면 아래 에러 메세지가 나옵니다.
ActiveModel::MissingAttributeError: missing attribute: <attribute>
여기에서 <attribute> 는 요청한 속성이며, id 메소드는 ActiveRecord::MissingAttributeError를 발생시키지 않기 때문에 관계(associations) 관련 기능을 사용할 경우 조심해야 합니다. 관계 기능은 적절히 동작하기 위해 id 메소드를 필요로 하기 때문입니다.
만약 특정 필드의 고유 값에 해당하는 단일 레코드만 가져오려는 경우 distinct 를 사용할 수 있습니다.
Client.select(:name).distinct SELECT DISTINCT name FROM clients
Board.select(:title).distinct 또한, 고유성 제한 조건을 제거할 수도 있습니다.
query = Client.select(:name).distinct # => Returns unique names query.distinct(false) # => Returns all names, even if there are duplicates
현재 Board는 위 이미지와 같이 데이터를 가지고 있는데, 중복 이라는 게시물 2개를 생성해두고 Board.select(:title).distinct 를 실행하면 중복인 게시물이 하나만 호출됩니다.
distinct(false)를 통해 고유성 제안 조건을 제거해보겠습니다.
아래 이미지는 query = Board.select(:title).distinct 로 query 를 지정하고, query.distinct(false) 를 호출한 결과입니다. 단순히 distinct 로 호출한 것과 달리 중복된 title 을 가진 레코드가 모두 호출됩니다.
Limit and Offset
Model.find 에서 실행한 SQL 에 LIMIT을 적용하려면 관계(relation)에서 limit 과 offset 메소드를 사용해 LIMIT을 지정할 수 있습니다.
limit 을 사용해 반환되는 레코드의 수를 지정할 수 있고, offset 을 사용해 레코드 반환을 시작하기 전에 스킵할 레코드 수를 지정할 수 있습니다.
Client.limit(5)
위와 같이 작성하면, 최대 5개의 client를 반환하고 offset 을 지정하지 않기 때문에 테이블의 첫 5개를 반환합니다.
SQL 은 아래와 같습니다.
SELECT * FROM clients LIMIT 5
Board.limit(3) 여기에 offset 을 추가해보겠습니다.
Client.limit(5).offset(30)
이렇게 작성하면 31번째부터 최대 5개의 client를 반환합니다.
SQL 은 다음과 같습니다.
SELECT * FROM clients LIMIT 5 OFFSET 30
Board.limit(3).offset(3)
Group
finder 로 실행된 SQL에 GROUP_BY 절을 적용하려면, group 메소드를 사용합니다.
예를 들어, 만약 주문이 작성된 날짜의 컬렉션을 찾으려면 다음과 같이 작성합니다.
Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)") SELECT date(created_at) as ordered_date, sum(price) as total_price FROM orders GROUP BY date(created_at)
이는 데이터베이스 안에 주문이 있는 각 날짜에 대해 단일 order 객체를 줄 것입니다.
Total of grouped items
단일 쿼리에서 그룹화된 항목의 합계를 가져오려면 group 뒤에 count 메소드를 사용합니다.
Order.group(:status).count # => { 'awaiting_approval' => 7, 'paid' => 12 } SELECT COUNT (*) AS count_all, status AS status FROM "orders" GROUP BY status
Board.group(:content).count 위 이미지는 "content" => 숫자 로 표현됩니다.
Having
SQL 은 HAVING 절을 사용해 GROUP BY 필드에 조건들을 지정할 수 있습니다.
find 에 having 메소드를 추가함으로써 Model.find에 의해 실행된 SQL에 HAVING 절을 추가할 수 있습니다.
Order.select("date(created_at) as ordered_date, sum(price) as total_price"). group("date(created_at)").having("sum(price) > ?", 100) SELECT date(created_at) as ordered_date, sum(price) as total_price FROM orders GROUP BY date(created_at) HAVING sum(price) > 100
이는 각 주문 객체에 대해 날짜와 총 가격을 반환하는데, 이때 주문 날짜와 $100 이상 가격인 위치로 그룹화해 반환합니다.
Overriding Conditions
unscope
unscope 메소드를 사용해 제거할 특정 조건을 지정할 수 있습니다.
Article.where('id > 10').limit(20).order('id asc').unscope(:order) SELECT * FROM articles WHERE id > 10 LIMIT 20 # Original query without `unscope` SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20
특정 where 조건에 대해서도 unscope를 사용할 수 있습니다.
Article.where(id: 10, trashed: false).unscope(where: :id) # SELECT "articles".* FROM "articles" WHERE trashed = 0
unscope를 사용한 관계(relation)은 합쳐지는 관계에도 영향을 미칩니다.
Article.order('id asc').merge(Article.unscope(:order)) # SELECT "articles".* FROM "articles"
only
only 메소드를 사용해 조건들을 재정의할 수 있습니다.
Article.where('id > 10').limit(20).order('id desc').only(:order, :where) SELECT * FROM articles WHERE id > 10 LIMIT 20 # Original query without `unscope` SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20
reselect
reselect 메소드는 기존에 작성된 select 를 재정의합니다.
Post.select(:title, :body).reselect(:created_at) SELECT `posts`.`created_at` FROM `posts`
위 경우에서 reselect 를 사용하지 않았다면 다음과 같이 표현됩니다.
Post.select(:title, :body).select(:created_at) SELECT `posts`.`title`, `posts`.`body`, `posts`.`created_at` FROM `posts`
reorder
reorder 메소드는 디폴트 범위 order 를 재정의합니다.
class Article < ApplicationRecord has_many :comments, -> { order('posted_at DESC') } end Article.find(10).comments.reorder('name')
이렇게 작성하면 posted_at 이 아니라 name 이 정렬 기준이 됩니다.
SQL 표현은 다음과 같습니다.
SELECT * FROM articles WHERE id = 10 LIMIT 1 SELECT * FROM comments WHERE article_id = 10 ORDER BY name
위 경우 reorder 가 사용되지 않으면 SQL 표현은 다음과 같습니다.
SELECT * FROM articles WHERE id = 10 LIMIT 1 SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC
reverse_order
reverse_order 메소드는 지정된 경우 order 절을 반대로 바꿉니다.
Client.where("orders_count > 10").order(:name).reverse_order SELECT * FROM clients WHERE orders_count > 10 ORDER BY name DESC
동일한 SQL 표현을 보면 ORDER BY name DESC 로 name 을 오름차순이 아닌 내림차순으로 정렬합니다.
만약 쿼리에 ordering 컬럼이 지정되지 않으면 reverse_order는 primary key를 역순으로 정렬합니다.
Client.where("orders_count > 10").reverse_order SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC
rewhere
rewhere 는 기존에 지정된 where 조건을 재정의 합니다.
Article.where(trashed: true).rewhere(trashed: false) SELECT * FROM articles WHERE `trashed` = 0
만약 rewhere 을 사용하지 않으면 다음과 같이 표현됩니다.
Article.where(trashed: true).where(trashed: false) SELECT * FROM articles WHERE `trashed` = 1 AND `trashed` = 0
Null Relation
none 메소드는 레코드가 없는 연결된 관계를 반환합니다. 반환된 관계에 연결된 모든 후속 조건은 빈 관계를 계속 생성합니다. 이 방법은 0 결과를 반환할 수 있는 방법 또는 범위에 대한 연쇄 반응이 필요한 시나리오에서 유용합니다.
Article.none # returns an empty Relation and fires no queries.
# The visible_articles method below is expected to return a Relation. @articles = current_user.visible_articles.where(name: params[:name]) def visible_articles case role when 'Country Manager' Article.where(country: country) when 'Reviewer' Article.published when 'Bad User' Article.none # => returning [] or nil breaks the caller code in this case end end
Readonly Objects
액티브 레코드는 반환된 객체의 수정을 명시적으로 허용하지 않는 관계에 대해 readonly 메소드를 제공합니다. readonly record 를 변경하려고 하면 ActiveRecord::ReadOnlyRecord 에러가 발생합니다.
client = Client.readonly.first client.visits += 1 client.save
client 가 읽기 전용(readonly) 객체로 설정되어 있기 때문에 위 코드는 visits가 업데이트 된 값으로 저장할 때 ActiveRecord::ReadOnlyRecord 에러를 발생시킵니다.
board = Board.readonly.find_by(title: '수정') 으로 제목이 '수정'인 게시판을 읽기전용으로 board로 지정했습니다. 그리고나서 제목을 '수정해보자'로 업데이트 해보았습니다.
이때, ActiveRecord::ReadOnlyRecord (Board is marked as readonly) 라는 에러가 발생하는 모습을 볼 수 있습니다.
Locking Record for Update
Locking 은 데이터베이스의 레코드를 업데이트하고 atomic 업데이트를 보장할 때 Race condition 을 방지하는 데에 유용합니다.
race condition 이란, 두 개 이상의 프로세스가 공통 자원을 병행적으로 읽거나 쓸 때, 공용 데이터에 대한 접근이 어떤 순서에 따라 이루어졌는지에 따라 그 실행 결과가 같지 않고 달라지는 상황을 말합니다. 경쟁 상태라는 말 그대로 두 개의 스레드가 하나의 자원을 놓고 서로 사용하려고 경쟁하는 상황을 의미합니다.
이를 해결하기 위해 Locking 을 활용해 작업의 순서를 정해주는 것입니다.
액티브 레코드는 optimistic locking 과 pessimistic locking, 두 가지 locking 메커니즘을 제공합니다.
Optimistic Locking
optimistic locking 을 사용하면 여러 사용자가 동일한 레코드에 접근해 편집할 수 있으며 데이터와의 충돌을 최소화합니다. 이 작업은 다른 프로세스가 열린 후 레코드가 변경되었는지 확인하는 방식으로 작동합니다. 업데이트가 무시된 경우 AcitveRecord::StaleObjectError 가 발생합니다.
Optimistic Locking Column
optimistic locking 을 사용하기 위해, 테이블은 integer 타입의 lock_version 이라는 컬럼을 갖고 있어야 합니다. 레코드가 업데이트 될 때마다, 액티브 레코드는 lock_version 컬럼을 증가시킵니다.
만약 현재 데이터베이스의 lock_version 컬럼에 있는 것보다 lock_version 필드에 있는 값이 더 낮은 업데이트 요청이 실행되는 경우 업데이트는 실패하고 ActivRecord::StaleObjectError 가 발생합니다.
c1 = Client.find(1) c2 = Client.find(1) c1.first_name = "Michael" c1.save c2.name = "should fail" c2.save # Raises an ActiveRecord::StaleObjectError
이 예시에서 rescue 로 예외처리를 하거나 롤백, 병합, 비즈니스 로직의 적용을 통해 충돌을 대비할 필요가 있습니다.
이때, ActiveRecord::Base.lock_optimistically = false를 통해 Optimistic locking 사용을 비활성화 할 수 있습니다.
lock_version 컬럼의 이름을 재정의하기 위해서 ActiveRecord::Base 는 locking_column 이라는 클래스 속성을 제공합니다.
class Client < ApplicationRecord self.locking_column = :lock_client_column end
Pessimistic Locking
pessimistic locking 은 기본 데이터베이스에서 제공하는 잠금 메커니즘을 사용합니다. 관계를 구축할 때 lock 을 사용하면 선택한 행을 배타적으로 잠금할 수 있습니다. lock 을 사용하는 관계는 일반적으로 교착 상태를 방지하기 위해 트랜잭션 내에서 래핑됩니다.
Item.transaction do i = Item.lock.first i.name = 'Jones' i.save! end
위 예시는 MySQL 백앤드에서 다음 SQL 과정을 수행합니다.
SQL (0.2ms) BEGIN Item Load (0.3ms) SELECT * FROM `items` LIMIT 1 FOR UPDATE Item Update (0.4ms) UPDATE `items` SET `updated_at` = '2009-02-07 18:05:56', `name` = 'Jones' WHERE `id` = 1 SQL (0.8ms) COMMIT
다른 유형의 잠금을 허용하기 위해 원시 SQL 을 lock 메소드에 전달할 수도 있습니다. 예를 들어, MySQL 에는 레코드를 잠글 수 있지만 다른 쿼리가 읽도록 허용하는 LOCK IN SHARE MODE 라는 표현이 있습니다. 이 표현은 잠금 옵션으로 전달합니다.
Item.transaction do i = Item.lock("LOCK IN SHARE MODE").find(1) i.increment!(:views) end
모델의 인스턴스가 이미 있는 경우 다음 코드를 사용해 트랜잭션을 시작하고 한번에 lock 할 수 있습니다.
item = Item.first item.with_lock do # This block is called within a transaction, # item is already locked. item.increment!(:views) end
여기까지 레일즈 가이드의 Active Record Query Interface 중 11 Locking Records for Update 까지 살펴보았습니다. 다음 포스팅에서 계속해서 12 Joining Tables 부터 보겠습니다.
'Ruby on Rails' 카테고리의 다른 글
[레일즈] Active Record Query Interface 4 (0) 2021.04.29 [레일즈] Active Record Query Interface 3 (0) 2021.04.27 [레일즈] Active Record Query Interface 1 (0) 2021.04.25 [레일즈] Active Record 메소드 (0) 2021.04.23 [레일즈] 액티브 레코드(Active Record) 기본 (0) 2021.04.23