influitive / apartment

Database multi-tenancy for Rack (and Rails) applications
2.66k stars 464 forks source link

Switch with block does not work #610

Closed jurgens closed 5 years ago

jurgens commented 5 years ago

Steps to reproduce

This is weird since I believe this is quite major feature and it should be working well

When using switch with block it does not switch to desired tenant but pulls data from public schema instead, here's code from console

2.4.4 :004 > Apartment::Tenant.switch('tenant1') { User.all }
  User Load (0.3ms)  SELECT "users".* FROM "users"
 => #<ActiveRecord::Relation [#<User id: 1, name: "user public 1", created_at: "2019-06-03 12:17:16", updated_at: "2019-06-03 12:17:16", status: 0>]>

2.4.4 :008 > Apartment::Tenant.switch!('tenant1'); User.all
  User Load (0.5ms)  SELECT "users".* FROM "users"
 => #<ActiveRecord::Relation [#<User id: 1, name: "user tenant1 user1", created_at: "2019-06-03 12:18:33", updated_at: "2019-06-03 12:18:33", status: 0>]>

Note that in both cases it returns users with same ID=1 but names are different (named after tenant)

Expected behavior

I would expect both switching methods to work in a similar way as documented.

Actual behavior

switch method with block does not switch tenant

System configuration

require 'apartment/elevators/generic'
Apartment.configure do |config|
  config.excluded_models = %w{ Tenant Topic }
end

Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda { |request|
  request.session[:tenant].presence || 'public'
}
lcjury commented 5 years ago

Could you open a new rails console, copy and paste the following and paste the full cli session here?

User.first
Apartment::Tenant.switch('tenant1') { User.first }
Apartment::Tenant.switch!('tenant1')
User.first

If both queries went to the public schema, you shouldn't have different "names".

jurgens commented 5 years ago
2.4.4 :001 > User.first
  User Load (0.4ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
 => #<User id: 1, name: "user public 1", created_at: "2019-06-03 12:17:16", updated_at: "2019-06-03 12:17:16", status: 0>

2.4.4 :002 > Apartment::Tenant.switch('tenant1') { User.first }
  User Load (1.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
 => #<User id: 1, name: "user tenant1 user1", created_at: "2019-06-03 12:18:33", updated_at: "2019-06-03 12:18:33", status: 0>

2.4.4 :003 > Apartment::Tenant.switch!('tenant1')
 => nil

2.4.4 :004 > User.first
  User Load (0.5ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
 => #<User id: 1, name: "user tenant1 user1", created_at: "2019-06-03 12:18:33", updated_at: "2019-06-03 12:18:33", status: 0>

in addition here's another trick

2.4.4 :008 > Apartment::Tenant.switch('tenant1') { User.all }
  User Load (0.2ms)  SELECT "users".* FROM "users"
 => #<ActiveRecord::Relation [#<User id: 1, name: "user public 1", created_at: "2019-06-03 12:17:16", updated_at: "2019-06-03 12:17:16", status: 0>]>

2.4.4 :009 > Apartment::Tenant.switch('tenant1') { User.first }
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
 => #<User id: 1, name: "user tenant1 user1", created_at: "2019-06-03 12:18:33", updated_at: "2019-06-03 12:18:33", status: 0>`
lcjury commented 5 years ago

I'm not able to reproduce your problem nor I have an idea of why it doesn't work. switch and switch! works correctly when using User.first, but they don't with User.all

Is possible that you have something inside your User model that may be overriding the schema?

jurgens commented 5 years ago

the project I'm running it in is an empty rails playground application created specifically for this purpose, but first we ran into this problem in another production rails app. here I'm just confirming this problem

class User < ApplicationRecord
  has_many :topic_users, class_name: 'TopicUser'
  has_many :topics, through: :topic_users
end
jurgens commented 5 years ago

here's the repo https://github.com/jurgens/apartment-playground

lcjury commented 5 years ago

I see the problem now. From what I remember, this is expected behavior, and any part of the documentation says something different.

The problem is with rails not loading the resources until it's necesary

if you do the following: User.all; rails execute something like this: User.all.inspect (that's why rails shows the result on the console)

But, if you execute the following:

users = User.all;1  #rails is going to inspect the integer instead of the User.all result
users.loaded #so, this is going to return false. This resource might not be used, so it's not loaded until we need it.
users.load #now users is loaded.

When you do:

users = Apartment::Tenant.switch('tenant1') { User.all };1 #this return the relationship 
users.loaded? #but it's not loaded. 
users.load #as we are out of the switch, if we load users. It's going to query using the schema_path

So, now we that we are out of 'tenant1' if we try to load u, it's going to execute the query with the search_path set to public (as we're out of the switch)

u.load #should return `"<User name: 'user public 1'>"`

if you force the loading inside the block, it works has you expect:

 u = Apartment::Tenant.switch('tenant1') { User.all.load }
 u[0] #should return `"<User name: 'user tenant 1'>"`

Hope this explanation helps, and sorry if I'm overexplaining things :)

mikecmpbll commented 5 years ago

yep. you're not executing the query in the block :).

jurgens commented 5 years ago

thanks, guys!

jurgens commented 5 years ago

feeling stupid, I should have figured it out myself. thanks again.