Backend/RubyOnRails

[RubyOnRails Guides] Active Record Query Interface - 2ํŽธ

Seyun(Marco) 2021. 3. 9. 21:40
728x90

[RubyOnRails Guides] Active Record Query Interface - 2ํŽธ

๐Ÿ’ผ ์„œ๋ก 

  • RubyOnRails Guides Active Record Basics ๋ฅผ ์ฐธ๊ณ ํ•ด ์ž‘์„ฑํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค.
  • Ruby version์€ 2.6.3์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • Ruby On Rails version์€ 5.2.1์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • 1ํŽธ์—์„œ๋Š” ๋‹จ๊ฑด ๋ฐ ๋ณต์ˆ˜ ์กฐํšŒ, ์กฐ๊ฑด, ์ •๋ ฌ, ๊ทธ๋ฃน ๋“ฑ์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค (์ฐธ๊ณ ๋งํฌ)

Locking Records for Update

  • DB ํŠธ๋žœ์žญ์…˜์ด 2๊ฐœ ์ด์ƒ ์ง„ํ–‰๋  ๊ฒฝ์šฐ ํ”„๋กœ์„ธ์Šค ๋‚ด์—์„œ ์ž‘์—…ํ•˜๋Š” ์“ฐ๋ ˆ๋“œ ์ž์›์ด ๊ผฌ์—ฌ race codnition(๊ฒฝ์Ÿ ์ƒํƒœ) ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ด ์šฐ๋ฆฌ๊ฐ€ ๊ธฐ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ค์ง€ ์•Š์„ ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋‹ค.
  • ๋”ฐ๋ผ์„œ ์šฐ๋ฆฌ๋Š” ์ด๋Ÿฐ ๊ฒฝ์šฐ Thread Safe ํ•˜๊ฒŒ ํ•ด์ค˜์•ผ ํ•˜๋Š”๋ฐ, DB์—์„œ๋Š” Lcok ๊ธฐ๋Šฅ์œผ๋กœ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค.
  • Locking์ด๋ž€ ๋‘๊ฐœ ์ด์ƒ์˜ ์ž‘์—…์ด ๋™์‹œ์— ์ง„ํ–‰๋  ๊ฒฝ์šฐ ์ด์ „ ์ž‘์—…์ด ๋๋‚ ๋•Œ ๊นŒ์ง€ ๋Œ€๊ธฐํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. Locking์„ ์„ค์ •ํ•˜๋ฉด ํŠธ๋žœ์žญ์…˜์ด unlock ํ•  ๋•Œ๊นŒ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋…์ ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1ed59229-52b8-4ff2-ba51-a63befcdf687/Untitled.png

  • Locking์—๋Š” ๋‘๊ฐ€์ง€ ์ข…๋ฅ˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‚™๊ด€์  ์ž ๊ธˆ, ๋น„๊ด€์  ์ž ๊ธˆ์ž…๋‹ˆ๋‹ค.

