ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [레일즈] Active Record Query Interface 3
    Ruby on Rails 2021. 4. 27. 17:29

    액티브레코드 쿼리 인터페이스에서 계속해서 12 Joining Table 부터 알아보겠습니다.


    Joining Tables

    액티브 레코드는 결과 SQL 에서 JOIN 절을 지정하는 두 가지 Finder 메소드인 join 과 left_outer_joins 를 제공합니다.

    여기서 join 은 INER JOIN 또는 사용자 지정 쿼리에 사용되어야 하지만, left_outer_join 은 LEFT OUTER JOIN 을 사용하는 쿼리에 사용됩니다.

     

    들어가기 전에, SQL의 join 개념에 대해 살펴보겠습니다.

     

    join 이란,

    둘 이상의 테이블을 연결해서 데이터를 검색하는 방법으로, 테이블을 연결하려면 테이블들이 적어도 하나의 컬럼을 공유하고 있어야 합니다. 이때, 이 공유하고 있는 칼럼을 PK 또는 FK 값으로 사용합니다.

     

    아래 이미지는 SQL JOIN의 종류를 보여주는데요

    지금은 개념적으로만 알아보고 SQL JOIN 의 종류와 사용법은 깔끔한 설명을 해주신 글을 첨부하겠습니다.

    yoo-hyeok.tistory.com/98

     

     

    위 글을 읽어보면, 서로 다른 테이블들이 동일한 컬럼을 갖고 있으면 그 컬럼을 통해 그 테이블들을 연결시켜준다고생각하면 이해가 쉽습니다.

     

    Joins

    다시 액티브 레코드 쿼리 인터페이스로 돌아와서, joins 메소드를 사용하는 방법에는 여러가지가 있는데요, 하나씩 보겠습니다.

     

    Using a String SQL Fragment

    joins 를 위해 JOIN 절을 특정하는 SQL 을 작성할 수 있습니다.

    Author.joins("INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'")

    여기서는 posts.author_id = authors.id 라는 칼럼으로 join 했습니다.

     

    이는 다음 SQL 문과 동일한 결과를 보여줍니다.

    SELECT authors.* FROM authors INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'

     

    Using Array/Hash of Named Associations

    액티브 레코드를 사용하면 join 메소드를 사용할 때 모델에 정의된 Associations 의 이름을 해당 연결에 대한 JOIN 절을 지정하는 바로가기로 사용할 수 있습니다.

     

    예를 들어, 아래 Category, Article, Comment, Guest 와 Tag 모델을 보겠습니다.

    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

    모델들의 관계를 보면,

    category 에 articles 가 속해있고, article 에는 comments 와 tags 가 속해있습니다. 그리고 comment 는 하나의 guest 를 가지며, guest 는 comment 에 속해있습니다. 마지막으로 tag는 article 에 속해있습니다.

     

    이제 INNER JOIN 을 사용해 다음 join 쿼리를 생성해봅니다.

     

    Joining a Single Association

    Category.joins(:articles)
    
    
    
    
    SELECT categories.* FROM categories
      INNER JOIN articles ON articles.category_id = categories.id

    이 두 코드는 모두, "article 이 있는 모든 Category 객체를 반환하라." 는 의미입니다.

    이때, 둘 이상의 article 에 동일한 category 가 있다면 category 들이 중복으로 나오게 되는데, 중복을 제거하고자 한다면 Category.joins(:articles).distinct 를 사용하면 됩니다.

     

    Board.joins(:comments)

     

    Joining Multiple Associations

    여러 Association를 조인하려면 다음과 같이 작성하면 됩니다.

    Article.joins(:category, :comments)

    이는 SQL에서 다음과 같습니다.

    SELECT articles.* FROM articles
      INNER JOIN categories ON categories.id = articles.category_id
      INNER JOIN comments ON comments.article_id = articles.id

    이 코드들을 해석하면, "category 와 최소한 하나 이상의 comment 가 있는 모든 articles 를 반환하라." 는 의미입니다.

    이때도 마찬가지로 comment 가 여러개 있는 article 은 여러번 반환됩니다.

     

    Board.joins(:user, :comments) 

    user 와 하나 이상의 comments 를 가진 모든 board 를 반환하라.

    Board.joins(:user, :comments)

     

    Joinig Nested Associations (Single Level)

    단일 수준의 nested 관계에서 조인입니다.

    Article.joins(comments: :guest)
    SELECT articles.* FROM articles
      INNER JOIN comments ON comments.article_id = articles.id
      INNER JOIN guests ON guests.comment_id = comments.id

    이 코드는 "guest 에 의해 작성된 comment 를 가진 모든 article 을 반환하라." 는 의미입니다.

     

    Board.joins(comments: :user)

     

    Joining Nested Associations (Multiple Level)

    다중 nested 관계에서 조인입니다.

    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

    이 코드는 "articles 중 guest 에 의해 작성된 comment 를 가진 article, tag 를 갖고 있는 article 을 가진 모든 category를 반환하라." 는 뜻입니다.

     

    그리고 각 모델들은 각각의 id 로 연결되어 있습니다.

     

    Specifying Conditions on the Joined Tables

    우리는 배열과 문자열 조건들을 사용해 조인된 테이블에 조건을 지정할 수 있습니다. 해시 조건은 조인된 테이블의 조건을 지정하기 위한 특별한 구문을 제공합니다.

    time_range = (Time.now.midnight - 1.day)..Time.now.midnight
    Client.joins(:orders).where('orders.created_at' => time_range)

    좀더 깔끔하게 작성하는 방법은 해시 조건을 중첩하는 것입니다.

    time_range = (Time.now.midnight - 1.day)..Time.now.midnight
    Client.joins(:orders).where(orders: { created_at: time_range })

    이렇게 작성하면 BETWEEN SQL 표현을 사용해 어제 생긴 order 를 가진 모든 clients 들을 찾아낼 수 있습니다.

     

    Board.joins(:comments).where('comments.created_at') 의 결과

     

     

    left_outer_joins

    연결된 레코드의 유무와 관계없이 레코드 집합을 선택하고자 하면 left_outer_joins 메소드를 사용합니다.

    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

    이 코드는 "author 가 갖고 있는 post 의 유무에 상관없이 posts 의 수로 author 를 반환하라." 는 의미입니다.


    Eager Loading Associations

    eager loading 은 가능한 적은 쿼리를 사용해 Model.find 에 의해 반환된 객체의 관련 레코드를 불러오는 메커니즘입니다.

     

    eager loading 이란 말 자체가 생소하기 때문에 또 찾아보겠습니다!

     

    우선 로딩 방식에 즉시로딩(eager loading)과 지연로딩(lazy loading) 이 있는데, 즉시 로딩이란 어떤 객체를 조회할 때 그 객체와 연관된 객체들을 한번에 가져오는 것이고, 지연로딩이란 객체를 조회할 때 그 객체만 가져오고 연관된 객체들은 프락시 초기화 방법으로 가져온다고 합니다.

     

    관련 글을 첨부합니다!

    sun-22.tistory.com/72

     

    다시 액티브 레코드로 돌아가서, 

     

    N+1 queries problem

    아래 코드를 보면 10개의 clients 를 찾아 그들의 postcodes 를 출력합니다.

    clients = Client.limit(10)
     
    clients.each do |client|
      puts client.address.postcode
    end

    이 코드는 처음 보기에 괜찮아 보입니다. 하지만, 그러나 실행된 쿼리의 총 수에 문제가 있습니다. 위 코드는 1 (10개의 clients 찾기) + 10(address 를 로드하기 위해 각 client 당 하나 씩) = 총 11개의 쿼리를 실행합니다.

     

    이러한 방법은 한번의 호출로 n 개의 모델을 가져온 뒤 n 개의 모델을 순회하면서 각각 모델에 관련된 모델에 접근할 때, 또다시 DB 에 호출하기 때문에 이때 n 번 호출하게 되어 어플리케이션 성능을 저하시킵니다.

     

    Solution to N + 1 queries problem

    액티브 레코드를 사용하면 조회될 모든 Associations을 사전에 지정할 수 있습니다. 이것은 Model.find 호출의 include 메소드를 활용하는 것입니다. include 메소드를 사용하면 액티브 레코드가 가능한 최소 쿼리 수를 사용해 지정된 모든 연결을 로드합니다.

     

    위 경우를 다시보면, Client.limit(10)가 address 를 eager load 하도록 다시 작성할 수 있습니다.

    clients = Client.includes(:address).limit(10)
     
    clients.each do |client|
      puts client.address.postcode
    end

    위 코드는 이전 코드에서 실행했던 11개의 쿼리가 아니라 2개의 쿼리만 실행합니다.

    SELECT * FROM clients LIMIT 10
    SELECT addresses.* FROM addresses
      WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))

     

    Eager Loading Multiple Associations

    액티브 레코드를 활용하면 include 메소드에 배열, 해시 또는 배열/해시의 중첩 해시를 사용해 단일 Model.find 호출에 대한 모든 Associations 를 로드할 수 있습니다.

     

    Array of Multiple Associations

    Article.includes(:category, :comments)

    이 코드는 모든 article 과 각 article 에 연관된 category 와 comments 를 불러옵니다.

     

    Nested Associations Hash

    Category.includes(articles: [{ comments: :guest }, :tags]).find(1)

    이 코드는 id 가 1인 category 를 찾고 관련된 모든 article, 관련 article 의 tag와 comments 그리고 모든 comments 의 guest 연결을 로드합니다.

     

    Specifying Conditions on Eager Loaded Associations

    액티브 레코드를 통해 joins 와 마찬가지로 즉시 로딩 되는 연결들에 대한 조건을 지정할 수 있지만, 그보다는 joins 을 사용하는 것을 추천합니다.

    그러나, 이 작업을 수행하는 경우에는 일반적으로 다음과 같이 사용할 수 있습니다.

    Article.includes(:comments).where(comments: { visible: true })

    이는 LEFT OUTER JOIN 을 포함하는 쿼리를 생성하는 반면, joins 메소드는 INNER JOIN 함수를 사용해 쿼리를 생성합니다.

    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)

    만약 where 조건이 없는 경우, 이 코드는 두 쿼리의 일반 집합을 생성합니다.

     

    이렇게 where 을 사용하는 것은 오직 해시를 전달할 경우에만 작동합니다. SQL-fragments 에서는 references를 사용해 joins 된 테이블을 강제로 실행해야 합니다.

    Article.includes(:comments).where("comments.visible = true").references(:comments)

    include 쿼리를 사용하는 경우, 만약 articles에 대한 comments 가 없으면 모든 articles 가 계속해서 로드됩니다. 조인(INNER JOIN)을 사용한다면 join 조건이 일치해야 하고, 그렇지 않으면 레코드가 반환되지 않습니다.

     

    Association 이 조인의 일부로 즉시로드 되는 경우, 로드된 모델에는 사용자 지정 선택 절의 필드가 표시되지 않습니다. 왜냐하면 이 필드들이 부모 레코드에 나와야 할지 자식 레코드에 나와야 할지 모호하기 때문입니다.


    오늘도 계속해서 레일즈 가이드의 액티브 레코드 쿼리 인터페이스를 살펴보았습니다. 모르는 부분이 계속 쏟아져 나오지만 하나씩 찾아가는 재미도 있네요!

     

     

Designed by Tistory.