티스토리 뷰

Ruby

[RoR] ActiveRecord 의 scope 사용시 주의할점

강씨아저씨 2020. 7. 3. 17:02

오늘의 포스팅 내용은 ActiveRecord 를 사용하면서 scope 를 이용했을 때 first, last 를 포함하면 안 되는 이유에 대해서 알아볼 예정이다. 우선 scopes 에 대한 정의는 여기에서 확인할 수 있다. 

 

간단하게 설명하자면 scope 는 ActiveRecord 에서 일반적으로 사용하는 Query 들을 지정할 수 있어서 필요시마다 함수 호출처럼 사용할 수 있게 도와준다. scopes 에서는 where, joins, includes 등이 사용 가능하고 실행결과로 ActiveRecord::Relation 혹은 nil 을 반환한다.

scope 의 활용 예시는 다음과 같다. 

module Accounts
  class User < ApplicationRecord
    scope :today_sign_up_users, -> { where('created_at >= ?',Time.now.beginning_of_day) }
    ...
  end
end

Accounts::User.today_sign_up_users
>> #<ActiveRecord::Relation [#<Accounts::User id: 1, ... >, #<Accounts::User id: 2, ... >,  #<Accounts::User id: 3, ... >, ...]>

물론 scope 를 쓰지 않고 Class Method 로 직접 호출하는 것도 가능하다.

Accounts::User.where('created_at >= ?',Time.now.beginning_of_day)
>> #<ActiveRecord::Relation [#<Accounts::User id: 1, ... >, #<Accounts::User id: 2, ... >,  #<Accounts::User id: 3, ... >, ...]>

문제가 되는 부분은 scope 를 써도 되고 안 쓰고 직접 호출해도 되다 보니, 간혹 scope 에 추가하면 안 되는 first, last 를 넣는 실수를 할 때가 있다. 거기다 first, last 를 넣었을 때 잘 동작하는 것처럼 보이기 때문에 쉽게 문제를 인지하지 못하고 있다가 엉뚱한 상황에서 장애가 발생하게 된다. 

 

문제가 발생할 수 있는 상황에 대한 예시는 다음과 같다.

"오늘 가입한 사용자 중에서 가장 마지막 사용자를 구하고 싶다. " 라는 생각에 다음과 같은 scope 를 정의하고 호출해보자.

module Accounts
  class User < ApplicationRecord
    scope :today_sign_up_users, -> { where('created_at >= ?',Time.now.beginning_of_day) }
    scope :today_latest_sign_up_user, -> { where('created_at >= ?',Time.now.beginning_of_day).last }
    ...
  end
end

Accounts::User.today_latest_sign_up_user
>> #<Accounts::User id: 50, ... >

뭔가 기대했던 그럴듯한 값을 반환했다. 실제로 해당 값은 오늘 가입한 사용자들 중에 가장 마지막 사용자의 정보가 맞다. 

 

문제는 다음과 같은 상황이 되었을 때 발생한다. 

만약 "오늘 가입한 사용자가 1명도 없는 상태인데 오늘 가입한 사용자들 중에 마지막 사용자를 구하려고 한다" 면 어떤 값을 반환해야 할까? 기대하는 값은 당연히 nil 혹은 비어있는 ActiveRecord::Relation 일 것이다. 그러나 실제로 동작해보면 다음과 같은 값을 반환한다. 

* 예시 scope 는 결과값이 없는 경우를 만들기 위해서 미래(내일)에 가입된 사용자들을 조회한다.

module Accounts
  class User < ApplicationRecord
    scope :today_sign_up_users, -> { where('created_at >= ?',Time.now.beginning_of_day) }
    scope :today_latest_sign_up_user, -> { where('created_at >= ?',Time.now.tomorrow).last }
    ...
  end
end

Accounts::User.today_latest_sign_up_user
>> #<ActiveRecord::Relation [#<Accounts::User id: 1, ... >, #<Accounts::User id: 2, ... >,  #<Accounts::User id: 3, ... >, ...]>

결과는 엉뚱하게도 전체 User 의 모든 데이터를 반환한다. 

 

 이렇게 엉뚱한 결과가 나오는 이유는 scope 는 기본적으로 ActiveRecord::Relation 이나 nil 을 반환하는 것으로 정의되어 있기 때문에 first / last 등의 단일 ActiveRecord 를 조회하는 옵션을 지원하지 않는다.(근데 문제는 막상 쓰면 지원하는 것처럼 보여서 더 문제다.) 이와 관련된 내용은 여기 에서도 확인할 수 있다. 

 

예시와 같은 이유로 scope 에서는 first, last 등을 사용하지 말고 만약 필요하다면 다음과 같이 scope 밖에서 별도로 호출하도록 하자.

module Accounts
  class User < ApplicationRecord
    scope :today_sign_up_users, -> { where('created_at >= ?',Time.now.beginning_of_day) }
    ...
  end
end

Accounts::User.today_sign_up_users.last
>> #<Accounts::User id: 50, ... >

오늘은 여기까지~

누군가에게 도움이 되었길 바라면서 오늘의 포스팅 끝~

댓글