Optimistic locking(๋‚™๊ด€์  ์ž ๊ธˆ)

  • ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘์„ ์œ„ํ•ด ๋™์ผํ•œ ๋ ˆ์ฝ”๋“œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋ฐ์ดํ„ฐ์™€ ์ตœ์†Œ ์ถฉ๋Œ์„ ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค.
  • ํ…Œ์ด๋ธ”์— lock_version ์ปฌ๋Ÿผ์ด ์žˆ๊ณ  ๋ ˆ์ฝ”๋“œ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋  ๋•Œ๋งˆ๋‹ค lock_version์„ ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค.
  • ํ˜„์žฌ lock_version ์ปฌ๋Ÿผ ๊ฐ’๋ณด๋‹ค ๋” ๋‚ฎ์€ ๊ฐ’์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•  ๊ฒฝ์šฐ ActvieRecord::StaleObejctError์™€ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ ์š”์ฒญ์ด ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0399d3d6-d2a5-4f33-9b38-977084429037/Untitled.png

  1. ์šด์˜์ž๊ฐ€ ๋ฐฐ์†ก์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•ด version์ด 1์ธ ์ฃผ๋ฌธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  2. ๊ณ ๊ฐ์€ ๋ฐฐ์†ก์ง€๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•ด version์ด 1์ธ ์ฃผ๋ฌธ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  3. ์šด์˜์ž๋Š” ๋ฐฐ์†ก์ƒํƒœ๋ฅผ ๋ฐฐ์†ก์ค‘์œผ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
  4. ๊ณ ๊ฐ์€ ๋ฐฐ์†ก์ง€๋ฅผ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
  5. ์šด์˜์ž์˜ ํŠธ๋žœ์žญ์…˜์ด ์ปค๋ฐ‹๋˜๋ฉด์„œ version์ด 2๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.
  6. ์ดํ›„์— ๊ณ ๊ฐ์˜ ํŠธ๋žœ์žญ์…˜์„ ์ปค๋ฐ‹ํ•˜๋ฉด version์ด ๋‹ค๋ฅด๊ธฐ ๋–„๋ฌธ์— ์ปค๋ฐ‹์ด ์‹คํŒจํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ์œ„์˜ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ฒ˜๋Ÿผ ํŠธ๋žœ์žญ์…˜ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ๋ฃจ๋น„์—์„œ ๋‚™๊ด€์  ์ž ๊ธˆ์„ ํ•˜๊ณ , ์•„๋ž˜์™€ ๊ฐ™์ด ์‹คํ–‰ํ•˜๋ฉด ActvieRecord::StaleObejctError๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
c1 = Client.find(1)
c2 = Client.find(1)

c1.first_name = "Michael"
c1.save

c2.name = "should fail"
c2.save # Raises an ActiveRecord::StaleObjectError
  • lock_version์˜ ์ปฌ๋Ÿผ๋ช…์„ ๋ณ€๊ฒฝํ• ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
class Client < ApplicationRecord
    lock_optimistically = true  
    self.locking_column = :lock_client_column
end

Pessimistic Locking(๋น„๊ด€์  ์ž ๊ธˆ)

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/854f0f7b-86d7-4346-a862-c1f5f3650836/Untitled.png

  • DB์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๋ณธ locking ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์‚ฌ์šฉํ•œ๋‹ค.
  • ๊ด€๊ณ„๋ฅผ ์ž‘์„ฑํ•  ๋•Œ lock์„ ์‚ฌ์šฉํ•˜๋ฉด ์„ ํƒํ•œ ํ–‰(row)์—์„œ exclusive lock์„ ์–ป์Šต๋‹ˆ๋‹ค.
  • ๋น„๊ด€์  ์ž ๊ธˆ์€ Dead Lock(๊ต์ฐฉ ์ƒํƒœ)์— ๋น ์งˆ์ˆ˜ ์žˆ๋‹ค.
    1. ์Šค๋ ˆ๋“œ 1: A ์ •๋ณด๋ฅผ ๊ตฌํ•˜๊ณ  ์ž ๊ธˆ
    2. ์Šค๋ ˆ๋“œ 2 : B ์ •๋ณด๋ฅผ ๊ตฌํ•˜๊ณ  ์ž ๊ธˆ
    3. ์Šค๋ ˆ๋“œ 1: B ์ •๋ณด๋ฅผ ๊ตฌํ•˜๊ณ ์ž ํ•  ๋–„ ๋ธ”๋กœํ‚น
    4. ์Šค๋ ˆ๋“œ 2: A ์ •๋ณด๋ฅผ ๊ตฌํ•˜๊ณ ์ž ํ•  ๋–„ ๋ธ”๋กœํ‚น
  • ๊ต์ฐฉ ์ƒํƒœ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด lock์„ ์‚ฌ์šฉํ•˜๋Š” ๊ด€๊ณ„๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ํŠธ๋žœ์žญ์…˜ ๋‚ด๋ถ€์— ๋žฉํ•‘(warpped) ๋ฉ๋‹ˆ๋‹ค.
Item.transaction do
  i = Item.lock.first
  i.name = 'Jones'
  i.save!
end
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
  • lock์˜ ๋‹ค๋ฅธ ์œ ํ˜•์„ ํ—ˆ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์›์‹œ SQL์„ ๋ฉ”์†Œ๋“œ์— ์ „๋‹ฌํ•  ์ˆ˜ ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
