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
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
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]]
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?)
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
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]]
Top comments (2)
and when you want to have a belongs_to only on son of STI ?
Thanks you Vladislav. I'm faced the same problem, so your post was very useful for me :)