Seongsiks

Being A DevOpser. Powered by
Obtvse, highlight.js, theme toc Creative Commons License
Seongsiks Twitter Github Email
DevOps Ruby On Rails Chef Projects Misc Movies & Drama ME

Rails4-3 ActiveModel

Overview

이번 포스트에서도 각종 메소드들이 Rails4에서 어떻게 달라졌는지 확인해 보도록 하겠습니다.

Scopes

Rails3에서는 모델 코드 안 Scope를 정해주면 해당 모델이 특정 범위안에서만 검색하는 기능을 사용할 수 있게 됩니다.

# Rails3 deprecated
scope :sold,  where(state: 'sold')
default_scope where(state: 'available')
-----------------------------------------
# Rails4
scope :sold, ->{  where(state: 'sold') }
default_scope ->{  where(state: 'sold') }
default_scope {  where(state: 'sold') }

->()은 파라미터로 블럭을 받아서 [proc object]를 생성해 넘겨주는 함수입니다. prc object는 closure를 이용할 수 있는 함수라고 생각하시면 됩니다. 이것에 대해서는 아래에서 더 자세히 설명하겠습니다.

위 코드에서 Rails4에서 proc으로 변경한 이유는 다음 코드를 보면서 설명 드리죠.

scope :recent, where(published_at: 2.weeks.ago) 
scope :recent_red, recent.where(color: 'red') 

위와 같은 코드에서 2.weeks.ago는 해당 모델이 로드 될때 실행되어 버립니다. 그럼 원래 의도는 항상 해당 모델에서 scope를 적용 시켜서 데이터를 찾는 시점에서 2주전 데이터를 가져오는것이었는데, 해당 모델이 생성됐을 때(결국 Rails가 구동될때)로부터 2주전을 계산해버리고, 이 날짜는 고정되어 버립니다. 그래서 Rails4에서는 proc으로 주지 않으면 Warning을 주는 것입니다. Rails4에서는 다음과 같이 사용하시면 됩니다.

# Rails4
scope :recent, ->{where(published_at: 2.weeks.ago) }
scope :recent, ->where(published_at: 2.weeks.ago) }

Proc & Closure

그럼 closure가 무엇이냐.. 좀 어렵지만.. 간단하게 보면

함수가 생성될때 자신의 주변을 기억한다.

말이 좀 이상하죠? proc를 설명하는 ruby 문서에 좋은 예제가 있습니다.

def gen_times(factor)
  return Proc.new {|n| n*factor }
end

times3 = gen_times(3)
times5 = gen_times(5)

times3.call(12)  #=> 36
times5.call(5)   #=> 25

#gen_times 함수내에 Proc을 생성하고 있죠? 이렇게 생성되고 있을 당시에 factor의 값을 Proc Object가 기억하게 됩니다. 그래서 #time3 Proc Object는 factor를 3이라고 기억하고, #time5는 5라고 기억하게 되는겁니다. 이거시!! Closure입니다. ^^

실제로 Closure는 굉장히 어려운 개념인데 제가 대충 설명하고 넘어가고 있는 겁니다. 잘 모르신다면 꼭 시간내서 공부하시길 빌어요.

Model.none

class User < ActiveRecord::Base
  def visible_posts
    case role
    when 'Country Manager'
      Post.where(country: country)
    when 'Reviewer'
      Post.published
    when 'Bad User'
      [] #비어있는 컬렉션을 의미하기 위해
    end
  end
end
------------------------------------
# at Controller
def index
  @posts = current_user.visible_posts
  #role이 'Bad User'일때
  @posts.recent # => NoMethodError!!!
end

#recent메소드를 호출하는 순간 NoMethodError가 발생합니다. 왜냐면 @posts는 [], Array이니깐요. Array는 recent라는 메소드를 당연히 안가지고 있죠. 이런 문제를 해결하기 위해서 만들어진 것이 Relation#none입니다. 뭐 기존에도 해결방법은 있었습니다.

# at Controller
def index
  @posts = current_user.visible_posts
  if @posts.any? #비어있는 Array인지 확인
    @posts.recent
  else
    []  #caller에게 비어 있는 컬렉션 대용으로 리턴하기
  end
end

뭔가 쫌.. 부자연스럽고 지저분하네요. Rails4에서는 Relation#none메소드를 이용해서 깔삼하게 이 문제를 해결할 수 있습니다.

class User < ActiveRecord::Base
  def visible_posts
    case role
    when 'Country Manager'
      Post.where(country: country)
    when 'Reviewer'
      Post.published
    when 'Bad User'
      Post.none
    end
  end
end

끝~!

Relation#none메소드는 ActiveRecord::Relation을 리턴해줍니다. 당연히 데이터베이스에 실제로 쿼리를 하지 않습니다. 이후 이 Relation에게

Post.where(country: country).recent
Post.published.recent
Post.none.recent