Item.transaction do
  i = Item.lock("LOCK IN SHARE MODE").find(1)
  i.increment!(:views)
end
  • ๋ชจ๋ธ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ด๋ฏธ ์žˆ๋Š” ๊ฒฝ์šฐ ์•„๋ž˜์™€ ๊ฐ™์€ ์ฝ”๋“œ๋กœ ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘ํ•˜๊ณ  ์ž ๊ธˆ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
item = Item.first
item.with_lock do
  # This block is called within a transaction,
  # item is already locked.
  item.increment!(:views)
end

Joining Tables

joins

  • ์กฐ์ธ์˜ ์ข…๋ฅ˜๋Š” ์—ฌ๋Ÿฌ๊ฐ€์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
  1. ํŠน์ • JOIN ์ ˆ์„ ์ง€์ •ํ•ด SQL์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

     Author.joins("INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'")
    
     SELECT authors.* FROM authors INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'
  2. Assciation ์ด๋ฆ„์„ ์กฐ์ธ ๋ฐฉ๋ฒ•์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

     class Category < ApplicationRecord
       has_many :articles
     end
    
     class Article < ApplicationRecord
       belongs_to :category
       has_many :comments
       has_many :tags
     end
    
     class Comment < ApplicationRecord
       belongs_to :article
       has_one :guest
     end
    
     class Guest < ApplicationRecord
       belongs_to :comment
     end
    
     class Tag < ApplicationRecord
       belongs_to :article
     end
    • ์•Œ์•„์„œ INNER JOIN์ด ๋ฉ๋‹ˆ๋‹ค.
  3. ๋‹จ์ผ ์—ฐ๊ฒฐ ์กฐ์ธ ํ•˜๋Š” ๋ฐฉ๋ฒ•

     Category.joins(:articles)
    
     SELECT categories.* FROM categories
       INNER JOIN articles ON articles.category_id = categories.id
    • 1:N์˜ ๊ด€๊ณ„์ด๋ฉด ์ค‘๋ณต cateogoies๊ฐ€ ์žˆ๋Š”๋ฐ disinct ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

      Category.joins(:articles).distinct
  4. ๋‹ค์ค‘ ์—ฐ๊ฒฐ์กฐ์ธํ•˜๋Š” ๋ฐฉ๋ฒ•

     Article.joins(:category, :comments)
    
     SELECT articles.* FROM articles
       INNER JOIN categories ON categories.id = articles.category_id
       INNER JOIN comments ON comments.article_id = articles.id
     Category.joins(articles: [{ comments: :guest }, :tags])
    
     SELECT categories.* FROM categories
       INNER JOIN articles ON articles.category_id = categories.id
       INNER JOIN comments ON comments.article_id = articles.id
       INNER JOIN guests ON guests.comment_id = comments.id
       INNER JOIN tags ON tags.article_id = articles.id
  • Join Table์— ์กฐ๊ฑด์„ ์ง€์ •ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where('orders.created_at' => time_range)
  • ์œ„์™€ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด BETWEEN๋ฌธ์˜ ์กฐ๊ฑด์ด ํฌํ•จ๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

left_outer_joins

  • OuterJoin์„ ํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
Author.left_outer_joins(:posts).distinct.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id')

SELECT DISTINCT authors.*, COUNT(posts.*) AS posts_count FROM "authors"
LEFT OUTER JOIN posts ON posts.author_id = authors.id GROUP BY authors.id

Eager Loding

  • ์ฆ‰์‹œ๋กœ๋”ฉ์œผ๋กœ Model.find() ์‹œ ์—ฐ๊ด€๊ด€๊ณ„๋กœ ๋˜์–ด ์žˆ๋Š” ์ฝ”๋“œ๋ฅผ ํ•œ๋ฐฉ์— ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ฒƒ ์ž…๋‹ˆ๋‹ค.

N + 1 ์ฟผ๋ฆฌ ๋ฌธ์ œ

clients = Client.limit(10)

clients.each do |client|
  puts client.address.postcode
