STI 모델에 대한 연결을 미리 로드하는 방법

14761 단어 railsoptimisationruby
STI(단일 테이블 상속)는 유용한 패턴입니다. 코드베이스를 구성하고 논리를 다른 클래스로 분할하는 데 도움이 됩니다. 많은 프로그래머가 이를 사용하고 패턴을 좋아하지만 한 가지 단점이 있습니다. 표준preload 방법으로 연관을 미리 로드할 수 없다는 것입니다. 이제 처리 방법을 보여 드리겠습니다.

사용자가 정보를 업로드하고 게시물을 작성할 수 있는 Instagram 스타일 애플리케이션이 있다고 상상해 보십시오. 다양한 유형의 게시물이 있습니다.
  • 하나의 이미지가 있는 게시물
  • 멀티 이미지 포스트
  • 및 비디오와 함께 게시

  • 코드베이스는 STI 패턴을 기반으로 합니다.

    # base class
    class Post < ApplicationRecord
    end
    
    # standard post with one image
    class ImagePost < Post
      has_one :image, dependent: :destroy, foreign_key: :post_id
    end
    
    # multi images post
    class MultiImagePost < Post
      has_many :images, dependent: :destroy, foreign_key: :post_id
    end
    
    
    # post with video
    class VideoPost < Post
      has_one :video, dependent: :destroy, foreign_key: :post_id
    end
    


    또한 Image 클래스는 Asset와 관련이 있습니다.

    class Image < ApplicationRecord
      belongs_to :post, foreign_key: :post_id
      has_one :asset
    end
    
    class Asset < ApplicationRecord
      belongs_to :image
    end
    


    게시물 유형마다 피드가 다르며 애플리케이션이 완벽하게 작동합니다.

    ImagePost.preload(image: :asset).each do |item|
      render 'image_post', item: item
    end
    # database queries:
    # ImagePost Load (0.8ms)  SELECT "posts".* FROM "posts" WHERE "posts"."type" = ?  [["type", "ImagePost"]]
    #  Image Load (1.4ms)  SELECT "images".* FROM "images" WHERE "images"."post_id" IN (?, ?)  [["post_id", 21], ["post_id", 22]]
    #  Asset Load (0.9ms)  SELECT "assets".* FROM "assets" WHERE "assets"."image_id" IN (?, ?)  [["image_id", 5], ["image_id", 6]]
    
    MultiImagePost.preload(images: :asset).each do |item|
      render 'multi_image_post', item: item
    end
    # database queries:
    # MultiImagePost Load (0.7ms)  SELECT "posts".* FROM "posts" WHERE "posts"."type" = ?  [["type", "MultiImagePost"]]
    # Image Load (1.1ms)  SELECT "images".* FROM "images" WHERE "images"."post_id" IN (?, ?)  [["post_id", 20], ["post_id", 25]]
    # Asset Load (1.0ms)  SELECT "assets".* FROM "assets" WHERE "assets"."image_id" IN (?, ?, ?, ?)  [["image_id", 3], ["image_id", 4], ["image_id", 7], ["image_id", 8]]
    
    VideoPost.preload(:video).each do |item|
      render 'video_post', item: item
    end
    # database queries:
    # VideoPost Load (0.9ms)  SELECT "posts".* FROM "posts" WHERE "posts"."type" = ?  [["type", "VideoPost"]]
    # Video Load (0.9ms)  SELECT "videos".* FROM "videos" WHERE "videos"."post_id" IN (?, ?)  [["post_id", 23], ["post_id", 24]]
    


    어느 날 한 고객이 모든 유형의 게시물을 하나의 피드에 렌더링하도록 요청했습니다. 그러나 한 가지 문제가 있습니다. 표준을 사용할 수 없습니다.preload.

    ActiveRecord::AssociationNotFoundError (Association named 'image' was not found on MultiImagePost; perhaps you misspelled it?)
    


    당신은 무엇을해야합니까? ActiveRecord::Associations::Preloader를 사용하여 새 피드에 대한 사용자 정의 사전 로드 로직을 작성할 수 있습니다. 클래스의 새 인스턴스를 생성한 다음 preload 메소드를 사용해야 합니다. 두 개의 매개변수가 필요합니다.
  • AR 아이템 컬렉션
  • 미리 로드하려는 관계가 있는 해시입니다.
    코드는 rails-magic처럼 보이지 않으며 모든 관계를 명시적으로 설명해야 하지만 완벽하게 작동합니다.

  • posts = Post.all
    preloader = ActiveRecord::Associations::Preloader.new
    preloader.preload(posts.select{ |i| i.type == 'ImagePost' }, image: :asset)
    preloader.preload(posts.select{ |i| i.type == 'MultiImagePost' }, images: :asset)
    preloader.preload(posts.select{ |i| i.type == 'VideoPost' }, :video)
    
    posts.each |item|
      render 'post', item: item
    end
    


    로그를 살펴보면Preloader 각 연결에 대해 하나의 쿼리를 보내고 N+1 쿼리가 없습니다.

    # 1) take posts from DB
    Post Load (0.4ms)  SELECT "posts".* FROM "posts"
    # 2) load images for all ImagePosts
    Image Load (1.5ms)  SELECT "images".* FROM "images" WHERE "images"."post_id" IN (?, ?)  [["post_id", 21], ["post_id", 22]]
    # 3) load assets for them
    Asset Load (1.0ms)  SELECT "assets".* FROM "assets" WHERE "assets"."image_id" IN (?, ?)  [["image_id", 5], ["image_id", 6]]
    # 4) load images for all MultiImagePost
    Image Load (0.4ms)  SELECT "images".* FROM "images" WHERE "images"."post_id" IN (?, ?)  [["post_id", 20], ["post_id", 25]]
    # 4) load assets for them
    Asset Load (0.2ms)  SELECT "assets".* FROM "assets" WHERE "assets"."image_id" IN (?, ?, ?, ?)  [["image_id", 3], ["image_id", 4], ["image_id", 7], ["image_id", 8]]
    # 5) load videos for all VideoPosts
    Video Load (0.4ms)  SELECT "videos".* FROM "videos" WHERE "videos"."post_id" IN (?, ?)  [["post_id", 23], ["post_id", 24]]
    

    좋은 웹페이지 즐겨찾기