Creating a multi-tenant application using PostgreSQL's schemas and Rails Creating a multi-tenant application using PostgreSQL's schemas and Rails ruby-on-rails ruby-on-rails

Creating a multi-tenant application using PostgreSQL's schemas and Rails


Update Dec 5, 2011

Thanks to Brad Robertson and his team, there's the Apartment gem. It's very useful and does a lot of the heavy lifting.

However, if you'll be tinkering with schemas, I strongly suggest knowing how it actually works. Familiarize yourself with Jerod Santo's walkthrough , so you'll know what the Apartment gem is more or less doing.

Update Aug 20, 2011 11:23 GMT+8

Someone created a blog post and walks though this whole process pretty well.

Update May 11, 2010 11:26 GMT+8

Since last night I've been able to get a method to work that creates a new schema and loads schema.rb into it. Not sure if what I'm doing is correct (seems to work fine, so far) but it's a step closer at least. If there's a better way please let me know.

  module SchemaUtils   def self.add_schema_to_path(schema)    conn = ActiveRecord::Base.connection    conn.execute "SET search_path TO #{schema}, #{conn.schema_search_path}"   end   def self.reset_search_path    conn = ActiveRecord::Base.connection    conn.execute "SET search_path TO #{conn.schema_search_path}"   end   def self.create_and_migrate_schema(schema_name)    conn = ActiveRecord::Base.connection    schemas = conn.select_values("select * from pg_namespace where nspname != 'information_schema' AND nspname NOT LIKE 'pg%'")    if schemas.include?(schema_name)     tables = conn.tables     Rails.logger.info "#{schema_name} exists already with these tables #{tables.inspect}"    else     Rails.logger.info "About to create #{schema_name}"     conn.execute "create schema #{schema_name}"    end    # Save the old search path so we can set it back at the end of this method    old_search_path = conn.schema_search_path    # Tried to set the search path like in the methods above (from Guy Naor)    # [METHOD 1]: conn.execute "SET search_path TO #{schema_name}"    # But the connection itself seems to remember the old search path.    # When Rails executes a schema it first asks if the table it will load in already exists and if :force => true.     # If both true, it will drop the table and then load it.     # The problem is that in the METHOD 1 way of setting things, ActiveRecord::Base.connection.schema_search_path still returns $user,public.    # That means that when Rails tries to load the schema, and asks if the tables exist, it searches for these tables in the public schema.    # See line 655 in Rails 2.3.5 activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb    # That's why I kept running into this error of the table existing when it didn't (in the newly created schema).    # If used this way [METHOD 2], it works. ActiveRecord::Base.connection.schema_search_path returns the string we pass it.    conn.schema_search_path = schema_name    # Directly from databases.rake.     # In Rails 2.3.5 databases.rake can be found in railties/lib/tasks/databases.rake    file = "#{Rails.root}/db/schema.rb"    if File.exists?(file)     Rails.logger.info "About to load the schema #{file}"     load(file)    else     abort %{#{file} doesn't exist yet. It's possible that you just ran a migration!}    end    Rails.logger.info "About to set search path back to #{old_search_path}."    conn.schema_search_path = old_search_path   end  end


Change line 38 to:

conn.schema_search_path = "#{schema_name}, #{old_search_path}"

I presume that postgres is trying to lookup existing table names when loading schema.rb and since you've set the search_path to only contain the new schema, it fails. This of course, is presuming you still have the public schema in your database.

Hope that helps.


Is there a gem/plugin that has these things already?

pg_power provides this functionality to create/drop PostgreSQL schemas in migration, like this:

def change  # Create schema  create_schema 'demography'  # Create new table in specific schema  create_table "countries", :schema => "demography" do |t|    # columns goes here  end  # Drop schema  drop_schema 'politics'end

Also it takes care about correctly dumping schemas into schema.rb file.