end
  • 10๋ช…์˜ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์กฐํšŒํ•œ ์ดํ›„์— ์ฃผ์†Œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋ฉด 10๊ฐœ์˜ ํด๋ผ์ด์–ธํŠธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” SQLํ•˜๋‚˜์™€ ๊ฐ ์ฃผ์†Œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” SQL 10๊ฐœ๋กœ N + 1 ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

N + 1 ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  • ์ด๊ฑธ ํ•ด๊ฒฐ ํ•˜๊ธฐ ์œ„ํ•ด eager๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.
  • Lazy Loding์ด ์•„๋‹ˆ๋ผ Eager Loding์œผ๋กœ Clinet๋ฅผ ์กฐํšŒํ• ๋•Œ join์œผ๋กœ address๊นŒ์ง€ ๊ฐ™์ด ๋ถˆ๋Ÿฌ์˜ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
clients = Client.includes(:address).limit(10)

clients.each do |client|
  puts client.address.postcode
end

SELECT * FROM clients LIMIT 10
SELECT addresses.* FROM addresses
  WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
  • ์•„๋ž˜์™€ ๊ฐ™์ด ์กฐ๊ฑด์„ ์ง€์ •ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
Article.includes(:comments).where(comments: { visible: true })

SELECT "articles"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "articles" LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id" WHERE (comments.visible = 1)

range

  • ๋ฒ”์œ„๋ฅผ ์ง€์ •ํ•˜๋ฉด ์—ฐ๊ด€ ๊ด€๊ณ„ ๋ชจ๋ธ์— ๋Œ€ํ•œ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ๋กœ ์ฐธ์กฐ ๋  ์ˆ˜ ์žˆ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ์ฟผ๋ฆฌ๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • scope๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
end

class Article < ApplicationRecord
  def self.published
    where(published: true)
  end
end
  • 2๊ฐœ๋‹ค ๋ชจ๋‘ ๋™์ผํ•˜๋ฉฐ ์›ํ•˜์‹œ๋Š” ๋ฐฉ์‹์œผ๋กœ ์„ค์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.
  • range ๋‚ด์—์„œ๋„ ์—ฐ๊ฒฐ์ด ๊ฐ€๋Šฅํ•˜๋นˆ๋‹ค.
class Article < ApplicationRecord
  scope :published,               -> { where(published: true) }
  scope :published_and_commented, -> { published.where("comments_count > 0") }
end
  • published ๋ฒ”์œ„๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” published ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.
Article.published

๋งค๊ฐœ๋ณ€์ˆ˜ ์ „๋‹ฌ

  • ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
class Article < ApplicationRecord
  scope :created_before, ->(time) { where("created_at < ?", time) }
end

class Article < ApplicationRecord
  def self.created_before(time)
    where("created_at < ?", time)
  end
end
  • ์•„๋ž˜์™€ ๊ฐ™์ด ํ˜ธ์ถœ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
Article.created_before(Time.zone.now)

์กฐ๊ฑด๋ฌธ ์‚ฌ์šฉ

class Article < ApplicationRecord
  scope :created_before, ->(time) { where("created_at < ?", time) if time.present? }
end

class Article < ApplicationRecord
  def self.created_before(time)
    where("created_at < ?", time) if time.present?
  end
end
  • ํ•œ๊ฐ€์ง€ ์ฃผ์˜์‚ฌํ•ญ์€ ์กฐ๊ฑด์€ false๊ฐ€ ๋‚˜์˜ค๋ฉด nul์„ ๋ฐ˜ํ™˜ํ•˜์ง€๋งŒ, scope๋Š” ํ•ญ์ƒ ActiveRecord::Relation ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ์กฐ๊ฑด ์ค‘ ํ•˜๋‚˜๋ผ๋„ false๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ์กฐ๊ฑด๋ถ€์™€ class method๋ฅผ ์—ฐ๊ฒฐํ•  ๋•Œ NoMethodError๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

default scope(๊ธฐ๋ณธ ๋ฒ”์œ„)

  • ์•„๋ž˜์™€ ๊ฐ™์ด ๊ธฐ๋ณธ ๋ฒ”์œ„๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