위 메소드를 아무리 호출해도 데이터 베이스를 실제로 접속하지 않고 비어있는 컬렉션을 얻어낼 수 있습니다. 빈 컬렉션 하나 가져오는 것도 쉬운일은 아니네요..

Relation#not

#Rails3
@posts = Post.where('author != ?', author)
# 웬만한 경우에 잘 됩니다 이 코드는 그런데... author가 nil이면요???

자, 위 상황에선 다음과 같은 쿼리가 만들어집니다.

 SELECT "post".* from "posts" WHERE( author != NULL);

뭐죠 이게?? 그래서 기존에는 이렇게 해결했습니다.

if author
  @posts = Post.where('author != ?', author)
else
  @posts = Post.where('author IS NOT NULL')
end

음... 아름답지 않아요.. 이제 Rails4에서는 이렇게 하면됩니다.

#Rails4
@posts = Post.where.not(author: author)

이 메소드는 자동으로 author가 nil인지 체크하고 쿼리에서 '!= ?''IS NOT NULL'로 변경해줍니다.

Relation#order

Scope Order VS Order

class User < ActiveRecord::Base
  default_scope { order(:name) }
end

@users = User.order("created_at DESC")

위 코드로 생성되는 쿼리에서 order는 어떤 순서라고 생각하시나요??

SELECT * FROM users ORDER BY name ASC, created_at DESC;

이렇게 scope에 걸린 order가 먼저 들어가고 나중에 order한것은 나중에 더해집니다. 그런데 Rails4에서는 이 순서가 뒤집혔습니다.

Multi Field Ordering

#rails3 
@users = User.order(:name, 'created_at DESC')
# => ORDER BY name ASC, created_at DESC
#rails4
@users = User.order(:name, created_at: :desc)
# => ORDER BY name ASC, created_at DESC

Relation#reference

@posts = Post.include(:comments).where("comment.name = 'seongsik'")

이 코드를 풀어보자면,

포스트를 가져오는데 코멘트를 같이 가져오고 이 코멘트의 이름 중 seongsik인것을 또 골라죠.

그런데 이 마지막에 "그렇게 가져온 코멘트" 중에 라는 조건이 문제입니다. 이코드에 어디에도 그렇게 가져온 이란 의미의 코드는 없습니다. 이 이런 사용법은 이제 Rails4에서는 Deprecation Warning을 날립니다.

# Rails4
@posts = Post.include(:comments)
             .where("comment.name = 'seongsik'")
             .references(:comments)

위와 같이 명시적으로 참조할 테이블의 이름을 #references로 줘야합니다.

그런데 이렇게 항상 #references메소드로 참조할 테이블을 알려줘야 하는것은 아닙니다. 다음과 같은 경우 사용하지 않아도 됩니다.

# 조건이 해시일 경우
@posts = Post.include(:comments)
             .where(:comments: {name: 'seongsik'})
@posts = Post.include(:comments)
             .where('comments.name' => 'seongsik')
# 조건이 없는 경우
@posts = Post.include(:comments).order('comments.name')

ActiveModel

실제로 데이터베이스가 없는 보통 ruby클래스도 강력한 helper기능을 가져다 쓸 수 있도록 Rails3부터 소개된 ActiveModel.

class SupportTicket
  include ActiveModel::Conversation
  include ActiveModel::Validations
  extend ActiveModel::Naming
  # 이렇게 상속을 받으면 아래 기능을 사용할 수 있습니다.  

  attr_accessor :title, :description

  validates_presence_of :title
  validates_presence_of :description
end

여기에 보태서 form helper

form_for(@support_ticket) do |f|
  ...
end

또, 마치 ActiveRecord처럼 쓸 수 있게 됩니다.

@support_ticket =  SupportTicket.new(support_params)
@support_ticket.valid?
@support_ticket.errors?
@support_ticket.to_param

자, 이렇게 좋은 ActiveModel을 Rails4 가 좀 더 많이 쓰시라고 우리 고객님들께 다음 shorten version의 상속 문을 준비했습니다.

class SupportTicket
  include ActiveModel::Model

  attr_accessor :title, :description

  validates_presence_of :title
  validates_presence_of :description
end

살짝 ActiveModel::Model의 코드를 보면,

# file path : actviemodel/lib/active_model/model.rb
def self.include(base)
  base.class_eval do 
    extend ActiveModel::Naming
    extend ActiveModel::Translation
    include ActiveModel::Validations
    include ActiveModel::Conversation
  end
end        

자 이렇게 Rails4 zombie outlaws의 두번째 강좌가 끝났네요. 휴우.. 근데 저희 잉여력을 눈치챈 분이 계신 듯 합니다.. 끝까지 다 정리할 수 있겠지?


[이전 Rails4-2 ActiveRecord]
[다음 Rails4 - 4 Strong Parameters, Authenticity Token, Filters, Sessions, Flash]
comments powered by Disqus
Back to Ruby On Rails