require 'minitest/autorun' require 'prism' require 'tmpdir' class OrmEagerLoadingTest < Minitest::Test def setup @dir = Dir.mktmpdir('prism-eager-store') @store = CountingStore.new(File.join(@dir, 'test.pstore')) @store.create_table(:authors, %w[id name]) @store.create_table(:posts, %w[id title author_id]) @store.create_table(:bios, %w[id author_id headline]) author_class = Class.new(Prism::Orm::Model) do table_name :authors has_many :posts, class_name: 'Post', foreign_key: 'author_id' has_one :bio, class_name: 'Bio', foreign_key: 'author_id' end post_class = Class.new(Prism::Orm::Model) do table_name :posts belongs_to :author, class_name: 'Author', foreign_key: 'author_id' end bio_class = Class.new(Prism::Orm::Model) do table_name :bios belongs_to :author, class_name: 'Author', foreign_key: 'author_id' end [author_class, post_class, bio_class].each { |klass| klass.store = @store } Object.const_set(:Author, author_class) Object.const_set(:Post, post_class) Object.const_set(:Bio, bio_class) end def teardown FileUtils.remove_entry(@dir) %i[Author Post Bio].each { |name| Object.send(:remove_const, name) if Object.const_defined?(name) } end def test_includes_has_many_avoids_n_plus_one_queries 3.times do |i| author = Author.create!(name: "author#{i}") 2.times { |j| Post.create!(title: "post#{i}-#{j}", author_id: author.id) } end @store.reset_query_count! authors = Author.all.includes(:posts).to_a baseline_queries = @store.query_count authors.each { |author| author.posts.each(&:title) } assert_equal baseline_queries, @store.query_count, "expected preloaded #posts to be free, but it issued #{@store.query_count - baseline_queries} more queries" total_posts = authors.sum { |author| author.posts.size } assert_equal 6, total_posts end def test_includes_belongs_to_preloads_single_target author = Author.create!(name: 'solo') Post.create!(title: 'a', author_id: author.id) Post.create!(title: 'b', author_id: author.id) @store.reset_query_count! posts = Post.all.includes(:author).to_a baseline = @store.query_count names = posts.map { |post| post.author.name } assert_equal baseline, @store.query_count assert_equal %w[solo solo], names end def test_includes_has_one_preloads author = Author.create!(name: 'writer') Bio.create!(headline: 'hello there', author_id: author.id) loaded = Author.all.includes(:bio).to_a.first assert_equal 'hello there', loaded.bio.headline end def test_includes_unknown_association_raises Author.create!(name: 'x') assert_raises(ArgumentError) { Author.all.includes(:nope).to_a } end def test_includes_reduces_queries_compared_to_lazy_access 3.times do |i| author = Author.create!(name: "author#{i}") 2.times { |j| Post.create!(title: "post#{i}-#{j}", author_id: author.id) } end @store.reset_query_count! Author.all.to_a.each { |author| author.posts.to_a } lazy_queries = @store.query_count @store.reset_query_count! Author.all.includes(:posts).to_a.each { |author| author.posts.to_a } eager_queries = @store.query_count assert_equal 4, lazy_queries # 1 authors query + 1 posts query per author (N+1) assert_equal 2, eager_queries # 1 authors query + 1 batched posts query assert_operator eager_queries, :<, lazy_queries end end # Wraps Store#all/#find to count reads, so tests can assert on actual # query volume instead of trusting behavior "looks" batched. class CountingStore < Prism::Orm::Store def initialize(*) super @query_count = 0 end def query_count @query_count end def reset_query_count! @query_count = 0 end def all(table) @query_count += 1 super end def find(table, id) @query_count += 1 super end end