class Client < ApplicationRecord
  default_scope { where("removed_at IS NULL") }
end

class Client < ApplicationRecord
  def self.default_scope
    # Should return an ActiveRecord::Relation.
  end
end
  • ์ฃผ์˜์‚ฌํ•ญ์€ Query Arguments๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
class Client < ApplicationRecord
  default_scope { where("active = ?", true) }
end

Client.new # => #<Client id: nil, active: nil>

scope ๋ณ‘ํ•ฉ

class User < ApplicationRecord
  scope :active, -> { where state: 'active' }
  scope :inactive, -> { where state: 'inactive' }
end

User.active.inactive

SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'inactive'
  • AND ์กฐ๊ฑด์„ ์‚ฌ์šฉํ•ด scope๋ฅผ ํ•ฉ์น˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
  • ์ฆ‰, ์ตœ์ข… SQL์ด AND์™€ JOIN ๋œ ๋ชจ๋“  ์กฐ๊ฑด์„ ๊ฐ–๋„๋ก scope๋ฅผ ํ˜ผํ•ฉํ•˜๊ณ  ์ผ์น˜์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
 User.active.merge(User.inactive)

SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
class User < ApplicationRecord
  default_scope { where state: 'pending' }
  scope :active, -> { where state: 'active' }
  scope :inactive, -> { where state: 'inactive' }
end

User.all
# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'

User.active
# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'active'

User.where(state: 'inactive')
# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive'
  • ์œ„์™€ ๊ฐ™์ด ์ค‘์š”ํ•œ ์ฃผ์˜ ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค. default_scope๊ฐ€ ์ œ์ผ ๋จผ์ € ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.(์ฆ‰, scope, where ์•ž์— ์ถ”๊ฐ€)

scope ์‚ญ์ œ

  • unscope ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
Client.unscoped.load

Client.unscoped.all
# SELECT "clients".* FROM "clients"

Client.where(published: false).unscoped.all
# SELECT "clients".* FROM "clients"
  • unscope๋Š” ์‚ฌ์ „์— ์ •์˜๋œ ๋ชจ๋“  scope๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ํ…Œ์ด๋ธ”์—์„œ๋Š” ์ผ๋ฐ˜ Query๋กœ ์ž‘์—…ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉด ์ข‹์Šต๋‹ˆ๋‹ค.
  • ๋ธ”๋ก์„ ํ—ˆ์šฉ๋„ ํ•ฉ๋‹ˆ๋‹ค.
Client.unscoped {
  Client.created_before(Time.zone.now)
}

Dynamic Finders

  • ํ…Œ์ด๋ธ”์— ์ •์˜ํ•œ ๋ชจ๋“  ํ•„๋“œ์— ๋Œ€ํ•ด finder๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ๋“ค์–ด find_by_name๋ผ๋Š” ๋ฉ”์„œ๋“œ๋กœ SELECT * FROM USER WHERE name = ์ฟผ๋ฆฌ๋ฅผ ๋งŒ๋“ค์ˆ˜ ์žˆ๋‹ค.
  • ์—ฌ๋Ÿฌ๊ฐœ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด and๋ฅผ ์ด์šฉํ•˜๋ฉด ๋œ๋‹ค.
    • Client.find_by_first_name_and_locked(...)

enum

  • ์ •์ˆ˜ ๊ฐ’์„ Set์œผ๋กœ ๋ชจ์•„ ๋†“๋Š” enum๋„ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
class Book < ApplicationRecord
  enum availability: [:available, :unavailable]
end

# Both examples below query just available books.
Book.available
# or
Book.where(availability: :available)

book = Book.new(availability: :available)
book.available?   # => true
book.unavailable! # => true
book.available?   # => false

Method chaining

  • ์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ”์—์„œ ํ•„ํ„ฐ๋ง ๋œ ๋ฐ์ดํ„ฐ ๊ฒ€์ƒ‰
Person
  .select('people.id, people.name, comments.text')
  .joins(:comments)
  .where('comments.created_at > ?', 1.week.ago)

SELECT people.id, people.name, comments.text
FROM people
INNER JOIN comments
  ON comments.person_id = people.id
