DEV Community

Cover image for How to preload associations for STI-model
Vladislav Kopylov
Vladislav Kopylov

Posted on

How to preload associations for STI-model

Single table inheritance (STI) is useful pattern. It helps organise codebase and split logic into different classes. Many programmers use it and like the pattern, but it has one disadvantage: you can’t preload associations by standard preload method. Now, I show you the way to handle it.

Imagine, you have an Instagram style application where users are able to upload information and create posts. There are different types of post:

  • post with one image
  • multi images post
  • and post with video

You codebase is based by STI pattern

# 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 
Enter fullscreen mode Exit fullscreen mode

Also Image class has relation to Asset

class Image < ApplicationRecord belongs_to :post, foreign_key: :post_id has_one :asset end class Asset < ApplicationRecord belongs_to :image end 
Enter fullscreen mode Exit fullscreen mode

You have different feeds for each post type, and your application works perfectly.

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]] 
Enter fullscreen mode Exit fullscreen mode

The other day, a customer asked you to render all types of post in one feed. But there is one problem: you can’t use standard .preload

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

What should you do? You can write your custom preload logic for new feed using ActiveRecord::Associations::Preloader You should create new instance of the class and then use preload method. It takes two parameters:

  • Collection of AR-items
  • Hash with relations that you want to preload. The code doesn’t look like rails-magic and you have to describe all relations explicitly, but it works perfectly.
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 
Enter fullscreen mode Exit fullscreen mode

Look into logs, Preloader sends one query for each association, and you there aren’t N+1 queries

# 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]] 
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
sbrocos profile image
Sergio Brocos

and when you want to have a belongs_to only on son of STI ?

Collapse
 
mainameiz profile image
Nikolay Markov

Thanks you Vladislav. I'm faced the same problem, so your post was very useful for me :)