使用 PostgreSQL 的模式和 Rails 创建多租户应用程序

Posted

技术标签:

【中文标题】使用 PostgreSQL 的模式和 Rails 创建多租户应用程序【英文标题】:Creating a multi-tenant application using PostgreSQL's schemas and Rails 【发布时间】:2011-02-16 11:41:14 【问题描述】:

我已经想通了

我正在学习如何在 Rails 中创建一个多租户应用程序,该应用程序根据用于查看应用程序的域或子域来提供来自不同模式的数据。

我已经回答了一些问题:

    如何让 subdomain-fu 也与域一起使用? Here's someone that asked the same question 将您带到this blog。 什么数据库,它的结构如何?这是一个很好的talk by Guy Naor,还有一个很好的question about PostgreSQL and schemas。 我已经知道我的模式都将具有相同的结构。他们持有的数据会有所不同。那么,如何为所有架构运行迁移?这是an answer。

这三点涵盖了我需要了解的很多一般知识。但是,在接下来的步骤中,我似乎有很多实现方式。我希望有更好、更简单的方法。

最后,我的问题

当新用户注册时,我可以轻松创建架构。但是,什么是加载其他架构已经拥有的结构的最佳和最简单的方法?这里有一些问题/场景可能会给你一个更好的主意。

    我是否应该将它传递给一个 shell 脚本,该脚本将公共模式转储到一个临时模式中,然后将其导入到我的主数据库中(就像 Guy Naor 在他的视频中所说的那样)?这是quick summary/script I got from the helpful #postgres on freenode。虽然这可能会奏效,但我将不得不在 Rails 之外做很多事情,这让我有点不舒服..这也让我想到了下一个问题。 有没有办法直接从 Ruby on Rails 中做到这一点?就像创建一个 PostgreSQL 架构,然后只需将 Rails 数据库架构(schema.rb - 我知道,这很混乱)加载到该 PostgreSQL 架构中。 是否有一个 gem/plugin 已经拥有这些东西? 像“create_pg_schema_and_load_rails_schema(the_new_schema_name)”这样的方法。如果没有,我可能会努力制作一个,但我怀疑它对所有移动部件的测试效果如何(特别是如果我最终使用 shell 脚本来创建和管理新的 PostgreSQL 模式)。

谢谢,我希望不会太长!

【问题讨论】:

【参考方案1】:

2011 年 12 月 5 日更新

感谢 Brad Robertson 和他的团队,Apartment gem。它非常有用,可以完成很多繁重的工作。

但是,如果您要修改模式,我强烈建议您了解它的实际工作原理。熟悉 Jerod Santo's walkthrough ,这样您就会知道 Apartment gem 或多或少在做什么。

2011 年 8 月 20 日 11:23 GMT+8 更新

有人创建了一个blog post 并很好地完成了整个过程。

2010 年 5 月 11 日 11:26 GMT+8 更新

从昨晚开始,我已经能够找到一种可以创建新模式并将 schema.rb 加载到其中的方法。不确定我所做的是否正确(到目前为止似乎工作正常)但至少更近了一步。如果有更好的方法请告诉我。


  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

【讨论】:

注意:这不适用于 Rails 3,原因我还不知道。请参阅groups.google.com/group/rubyonrails-talk/browse_thread/thread/… 了解更多详情(但没有答案)。 对此的任何更新,我正在尝试实现完全相同的目标。我在 ActiveRecord 3.x Postgres 适配器中看到的是 table_exists? 方法似乎忽略了当前模式......因此加载 schema.rb 失败,因为它试图删除一个不存在的表 仅供参考,这是我在 Rails github 页面中提交的一个问题:github.com/rails/rails/issues/1518#issuecomment-1316011 @brad,我正在重新实现这个多租户模式,并决定使用更新后的方法。我正在关注 Jerod Santo 的博客文章并升级到 Rails 3.0.10。当我将 schema.rb 加载到新创建的模式中时,我遇到了关于现有表的相同错误。我跟着你的票,看起来它已经合并了。我查看了 3.0.10 的源代码以确保您的更改在那里。不幸的是,错误仍然存​​在!您的补丁是否特别修复了groups.google.com/forum/#!topic/rubyonrails-talk/wEwDM0iuvzw 中的错误? 有趣...我们一直在使用我的补丁,没有任何问题。我还没有升级到 3.0.10,但我会让你知道我的发现。仅供参考,我写了一个 gem 来进行多租户,我们正在生产中使用它和我的补丁。我希望尽快试用 3.0.10,但同时检查我的 gem,让我知道你的想法。【参考方案2】:

将第 38 行更改为:

conn.schema_search_path = "#schema_name, #old_search_path"

我认为 postgres 在加载 schema.rb 时会尝试查找现有表名,并且由于您已将 search_path 设置为仅包含新模式,因此它会失败。当然,这是假设您的数据库中仍有公共架构。

希望对您有所帮助。

【讨论】:

【参考方案3】:

是否有已经拥有这些东西的 gem/插件?

pg_power 提供此功能以在迁移中创建/删除 PostgreSQL 模式,如下所示:

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

它还负责将模式正确转储到 schema.rb 文件中。

【讨论】:

以上是关于使用 PostgreSQL 的模式和 Rails 创建多租户应用程序的主要内容,如果未能解决你的问题,请参考以下文章

Rails Postgresql 多个模式和相同的表名

rails3、postgresql 和 postGIS:在迁移中冻结

在 Rails 和 PostgreSQL 中完全忽略时区

Rails 在应用程序 ubuntu 中与 Postgresql 数据库连接

使用 Rails + Postgresql + Heroku 构建数据库

Rails 6 Docker 与 PostgreSQL 连接错误