WHERE comments.created_at = '2015-01-01'
  • ์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ”์—์„œ ํŠน์ • ๋ฐ์ดํ„ฐ ๊ฒ€์ƒ‰
Person
  .select('people.id, people.name, companies.name')
  .joins(:company)
  .find_by('people.name' => 'John') # this should be the last

SELECT people.id, people.name, companies.name
FROM people
INNER JOIN companies
  ON companies.person_id = people.id
WHERE people.name = 'John'
LIMIT 1

์ƒˆ๋กœ์šด ๋ชจ๋ธ ์กฐํšŒ ๋˜๋Š” ์ €์žฅ

find_or_create_by

  • ๋ ˆ์ฝ”๋“œ๊ฐ€ ์กด์žฌํ•˜๋ฉด ์กฐํšŒํ•˜๊ณ  ๋ ˆ์ฝ”๋“œ๊ฐ€ ์—†์œผ๋ฉด ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
Client.find_or_create_by(first_name: 'Andy')

SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
BEGIN
INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57')
COMMIT

find_or_create_by!

  • create๊ฐ€ ์•„๋‹Œ new ๋ฉ”์„œ๋“œ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ์ดˆ๊ธฐํ™” ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, ์ƒˆ๋กœ์šด Modal ์ธ์Šคํ„ด์Šค๊ฐ€ ์ƒ์„ฑ๋˜์ง€๋งŒ, DB์—๋Š” ์ €์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
Client.find_or_create_by!(first_name: 'Marco')
  • validate์ค‘์— presence: true๋กœ ์„ค์ •๋˜์–ด ์žˆ๋Š”๊ฒŒ ์žˆ๋‹ค๋ฉด, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

find_or_initialize_by

  • DB์— ์ €์žฅ๋˜์ง€ ์•Š๊ณ  ์ธ์Šคํ„ด์Šค๋งŒ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • ์ถ”ํ›„ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด save ๋ฉ”์†Œ๋“œ๋ฅผ ์‹คํ–‰์‹œ์ผœ์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๋ชจ๋ธ ์กฐํšŒ ์ฟผ๋ฆฌ

find_by_sql

  • SQL์„ ์ด์šฉํ•ด ํ…Œ์ด๋ธ”์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
