This is the third post about accounts in the app. I should make something simple, change every query in the app, and add “account_id” everywhere. Probably yes. But not this time. If you follow my posts maybe you realized all data in this app are user(account) oriented. There is no reason to allow one account to have access to another account’s data. So I decide to use multitenancy pattern.
Multitenancy is a software architecture pattern where a single instance of an application serves multiple tenants or customers. Each tenant’s data and configuration are isolated from each other, providing the illusion that each tenant has its own dedicated instance of the application.
In a multitenant application, tenants are typically identified by unique identifiers, such as account IDs or subdomains. The application uses this identifier to route requests and ensure that data and resources are only accessible to the corresponding tenant.
There are different types of multitenancy:
-
Single Database, Shared Schema: In this approach, all tenants share the same database schema, but their data is isolated using filtering or partitioning. Each table includes a column to identify the tenant, and queries include filters based on the tenant identifier.
-
Single Database, Separate Schema: Each tenant has its own separate schema within the same database. This approach offers stronger isolation between tenants and can be more performant than the shared schema approach.
-
Multiple Databases: In this approach, each tenant has its own dedicated database instance. This provides the highest level of isolation but may require more resources to manage.
If I would have Postgres probably I would go to multiple schemas, but since I have chosen sqlite it would be nice and easy to use multiple databases.
As I mentioned tenants are typically identified by unique identifiers. And in a previous post, I created middleware where I set this id in almost every request. Now I can create another middleware where I can set a database based on tenant id.
class MultitenancyMiddleware
def initialize(app)
@app = app
end
def call(env)
tenant_id = env["account_id"]
if tenant_id
unless File.exist?("#{ENV["DB_DIRECTORY"] + ENV["RACK_ENV"]}_#{tenant_id}.sqlite3")
create_tenant_database(tenant_id)
end
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: "#{ENV["DB_DIRECTORY"] + ENV["RACK_ENV"]}_#{tenant_id}.sqlite3")
else
ActiveRecord::Base.configurations = YAML.load_file("db/configuration.yml")
ActiveRecord::Base.establish_connection(ENV["RACK_ENV"].to_sym)
end
@app.call(env)
end
private
def create_tenant_database(tenant_id)
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: "#{ENV["DB_DIRECTORY"] + ENV["RACK_ENV"]}_#{tenant_id}.sqlite3")
ActiveRecord::MigrationContext.new("db/migrations").migrate
end
end
As you can see, I’m still using the main database. I have to store accounts somewhere. This is only one global thing in this application. So if someone wants to create an account, or login into these requests it hit the main database, otherwise it hits to separate DB.
Because I set the database connection in middleware I don’t need to setup it up inside the application so I can remove it from the repositories
def setup_database
ActiveRecord::Base.configurations = YAML.load_file("db/configuration.yml")
ActiveRecord::Base.establish_connection(ENV["RACK_ENV"].to_sym)
end
One important thing. Now we don’t have one database, we have multiple databases and if we run rake db:migrate
it runs migrations only for main database. If I want to migrate everydatabase I need custom script for that.
require "active_record"
require "dotenv"
require_relative "./records/account"
Dotenv.load(".env.development") if ENV["RACK_ENV"] == "development"
Dotenv.load(".env.test") if ENV["RACK_ENV"] == "test"
ActiveRecord::Base.configurations = YAML.load_file("db/configuration.yml")
ActiveRecord::Base.establish_connection(ENV["RACK_ENV"].to_sym)
Db::Records::Account.find_each do |tenant|
tenant_id = tenant.id
db_config = {
adapter: "sqlite3",
database: "#{ENV["DB_DIRECTORY"] + ENV["RACK_ENV"]}_#{tenant_id}.sqlite3"
}
ActiveRecord::Base.establish_connection(db_config)
ActiveRecord::Migration.verbose = false
begin
ActiveRecord::MigrationContext.new("db/migrations", ActiveRecord::SchemaMigration).migrate
rescue StandardError => e
puts "Migration failed for tenant #{tenant_id}: #{e.message}"
end
ActiveRecord::Base.remove_connection
end
Is it worth?
That’s a good question. Probably it would be nice to have some benchmarks. But I don’t have any. Also with my potential amount of data performance would never be a benefit (issue). But from a code perspective, it’s much nicer (in my opinion). We don’t care about users(accounts) we just have the logic of our application and the database is set at the beginning, that’s all. Of course, multitenancy doesn’t fit every application, but in this case, it fits perfectly
Like always, the whole code is on github