influitive / apartment

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

Switch block doesn't work #618

Open im-not-a-robot opened 4 years ago

im-not-a-robot commented 4 years ago

Steps to reproduce

Apartment::Tenant.switch("domain.com") do
     Content.all
end

Expected behavior

Show content from expected schema

Actual behavior

Show content from public schema

System configuration

#

Apartment Configuration

# Apartment.configure do |config|

Add any models that you do not want to be multi-tenanted, but remain in the global (public) namespace.

A typical example would be a Customer or Tenant model that stores each Tenant's information.

# config.excluded_models = %w{ JwtBlacklist Organization OrganizationsUser Profile Role User }

In order to migrate all of your Tenants you need to provide a list of Tenant names to Apartment.

You can make this dynamic by providing a Proc object to be called on migrations.

This object should yield either:

- an array of strings representing each Tenant name.

- a hash which keys are tenant names, and values custom db config (must contain all key/values required in database.yml)

#

config.tenant_names = lambda{ Customer.pluck(:tenant_name) }

config.tenant_names = ['tenant1', 'tenant2']

config.tenant_names = {

'tenant1' => {

adapter: 'postgresql',

host: 'some_server',

port: 5555,

database: 'postgres' # this is not the name of the tenant's db

but the name of the database to connect to before creating the tenant's db

mandatory in postgresql

},

'tenant2' => {

adapter: 'postgresql',

database: 'postgres' # this is not the name of the tenant's db

but the name of the database to connect to before creating the tenant's db

mandatory in postgresql

}

}

config.tenant_names = lambda do

Tenant.all.each_with_object({}) do |tenant, hash|

hash[tenant.name] = tenant.db_configuration

end

end

# config.tenant_names = lambda { Organization.pluck :domain }

PostgreSQL:

Specifies whether to use PostgreSQL schemas or create a new database per Tenant.

#

MySQL:

Specifies whether to switch databases by using use statement or re-establish connection.

#

The default behaviour is true.

#

config.use_schemas = true

#

==> PostgreSQL only options

Apartment can be forced to use raw SQL dumps instead of schema.rb for creating new schemas.

Use this when you are using some extra features in PostgreSQL that can't be represented in

schema.rb, like materialized views etc. (only applies with use_schemas set to true).

(Note: this option doesn't use db/structure.sql, it creates SQL dump by executing pg_dump)

#

config.use_sql = false

There are cases where you might want some schemas to always be in your search_path

e.g when using a PostgreSQL extension like hstore.

Any schemas added here will be available along with your selected Tenant.

#

config.persistent_schemas = %w{ hstore }

<== PostgreSQL only options

#

By default, and only when not using PostgreSQL schemas, Apartment will prepend the environment

to the tenant name to ensure there is no conflict between your environments.

This is mainly for the benefit of your development and test environments.

Uncomment the line below if you want to disable this behaviour in production.

#

config.prepend_environment = !Rails.env.production?

When using PostgreSQL schemas, the database dump will be namespaced, and

apartment will substitute the default namespace (usually public) with the

name of the new tenant when creating a new tenant. Some items must maintain

a reference to the default namespace (ie public) - for instance, a default

uuid generation. Uncomment the line below to create a list of namespaced

items in the schema dump that should not have their namespace replaced by

the new tenant

#

config.pg_excluded_names = ["uuid_generate_v4"]

end

Setup a custom Tenant switching middleware. The Proc should return the name of the Tenant that

you want to switch to.

Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda { |request|

request.host.split('.').first

}

Rails.application.config.middleware.use Apartment::Elevators::Domain

Rails.application.config.middleware.use Apartment::Elevators::Subdomain

Rails.application.config.middleware.use Apartment::Elevators::FirstSubdomain

Rails.application.config.middleware.use Apartment::Elevators::Host


  * `use_schemas`: `true`

* Rails (or ActiveRecord) version:
5.2.3

* Ruby version:
2.6.3

* Additional Info :
1. If im using switch!, it works perfectly.

Using Switch :

2.6.3 :036 > Apartment::Tenant.switch("domain.com") { Content.all } Content Load (0.9ms) SELECT "contents".* FROM "contents" LIMIT $1 [["LIMIT", 11]] => #<ActiveRecord::Relation []>


Using Switch! :

2.6.3 :001 > Apartment::Tenant.switch!("domain.com") => nil 2.6.3 :002 > Content.all Content Load (1.2ms) SELECT "contents".* FROM "contents" LIMIT $1 [["LIMIT", 11]] => #<ActiveRecord::Relation [#<Content id: 3, path: "test3", name: "test3", description: nil, published: false, active: true, user_id: nil, created_at: "2019-08-30 22:17:24", updated_at: "2019-08-30 22:17:24">, ...]>


2. If im using switch block with Content.all.load, it works perfectly

2.6.3 :001 > Apartment::Tenant.switch("domain.com") { Content.all.load } Content Load (0.9ms) SELECT "contents".* FROM "contents" => #<ActiveRecord::Relation [#<Content id: 3, path: "test3", name: "test3", description: nil, published: false, active: true, user_id: nil, created_at: "2019-08-30 22:17:24", updated_at: "2019-08-30 22:17:24">, ...]>



I think its similiar with issue #610 
lcjury commented 4 years ago

Did you read the issue #610 completely? this comment https://github.com/influitive/apartment/issues/610#issuecomment-515212827 explain why your code doesn't work as expected.

In summary, when you do Content.all the all method returns a Relation object (not a collection of Content), but rails inspect the return value of each line you execute in the console, thus, rails execute this Content.all.inspect instead of Content.all

So, what rails console is trully executing is the following:

contents = Apartment::Tenant.switch("domain.com") { Content.all }; 1 # whe can use the ;1 to avoid rails from inspecting the Content.all variable.
contents.inspect

It's inspecting the value of the relation outside of the switch.

im-not-a-robot commented 4 years ago

Yeah im already read that issue completely So the solution is i must using Content.all.load to load from expected schema ? Or may there's another solution for best practice ?

lcjury commented 4 years ago

Not really, the solution depends completely on what you want to achieve. But you shouldn't need to use Content.all.load. Apartment let you write your code the same way you been writing it.

If you want to use "best practises", you should learn how data loading works on rails and understand why Content.all don't bring the data until you try to access it;

contents = Content.all; 1
contents.loaded? #false: is not loaded
contents = contents.where('id < 100')
contents.loaded? #false: is not loaded
contents[0]
contents.loaded? #true, as you tried to access an item of the relation, rails execute the query and load its contents.

when you open the Rails console and you execute the following piece of code:

Content.all

Rails console is adds the inspect method to the value returned by the statement, thus the following code is executed:

Content.all.inspect

the inspect method access the relation, forcing it to be loaded. This is only part of the rails console behavior.

im-not-a-robot commented 4 years ago

Okay thanks for the information. Its very interesting.

But theres some question in my head. Im using graphql, so when im using switch and return in the block, the result still come from public schema.

In Example :

Apartment::Tenant.switch("domain.com") do
     contents = Content.all
     return contents
end

My question is why the data is still come from public schema even im return the data inside switch block ?

lcjury commented 4 years ago
contents = Apartment::Tenant.switch("domain.com") do
     contents = Content.all
     contents.loaded? # false
     return contents
end
contents.loaded? # false
contents[0] <-- in this line, you're in the public schema context, as the collection is not loaded, it's going to load the data from the public schema

In this example you might want to use load inside the switch. But, as I said, it depends.