From the GoRails video:
https://gorails.com/episodes/row-level-multitenancy-in-rails
Their Rails 4.2.1 example code:
https://github.com/gorails-screencasts/row-level-multitenancy
I first created the project in Rubymine as Rails app using:
- rails 7.0.4.3
- ruby 3.2.2
- database: postgresql
- javascript: importmaps
- additional option: none
After project setup in Rubymine, add .idea to .gitignore file.
Then ran:
bundle exec rake db:drop db:create db:migrate db:seed
Commit: 'Initial project setup', 173d0ac
The video assumes an existing rails project with devise already installed. So I did that setup.
I first added the devise and acts_as_tenant gems to the Gemfile and ran:
bundle # same as 'bundle install
Then ran:
bundle exec rails generate devise:install
bundle exec rails generate devise:views
The devise install walks through the steps to add the following to the config/environments/development.rb file:
config.action_mailer.default_url_options = { host: 'lvh.me', port: 3000 }
I also added the following to the config/environments/development.rb file to use the 'lvh.me' host name, which is a new requirement in recent versions of Rails:
config.hosts << 'lvh.me' # allows lvh.me to work in development
config.hosts << /.*\.lvh\.me/ # allows subdomains to work in development
I did not change or configure devise in any other way.
I then created a controller to use as the root of the application:
bundle exec rails generate controller Hello index
...and set the root in the routes.rb file:
Rails.application.routes.draw do
root "hello#index"
end
Part of the devise setup is to create a User model:
bundle exec rails generate devise User
Then ran the db migration:
bundle exec rake db:migrate
I was then able to start the server and see the root page at: http://lvh.me:3000
Commit: 'Gems, devise, User, and root route setup', 78e1ba3
Next, I added the filter to the application controller for tenant lookup.
Then added an initializer for acts_as_tenant, turning the require_tenant setting off.
This allows the application to run without a tenant, which assumes there is a top level presence in a multi-tenant system that has access to all tenant data.
Next in the video, I scaffolded Post with a belongs_to User so that Posts are a tenant specific object:
bundle exec rails generate scaffold Post user:belongs_to title body:text
Then ran the db migration:
bundle exec rake db:migrate
I also added the following to the Post model:
class Post < ApplicationRecord
acts_as_tenant(:user)
belongs_to :user
end
The acts_as_tenant(:user) line adds a default scope to the Post model that enforces a lookup by the tenant, which in this case is the User model.
Commit: 'Setting up a Post model with a tenant filter', 2789867
Following along, I then removed user_id from the post params and post forms.
I also added (this is just me, it was not in the video) a menu to get around the application.
Notice the Devise sign out link uses a different format for Rails 7 now that it includes Turbo.
<div>
<span><%= link_to 'Home |', root_path %></span>
<span><%= link_to 'Sign Up |', new_user_registration_path %></span>
<span><%= link_to 'Login |', user_session_path %></span>
<span><%= link_to 'Posts |', posts_path %></span>
<span><%= link_to 'New Post |', new_post_path %></span>
<span><%= link_to 'Logout', destroy_user_session_path, data: { turbo_method: :delete } %></span>
</div>
This requiring the delete method is set automatically by default in the Devise initializer as:
config.sign_out_via = :delete
Commit: 'Post controller and forms setup', a915038
At this point I was able to regster two new users ([email protected], [email protected]), login, and create posts which only show up based on the tenant associated with the post. Which, in this case is "User".
This next part wasn't in the video but I added a redirect after login to the posts index page:
def after_sign_in_path_for(resource)
stored_location_for(resource) || posts_path
end
This takes you right to posts so you can see that they are being filtered.