Client.find_by_sql("SELECT * FROM clients
  INNER JOIN orders ON clients.id = orders.client_id
  ORDER BY clients.created_at desc")
# =>  [
#   #<Client id: 1, first_name: "Lucas" >,
#   #<Client id: 2, first_name: "Jan" >,
#   ...
# ]

select_all

  • ๋ชจ๋“  ๋ชจ๋ธ์„ ๊ฐ€์ ธ์˜ค๋Š” ์กฐํšŒ ์ฟผ๋ฆฌ์ž…๋‹ˆ๋‹ค.
Client.connection.select_all("SELECT first_name, created_at FROM clients 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"}
# ]

pluck

  • ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”์—์„œ ๋‹จ์ผ ๋˜๋Š” ์—ฌ๋Ÿฌ ์ปฌ๋Ÿผ์„ ๊ฒ€์ƒ‰ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
Client.where(active: true).pluck(:id)
# SELECT id FROM clients WHERE active = 1
# => [1, 2, 3]

Client.distinct.pluck(:role)
# SELECT DISTINCT role FROM clients
# => ['admin', 'member', 'guest']

Client.pluck(:id, :name)
# SELECT clients.id, clients.name FROM clients
# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
  • ๊ฒฐ๊ณผ๊ฐ€ ๋ฐฐ์—ด๋กœ ๋‚˜์˜ค๊ธฐ ๋•Œ๋ฌธ์— ์ฟผ๋ฆฌ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ฌ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ทธ๋Ÿฌ๋‚˜ pluck๋ฅผ ์‹คํ–‰์‹œํ‚ค๋Š” ์ˆœ๊ฐ„ Query๋ฅผ triggerํ•˜๊ธฐ ๋•Œ๋ฌธ์— chaining์„ ์‚ฌ์šฉํ• ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
Client.pluck(:name).limit(1)
# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>

Client.limit(1).pluck(:name)
# => ["David"]

ids

  • ๊ธฐ๋ณธํ‚ค๋ฅผ ์‚ฌ์šฉํ•ด ๊ด€๊ณ„์˜ ๋ชจ๋“  ID๋ฅผ ๊ฐ€์ ธ์˜ฌ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ชจ๋ธ ์กด์žฌ ์—ฌ๋ถ€ ์ฟผ๋ฆฌ

exist?

  • ๋‹จ์ˆœํžˆ ๊ฐ์ฒด์˜ ์กด์žฌ๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์œผ๋กœ Boolean๊ฐ’์ด ๋‚˜์˜จ๋‹ค.
  • ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ๊ฐ’๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
Client.exists?(id: [1,2,3])
# or
Client.exists?(name: ['John', 'Sergei'])
  • ๊ทธ๋Ÿฌ๋‚˜ ์œ„์˜ ๋ฐฉ์‹์€ ์ผ๋ถ€๋งŒ ์กด์žฌํ•ด๋„ true๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ๋ชจ๋‘ ์กด์žฌํ•˜์ง€ ์•Š์•„์•ผ false๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

์ปฌ๋Ÿผ ๊ฐ’ ๊ณ„์‚ฐ ์ฟผ๋ฆฌ

Count

  • ๋ชจ๋ธ ๊ฐฏ์ˆ˜๋ฅผ ์…€์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
Bulletin.count

# (0.1ms)  SELECT COUNT(*) FROM "bulletins"
#  => 63

Average & Sum

  • ํ‰๊ท  & ํ•ฉ๊ณ„์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
Client.average("orders_count")
Client.sum("orders_count")

Maximum & Minimum

  • ์ตœ๋Œ“๊ฐ’& ์ตœ์†Ÿ๊ฐ’์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
Client.maximum("age")
Client.minimum("age")

์ฟผ๋ฆฌ ํƒ์ƒ‰ ๊ฒฐ๊ณผ ํ™•์ธ

  • ์ฟผ๋ฆฌ ํƒ์ƒ‰ ๊ณผ์ •์„ ์‚ดํŽด๋ณผ ์ˆ˜ ์žˆ๋Š” explain ๋ฉ”์†Œ๋“œ๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
User.where(id: 1).joins(:articles).explain

## PostgreSQL

EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1
                                  QUERY PLAN
------------------------------------------------------------------------------
 Nested Loop Left Join  (cost=0.00..37.24 rows=8 width=0)
   Join Filter: (articles.user_id = users.id)
   ->  Index Scan using users_pkey on users  (cost=0.00..8.27 rows=1 width=4)
         Index Cond: (id = 1)
   ->  Seq Scan on articles  (cost=0.00..28.88 rows=8 width=4)
         Filter: (articles.user_id = 1)
(6 rows)

๐Ÿ˜‡ ๊ฒฐ๋ก 

  • Rails๋Š” Active Record๋กœ ORM ๊ธฐ๋Šฅ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
  • Query Interface๋กœ ๊ฐœ๋ฐœ์ž๊ฐ€ Query๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š๊ณ ๋„ DB SQL์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋Œ€๋ถ€๋ถ„์˜ Query ๋ช…๋ น๋ฌธ์„ ์ง€์›ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐœ๋ฐœ์ž๊ฐ€ Query์— ์ง‘์ค‘ํ•  ํ•„์š”๊ฐ€ ์—†์œผ๋ฉฐ vender์— ๋งž์ถฐ SQL์ด ์กฐ๊ธˆ์”ฉ ๋‹ค๋ฅธ๋ฐ ์† ์‰ฝ๊ฒŒ vender๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ๋„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
  • 2ํŽธ์—์„œ๋Š” Lock, Join, Eger Loading ๋“ฑ์„ ์•Œ์•„๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์ถœ์ฒ˜

Active Record Query Interface - Ruby on Rails Guides

๋น„๊ด€์  ์ž ๊ธˆ(์„ ์  ์ž ๊ธˆ, Pessimistic Lock)๊ณผ ๋‚™๊ด€์  ์ž ๊ธˆ(๋น„์„ ์  ์ž ๊ธˆ, Optimistic Lock)

728x90
728x90