ros-apartment 3.4.4 → 4.0.0.alpha2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +221 -530
- data/config/default.yml +9 -0
- data/lib/apartment/CLAUDE.md +76 -251
- data/lib/apartment/adapters/CLAUDE.md +34 -161
- data/lib/apartment/adapters/abstract_adapter.rb +233 -200
- data/lib/apartment/adapters/mysql2_adapter.rb +64 -50
- data/lib/apartment/adapters/postgresql_database_adapter.rb +76 -0
- data/lib/apartment/adapters/postgresql_schema_adapter.rb +118 -0
- data/lib/apartment/adapters/sqlite3_adapter.rb +32 -44
- data/lib/apartment/adapters/trilogy_adapter.rb +4 -20
- data/lib/apartment/cli/migrations.rb +113 -0
- data/lib/apartment/cli/pool.rb +67 -0
- data/lib/apartment/cli/seeds.rb +48 -0
- data/lib/apartment/cli/tenants.rb +85 -0
- data/lib/apartment/cli.rb +18 -0
- data/lib/apartment/concerns/model.rb +140 -0
- data/lib/apartment/config.rb +217 -0
- data/lib/apartment/configs/mysql_config.rb +19 -0
- data/lib/apartment/configs/postgresql_config.rb +39 -0
- data/lib/apartment/current.rb +18 -0
- data/lib/apartment/elevators/CLAUDE.md +49 -10
- data/lib/apartment/elevators/first_subdomain.rb +6 -6
- data/lib/apartment/elevators/generic.rb +79 -8
- data/lib/apartment/elevators/header.rb +26 -0
- data/lib/apartment/elevators/host.rb +5 -18
- data/lib/apartment/elevators/host_hash.rb +6 -10
- data/lib/apartment/elevators/subdomain.rb +8 -27
- data/lib/apartment/errors.rb +127 -0
- data/lib/apartment/instrumentation.rb +18 -0
- data/lib/apartment/lifecycle.rb +32 -0
- data/lib/apartment/migrator.rb +210 -37
- data/lib/apartment/patches/connection_handling.rb +98 -0
- data/lib/apartment/patches/live_tenant_propagation.rb +53 -0
- data/lib/apartment/pool_manager.rb +130 -0
- data/lib/apartment/pool_reaper.rb +211 -0
- data/lib/apartment/railtie.rb +172 -42
- data/lib/apartment/schema_cache.rb +26 -0
- data/lib/apartment/schema_dumper_patch.rb +48 -0
- data/lib/apartment/tasks/CLAUDE.md +16 -93
- data/lib/apartment/tasks/v4.rake +50 -0
- data/lib/apartment/tenant.rb +323 -45
- data/lib/apartment/tenant_name_validator.rb +87 -0
- data/lib/apartment/tenant_validator.rb +186 -0
- data/lib/apartment/test_fixtures.rb +34 -0
- data/lib/apartment/version.rb +1 -1
- data/lib/apartment.rb +304 -134
- data/lib/generators/apartment/install/install_generator.rb +6 -1
- data/lib/generators/apartment/install/templates/apartment.rb +65 -100
- data/lib/generators/apartment/install/templates/binstub +6 -0
- data/lib/rubocop/apartment.rb +4 -0
- data/lib/rubocop/cop/apartment/no_direct_current_write.rb +79 -0
- data/lib/rubocop/cop/apartment/prefer_block_switch.rb +39 -0
- data/ros-apartment.gemspec +14 -18
- metadata +89 -54
- data/.gitignore +0 -17
- data/.pryrc +0 -5
- data/.rspec +0 -4
- data/.rubocop.yml +0 -176
- data/.ruby-version +0 -1
- data/AGENTS.md +0 -19
- data/Appraisals +0 -145
- data/CLAUDE.md +0 -210
- data/CODE_OF_CONDUCT.md +0 -71
- data/Gemfile +0 -20
- data/Guardfile +0 -11
- data/RELEASING.md +0 -106
- data/Rakefile +0 -158
- data/context7.json +0 -4
- data/docs/adapters.md +0 -177
- data/docs/architecture.md +0 -274
- data/docs/elevators.md +0 -226
- data/docs/images/log_example.png +0 -0
- data/legacy_CHANGELOG.md +0 -965
- data/lib/apartment/active_record/connection_handling.rb +0 -31
- data/lib/apartment/active_record/internal_metadata.rb +0 -9
- data/lib/apartment/active_record/postgres/schema_dumper.rb +0 -20
- data/lib/apartment/active_record/postgresql_adapter.rb +0 -58
- data/lib/apartment/active_record/schema_migration.rb +0 -11
- data/lib/apartment/adapters/abstract_jdbc_adapter.rb +0 -20
- data/lib/apartment/adapters/jdbc_mysql_adapter.rb +0 -19
- data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +0 -62
- data/lib/apartment/adapters/postgis_adapter.rb +0 -13
- data/lib/apartment/adapters/postgresql_adapter.rb +0 -325
- data/lib/apartment/console.rb +0 -24
- data/lib/apartment/custom_console.rb +0 -42
- data/lib/apartment/deprecation.rb +0 -8
- data/lib/apartment/log_subscriber.rb +0 -45
- data/lib/apartment/model.rb +0 -29
- data/lib/apartment/tasks/enhancements.rb +0 -122
- data/lib/apartment/tasks/schema_dumper.rb +0 -110
- data/lib/apartment/tasks/task_helper.rb +0 -292
- data/lib/tasks/apartment.rake +0 -133
data/README.md
CHANGED
|
@@ -1,718 +1,409 @@
|
|
|
1
1
|
# Apartment
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/ros-apartment)
|
|
4
|
+
[](https://github.com/rails-on-services/apartment/actions/workflows/ci.yml)
|
|
4
5
|
[](https://codecov.io/gh/rails-on-services/apartment)
|
|
5
6
|
|
|
6
|
-
*
|
|
7
|
+
*Database-level multitenancy for Rails and ActiveRecord*
|
|
7
8
|
|
|
8
|
-
Apartment
|
|
9
|
-
application. If you need to have certain data sequestered based on account or company,
|
|
10
|
-
but still allow some data to exist in a common tenant, Apartment can help.
|
|
11
|
-
|
|
12
|
-
## Apartment Fork: ros-apartment
|
|
13
|
-
|
|
14
|
-
This gem is a fork of the original Apartment gem, which is no longer maintained. We have continued development under the name `ros-apartment` to keep the gem up-to-date and compatible with the latest versions of Rails. `ros-apartment` is designed as a drop-in replacement for the original, allowing you to seamlessly transition your application without code changes.
|
|
15
|
-
|
|
16
|
-
## Community Support
|
|
17
|
-
|
|
18
|
-
This project thrives on community support. Whether you have an idea for a new feature, find a bug, or need help with `ros-apartment`, we encourage you to participate! For questions and troubleshooting, check out our [Discussions board](https://github.com/rails-on-services/apartment/discussions) to connect with the community. You can also open issues or submit pull requests directly. We are committed to maintaining `ros-apartment` and ensuring it remains a valuable tool for Rails developers.
|
|
19
|
-
|
|
20
|
-
### Maintainer Update
|
|
21
|
-
|
|
22
|
-
As of May 2024, Apartment is maintained with the support of [CampusESP](https://www.campusesp.com). We continue to keep Apartment open-source under the MIT license. We also want to recognize and thank the previous maintainers for their valuable contributions to this project.
|
|
23
|
-
|
|
24
|
-
## Installation
|
|
25
|
-
|
|
26
|
-
### Requirements
|
|
27
|
-
|
|
28
|
-
- Ruby 3.1+
|
|
29
|
-
- Rails 7.0+ (Rails 6.1 support was dropped in v3.4.0)
|
|
30
|
-
- PostgreSQL, MySQL, or SQLite3
|
|
31
|
-
|
|
32
|
-
### Rails
|
|
33
|
-
|
|
34
|
-
Add the following to your Gemfile:
|
|
9
|
+
Apartment isolates tenant data at the **database level** — using PostgreSQL schemas or separate databases — so that tenant data separation is enforced by the database engine, not application code.
|
|
35
10
|
|
|
36
11
|
```ruby
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Then generate your `Apartment` config file using
|
|
41
|
-
|
|
42
|
-
```ruby
|
|
43
|
-
bundle exec rails generate apartment:install
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
This will create a `config/initializers/apartment.rb` initializer file.
|
|
47
|
-
Configure as needed using the docs below.
|
|
48
|
-
|
|
49
|
-
That's all you need to set up the Apartment libraries. If you want to switch tenants
|
|
50
|
-
on a per-user basis, look under "Usage - Switching tenants per request", below.
|
|
51
|
-
|
|
52
|
-
## Usage
|
|
53
|
-
|
|
54
|
-
### Video Tutorial
|
|
55
|
-
|
|
56
|
-
How to separate your application data into different accounts or companies.
|
|
57
|
-
[GoRails #47](https://gorails.com/episodes/multitenancy-with-apartment)
|
|
58
|
-
|
|
59
|
-
### Creating new Tenants
|
|
60
|
-
|
|
61
|
-
Before you can switch to a new apartment tenant, you will need to create it. Whenever
|
|
62
|
-
you need to create a new tenant, you can run the following command:
|
|
63
|
-
|
|
64
|
-
```ruby
|
|
65
|
-
Apartment::Tenant.create('tenant_name')
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
If you're using the [prepend environment](https://github.com/rails-on-services/apartment#handling-environments) config option or you AREN'T using Postgresql Schemas, this will create a tenant in the following format: "#{environment}\_tenant_name".
|
|
69
|
-
In the case of a sqlite database, this will be created in your 'db/' folder. With
|
|
70
|
-
other databases, the tenant will be created as a new DB within the system.
|
|
71
|
-
|
|
72
|
-
When you create a new tenant, all migrations will be run against that tenant, so it will be
|
|
73
|
-
up to date when create returns.
|
|
74
|
-
|
|
75
|
-
#### Notes on PostgreSQL
|
|
76
|
-
|
|
77
|
-
PostgreSQL works slightly differently than other databases when creating a new tenant. If you
|
|
78
|
-
are using PostgreSQL, Apartment by default will set up a new [schema](http://www.postgresql.org/docs/9.3/static/ddl-schemas.html)
|
|
79
|
-
and migrate into there. This provides better performance, and allows Apartment to work on systems like Heroku, which
|
|
80
|
-
would not allow a full new database to be created.
|
|
81
|
-
|
|
82
|
-
One can optionally use the full database creation instead if they want, though this is not recommended
|
|
83
|
-
|
|
84
|
-
### Switching Tenants
|
|
85
|
-
|
|
86
|
-
To switch tenants using Apartment, use the following command:
|
|
87
|
-
|
|
88
|
-
```ruby
|
|
89
|
-
Apartment::Tenant.switch('tenant_name') do
|
|
90
|
-
# ...
|
|
12
|
+
Apartment::Tenant.switch('acme') do
|
|
13
|
+
User.all # only returns users in the 'acme' schema/database
|
|
91
14
|
end
|
|
92
15
|
```
|
|
93
16
|
|
|
94
|
-
When
|
|
95
|
-
you specify (with the exception of excluded models, see below). The tenant is automatically
|
|
96
|
-
switched back at the end of the block to what it was before.
|
|
97
|
-
|
|
98
|
-
There is also `switch!` which doesn't take a block, but it's recommended to use `switch`.
|
|
99
|
-
To return to the default tenant, you can call `switch` with no arguments.
|
|
17
|
+
## When to Use Apartment
|
|
100
18
|
|
|
101
|
-
|
|
19
|
+
Apartment uses **schema-per-tenant** (PostgreSQL) or **database-per-tenant** (MySQL/SQLite) isolation. This is one of several approaches to multitenancy in Rails. Choose the right one for your situation:
|
|
102
20
|
|
|
103
|
-
|
|
21
|
+
| Approach | Isolation | Best for | Gem |
|
|
22
|
+
|----------|-----------|----------|-----|
|
|
23
|
+
| **Row-level** (shared tables, `WHERE tenant_id = ?`) | Application-enforced | Many tenants, greenfield apps, cross-tenant reporting | [`acts_as_tenant`](https://github.com/ErwinM/acts_as_tenant) |
|
|
24
|
+
| **Schema-level** (PostgreSQL schemas) | Database-enforced | Fewer high-value tenants, regulatory requirements, retrofitting existing apps | `ros-apartment` |
|
|
25
|
+
| **Database-level** (separate databases) | Full isolation | Strictest isolation, per-tenant performance tuning | `ros-apartment` |
|
|
104
26
|
|
|
105
|
-
|
|
106
|
-
Apartment::Tenant.switch(['tenant_1', 'tenant_2']) do
|
|
107
|
-
# ...
|
|
108
|
-
end
|
|
109
|
-
```
|
|
27
|
+
**Use Apartment when** you need hard data isolation between tenants — where a missed `WHERE` clause can't accidentally leak data across tenants. This is common in regulated industries, B2B SaaS with contractual isolation requirements, or when retrofitting an existing single-tenant app.
|
|
110
28
|
|
|
111
|
-
|
|
29
|
+
**Consider row-level tenancy instead** if you have many tenants (hundreds+), need cross-tenant queries, or are starting a greenfield project. Row-level is simpler, uses fewer database resources, and scales more linearly. See the [Arkency comparison](https://blog.arkency.com/comparison-of-approaches-to-multitenancy-in-rails-apps/) for a thorough analysis.
|
|
112
30
|
|
|
113
|
-
|
|
114
|
-
Apartment can support many different "Elevators" that can take care of this routing to your data.
|
|
31
|
+
## About ros-apartment
|
|
115
32
|
|
|
116
|
-
|
|
117
|
-
See the [Middleware Considerations](#middleware-considerations) section for more.
|
|
33
|
+
This gem is a maintained fork of the original [Apartment gem](https://github.com/influitive/apartment). Maintained by [CampusESP](https://www.campusesp.com) since 2024. Same `require 'apartment'`; v4 introduces a pool-per-tenant architecture that replaces the thread-local switching of v3. Tenant context is fiber-safe via `CurrentAttributes`, and connection pools are managed per tenant rather than swapping search paths on a shared connection. See the [upgrade guide](docs/upgrading-to-v4.md) for migration steps from v3.
|
|
118
34
|
|
|
119
|
-
|
|
120
|
-
by default. You can see this in `config/initializers/apartment.rb` after running
|
|
121
|
-
that generator. If you're *not* using the generator, you can specify your
|
|
122
|
-
elevator below. Note that in this case you will **need** to require the elevator
|
|
123
|
-
manually in your `application.rb` like so
|
|
35
|
+
## Installation
|
|
124
36
|
|
|
125
|
-
|
|
126
|
-
# config/application.rb
|
|
127
|
-
require 'apartment/elevators/subdomain' # or 'domain', 'first_subdomain', 'host'
|
|
128
|
-
```
|
|
37
|
+
### Requirements
|
|
129
38
|
|
|
130
|
-
|
|
39
|
+
- Ruby 3.3+
|
|
40
|
+
- Rails 7.2+
|
|
41
|
+
- PostgreSQL 14+, MySQL 8.4+, or SQLite3
|
|
131
42
|
|
|
132
|
-
|
|
43
|
+
### Setup
|
|
133
44
|
|
|
134
45
|
```ruby
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
class Application < Rails::Application
|
|
138
|
-
config.middleware.use Apartment::Elevators::Subdomain
|
|
139
|
-
end
|
|
140
|
-
end
|
|
46
|
+
# Gemfile
|
|
47
|
+
gem 'ros-apartment', require: 'apartment'
|
|
141
48
|
```
|
|
142
49
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# config/initializers/apartment/subdomain_exclusions.rb
|
|
147
|
-
Apartment::Elevators::Subdomain.excluded_subdomains = ['www']
|
|
50
|
+
```bash
|
|
51
|
+
bundle install
|
|
52
|
+
bundle exec rails generate apartment:install
|
|
148
53
|
```
|
|
149
54
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
#### Switch on first subdomain
|
|
55
|
+
## Quick Start
|
|
153
56
|
|
|
154
|
-
|
|
57
|
+
The generated initializer at `config/initializers/apartment.rb` configures Apartment:
|
|
155
58
|
|
|
156
59
|
```ruby
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
end
|
|
60
|
+
Apartment.configure do |config|
|
|
61
|
+
config.tenant_strategy = :schema # :schema (PostgreSQL) or :database_name (MySQL/SQLite)
|
|
62
|
+
config.tenants_provider = -> { Customer.pluck(:subdomain) }
|
|
63
|
+
config.default_tenant = 'public' # auto-defaults for :schema; required for :database_name
|
|
162
64
|
end
|
|
163
65
|
```
|
|
164
66
|
|
|
165
|
-
|
|
67
|
+
Tenant context is block-scoped. Always use `Apartment::Tenant.switch` with a block in application code; it guarantees cleanup on exceptions.
|
|
166
68
|
|
|
167
69
|
```ruby
|
|
168
|
-
|
|
169
|
-
Apartment::Elevators::FirstSubdomain.excluded_subdomains = ['www']
|
|
170
|
-
```
|
|
70
|
+
Apartment::Tenant.create('acme')
|
|
171
71
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
#### Switch on domain
|
|
175
|
-
|
|
176
|
-
To switch based on full domain (excluding the 'www' subdomains and top level domains *ie '.com'* ) use the following:
|
|
177
|
-
|
|
178
|
-
```ruby
|
|
179
|
-
# application.rb
|
|
180
|
-
module MyApplication
|
|
181
|
-
class Application < Rails::Application
|
|
182
|
-
config.middleware.use Apartment::Elevators::Domain
|
|
183
|
-
end
|
|
72
|
+
Apartment::Tenant.switch('acme') do
|
|
73
|
+
User.create!(name: 'Alice') # in the 'acme' schema
|
|
184
74
|
end
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
Note that if you have several subdomains, then it will match on the first *non-www* subdomain:
|
|
188
|
-
- example.com => example
|
|
189
|
-
- www.example.com => example
|
|
190
|
-
- a.example.com => a
|
|
191
|
-
|
|
192
|
-
#### Switch on full host using a hash
|
|
193
75
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
```ruby
|
|
197
|
-
# application.rb
|
|
198
|
-
module MyApplication
|
|
199
|
-
class Application < Rails::Application
|
|
200
|
-
config.middleware.use Apartment::Elevators::HostHash, {'example.com' => 'example_tenant'}
|
|
201
|
-
end
|
|
202
|
-
end
|
|
76
|
+
Apartment::Tenant.drop('acme')
|
|
203
77
|
```
|
|
204
78
|
|
|
205
|
-
|
|
79
|
+
`switch!` exists for console/REPL use but is discouraged in application code.
|
|
206
80
|
|
|
207
|
-
|
|
81
|
+
Global models that live outside tenant schemas use `pin_tenant`:
|
|
208
82
|
|
|
209
83
|
```ruby
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
config.middleware.use Apartment::Elevators::Host
|
|
214
|
-
end
|
|
84
|
+
class Company < ApplicationRecord
|
|
85
|
+
include Apartment::Model
|
|
86
|
+
pin_tenant # always queries the default (public) schema
|
|
215
87
|
end
|
|
216
88
|
```
|
|
217
89
|
|
|
218
|
-
|
|
90
|
+
## Configuration Reference
|
|
219
91
|
|
|
220
|
-
|
|
221
|
-
Apartment::Elevators::Host.ignored_first_subdomains = ['www']
|
|
222
|
-
```
|
|
92
|
+
All options are set in `config/initializers/apartment.rb` inside an `Apartment.configure` block.
|
|
223
93
|
|
|
224
|
-
|
|
225
|
-
- example.com => example.com
|
|
226
|
-
- www.example.com => example.com
|
|
227
|
-
- a.example.com => a.example.com
|
|
228
|
-
- www.a.example.com => a.example.com
|
|
94
|
+
### Required Options
|
|
229
95
|
|
|
230
|
-
|
|
96
|
+
`tenant_strategy`: the isolation method. `:schema` for PostgreSQL schema-per-tenant, `:database_name` for MySQL/SQLite database-per-tenant.
|
|
231
97
|
|
|
232
|
-
|
|
98
|
+
`tenants_provider`: a callable that returns tenant names. Called at migration time and by rake tasks. Example: `-> { Customer.pluck(:subdomain) }`.
|
|
233
99
|
|
|
234
|
-
|
|
235
|
-
# application.rb
|
|
236
|
-
module MyApplication
|
|
237
|
-
class Application < Rails::Application
|
|
238
|
-
# Obviously not a contrived example
|
|
239
|
-
config.middleware.use Apartment::Elevators::Generic, proc { |request| request.host.reverse }
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
Your other option is to subclass the Generic elevator and implement your own
|
|
245
|
-
switching mechanism. This is exactly how the other elevators work. Look at
|
|
246
|
-
the `subdomain.rb` elevator to get an idea of how this should work. Basically
|
|
247
|
-
all you need to do is subclass the generic elevator and implement your own
|
|
248
|
-
`parse_tenant_name` method that will ultimately return the name of the tenant
|
|
249
|
-
based on the request being made. It *could* look something like this:
|
|
250
|
-
|
|
251
|
-
```ruby
|
|
252
|
-
# app/middleware/my_custom_elevator.rb
|
|
253
|
-
class MyCustomElevator < Apartment::Elevators::Generic
|
|
254
|
-
|
|
255
|
-
# @return {String} - The tenant to switch to
|
|
256
|
-
def parse_tenant_name(request)
|
|
257
|
-
# request is an instance of Rack::Request
|
|
100
|
+
### Pool Settings
|
|
258
101
|
|
|
259
|
-
|
|
260
|
-
tenant_name = SomeModel.from_request(request)
|
|
102
|
+
`tenant_pool_size`: connections per tenant pool (default: 5).
|
|
261
103
|
|
|
262
|
-
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
```
|
|
104
|
+
`pool_idle_timeout`: seconds before an idle tenant pool is eligible for reaping (default: 300).
|
|
266
105
|
|
|
267
|
-
|
|
106
|
+
`max_total_connections`: hard cap across all tenant pools; nil for unlimited (default: nil).
|
|
268
107
|
|
|
269
|
-
|
|
108
|
+
### Elevator (Request Tenant Detection)
|
|
270
109
|
|
|
271
110
|
```ruby
|
|
272
|
-
|
|
111
|
+
config.elevator = :subdomain
|
|
112
|
+
config.elevator_options = {}
|
|
273
113
|
```
|
|
274
114
|
|
|
275
|
-
|
|
115
|
+
The Railtie auto-inserts elevator middleware after `ActionDispatch::Callbacks` (just before cookies/sessions in full mode; works in API mode too).
|
|
276
116
|
|
|
277
|
-
|
|
117
|
+
See the [Elevators](#elevators) section for available options.
|
|
278
118
|
|
|
279
|
-
|
|
119
|
+
### Migrations
|
|
280
120
|
|
|
281
|
-
|
|
121
|
+
`parallel_migration_threads`: number of threads for parallel tenant migration; 0 for sequential (default: 0).
|
|
282
122
|
|
|
283
|
-
|
|
284
|
-
Rails.application.config.middleware.insert_before Warden::Manager, Apartment::Elevators::Subdomain
|
|
285
|
-
```
|
|
123
|
+
`schema_load_strategy`: how to initialize new tenant schemas on create. `nil` (no schema loading), `:schema_rb`, or `:sql` (default: nil).
|
|
286
124
|
|
|
287
|
-
|
|
125
|
+
`seed_after_create`: run seeds after tenant creation (default: false).
|
|
288
126
|
|
|
289
|
-
|
|
127
|
+
`seed_data_file`: path to a custom seeds file; uses `db/seeds.rb` when nil (default: nil).
|
|
290
128
|
|
|
291
|
-
|
|
129
|
+
`schema_file`: path to a custom schema file for tenant creation (default: nil).
|
|
292
130
|
|
|
293
|
-
|
|
294
|
-
Apartment::Tenant.drop('tenant_name')
|
|
295
|
-
```
|
|
131
|
+
`check_pending_migrations`: raise `PendingMigrationError` in local environments when a tenant has unapplied migrations (default: true).
|
|
296
132
|
|
|
297
|
-
|
|
133
|
+
### Advanced
|
|
298
134
|
|
|
299
|
-
|
|
135
|
+
`schema_cache_per_tenant`: load per-tenant schema cache files when establishing tenant pools (default: false).
|
|
300
136
|
|
|
301
|
-
|
|
137
|
+
`active_record_log`: tag Rails log output with the current tenant using `ActiveSupport::TaggedLogging`. Log lines inside a `switch` block are tagged with `tenant=name`; nested switches stack tags (`[tenant=acme] [tenant=widgets]`). Requires `Rails.logger` to respond to `tagged` (default: false).
|
|
302
138
|
|
|
303
|
-
`
|
|
304
|
-
1. `tenant_list` - list available tenants while using the console
|
|
305
|
-
2. `st(tenant_name:String)` - Switches the context to the tenant name passed, if
|
|
306
|
-
it exists.
|
|
139
|
+
`sql_query_tags`: add a `tenant` tag to `ActiveRecord::QueryLogs` so SQL queries include a `/* tenant='name' */` comment. Visible in slow query logs, `pg_stat_activity`, and database monitoring tools (default: false).
|
|
307
140
|
|
|
308
|
-
|
|
141
|
+
`shard_key_prefix`: prefix for ActiveRecord shard keys used in tenant pool registration (default: `'apartment'`). Must match `/[a-z_][a-z0-9_]*/`.
|
|
309
142
|
|
|
310
|
-
|
|
311
|
-
the context in which you're running. It shows the environment as well as the tenant
|
|
312
|
-
that is currently switched to. In order for you to enable this, you need to require
|
|
313
|
-
the custom console in your application.
|
|
143
|
+
### Tenant Naming
|
|
314
144
|
|
|
315
|
-
|
|
316
|
-
Please note that we rely on `pry-rails` to edit the prompt, thus your project needs
|
|
317
|
-
to install it as well. In order to do so, you need to add `gem 'pry-rails'` to your
|
|
318
|
-
project's gemfile.
|
|
145
|
+
`environmentify_strategy`: how to namespace tenant names per Rails environment. `nil` (no prefix), `:prepend`, `:append`, or a callable (default: nil).
|
|
319
146
|
|
|
320
|
-
|
|
147
|
+
### RBAC
|
|
321
148
|
|
|
322
|
-
|
|
149
|
+
`migration_role`: a Symbol naming the database role used for migrations (default: nil, uses the connection's default role).
|
|
323
150
|
|
|
324
|
-
|
|
151
|
+
`app_role`: a String or callable returning the restricted role for application queries (default: nil).
|
|
325
152
|
|
|
326
|
-
|
|
153
|
+
### PostgreSQL
|
|
327
154
|
|
|
328
155
|
```ruby
|
|
329
156
|
Apartment.configure do |config|
|
|
330
|
-
|
|
157
|
+
config.configure_postgres do |pg|
|
|
158
|
+
pg.persistent_schemas = ['shared_extensions']
|
|
159
|
+
end
|
|
331
160
|
end
|
|
332
161
|
```
|
|
333
162
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
This is configurable by setting: `tenant_presence_check`. It defaults to true
|
|
337
|
-
in order to maintain the original gem behavior. This is only checked when using one of the PostgreSQL adapters.
|
|
338
|
-
The original gem behavior, when running `switch` would look for the existence of the schema before switching. This adds an extra query on every context switch. While in the default simple scenarios this is a valid check, in high volume platforms this adds some unnecessary overhead which can be detected in some other ways on the application level.
|
|
339
|
-
|
|
340
|
-
Setting this configuration value to `false` will disable the schema presence check before trying to switch the context.
|
|
163
|
+
PostgreSQL extensions (hstore, uuid-ossp, etc.) should be installed in a persistent schema so they're accessible from all tenant schemas:
|
|
341
164
|
|
|
342
165
|
```ruby
|
|
343
|
-
|
|
344
|
-
|
|
166
|
+
# lib/tasks/db_enhancements.rake
|
|
167
|
+
namespace :db do
|
|
168
|
+
task extensions: :environment do
|
|
169
|
+
ActiveRecord::Base.connection.execute('CREATE SCHEMA IF NOT EXISTS shared_extensions;')
|
|
170
|
+
ActiveRecord::Base.connection.execute('CREATE EXTENSION IF NOT EXISTS HSTORE SCHEMA shared_extensions;')
|
|
171
|
+
ActiveRecord::Base.connection.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp" SCHEMA shared_extensions;')
|
|
172
|
+
end
|
|
345
173
|
end
|
|
174
|
+
|
|
175
|
+
Rake::Task['db:create'].enhance { Rake::Task['db:extensions'].invoke }
|
|
176
|
+
Rake::Task['db:test:purge'].enhance { Rake::Task['db:extensions'].invoke }
|
|
346
177
|
```
|
|
347
178
|
|
|
348
|
-
|
|
179
|
+
Ensure your `database.yml` includes the persistent schema:
|
|
349
180
|
|
|
350
|
-
|
|
351
|
-
|
|
181
|
+
```yaml
|
|
182
|
+
schema_search_path: "public,shared_extensions"
|
|
183
|
+
```
|
|
352
184
|
|
|
353
|
-
|
|
185
|
+
Additional PostgreSQL options (set inside the `configure_postgres` block):
|
|
354
186
|
|
|
355
|
-
|
|
187
|
+
`include_schemas_in_dump`: non-public schemas to include in schema dumps, e.g., `%w[ext shared]` (default: []).
|
|
356
188
|
|
|
357
|
-
|
|
189
|
+
### MySQL
|
|
358
190
|
|
|
359
191
|
```ruby
|
|
360
192
|
Apartment.configure do |config|
|
|
361
|
-
config.
|
|
193
|
+
config.configure_mysql do |my|
|
|
194
|
+
# MySQL-specific options
|
|
195
|
+
end
|
|
362
196
|
end
|
|
363
197
|
```
|
|
364
198
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
If you have some models that should always access the 'public' tenant, you can specify this by configuring Apartment using `Apartment.configure`. This will yield a config object for you. You can set excluded models like so:
|
|
368
|
-
|
|
369
|
-
```ruby
|
|
370
|
-
config.excluded_models = ["User", "Company"] # these models will not be multi-tenanted, but remain in the global (public) namespace
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
Note that a string representation of the model name is now the standard so that models are properly constantized when reloaded in development
|
|
199
|
+
## Elevators
|
|
374
200
|
|
|
375
|
-
|
|
201
|
+
Elevators are Rack middleware that detect the tenant from the incoming request and call `Apartment::Tenant.switch` for the duration of that request.
|
|
376
202
|
|
|
377
|
-
|
|
378
|
-
> Since model exclusions must come from referencing a real ActiveRecord model, `has_and_belongs_to_many` is NOT supported. In order to achieve a many-to-many relationship for excluded models, you MUST use `has_many :through`. This way you can reference the join model in the excluded models configuration.
|
|
203
|
+
Available elevators:
|
|
379
204
|
|
|
380
|
-
|
|
205
|
+
- Subdomain: `acme.example.com` -> `'acme'`
|
|
206
|
+
- Domain: `acme.com` -> `'acme'`
|
|
207
|
+
- Host: full hostname matching
|
|
208
|
+
- HostHash: `{ 'acme.com' => 'acme_tenant' }`
|
|
209
|
+
- FirstSubdomain: first subdomain in a multi-level chain
|
|
210
|
+
- Header: tenant name from an HTTP header (new in v4)
|
|
381
211
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
Apartment can be forced to use raw SQL dumps insted of `schema.rb` for creating new schemas. Use this when you are using some extra features in postgres that can't be represented in `schema.rb`, like materialized views etc.
|
|
385
|
-
|
|
386
|
-
This only applies while using postgres adapter and `config.use_schemas` is set to `true`.
|
|
387
|
-
(Note: this option doesn't use `db/structure.sql`, it creates SQL dump by executing `pg_dump`)
|
|
388
|
-
|
|
389
|
-
Enable this option with:
|
|
212
|
+
Configuration via `config.elevator`:
|
|
390
213
|
|
|
391
214
|
```ruby
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
### Providing a Different default_tenant
|
|
396
|
-
|
|
397
|
-
By default, ActiveRecord will use `"$user", public` as the default `schema_search_path`. This can be modified if you wish to use a different default schema be setting:
|
|
398
|
-
|
|
399
|
-
```ruby
|
|
400
|
-
config.default_tenant = "some_other_schema"
|
|
215
|
+
Apartment.configure do |config|
|
|
216
|
+
config.elevator = :subdomain
|
|
217
|
+
end
|
|
401
218
|
```
|
|
402
219
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
### Persistent Schemas
|
|
220
|
+
The Railtie inserts the elevator after `ActionDispatch::Callbacks` automatically. In the full middleware stack this places it just before cookies, sessions, and authentication. In API mode (where cookies/sessions are absent), `Callbacks` is still present so the elevator works without changes.
|
|
406
221
|
|
|
407
|
-
|
|
222
|
+
If you need different positioning, skip `config.elevator` and insert manually:
|
|
408
223
|
|
|
409
224
|
```ruby
|
|
410
|
-
config.
|
|
225
|
+
# config/application.rb
|
|
226
|
+
config.middleware.insert_before 'Warden::Manager', Apartment::Elevators::Subdomain
|
|
411
227
|
```
|
|
412
228
|
|
|
413
|
-
###
|
|
414
|
-
|
|
415
|
-
Persistent Schemas have numerous useful applications. [Hstore](http://www.postgresql.org/docs/9.1/static/hstore.html), for instance, is a popular storage engine for Postgresql. In order to use extensions such as Hstore, you have to install it to a specific schema and have that always in the `schema_search_path`.
|
|
416
|
-
|
|
417
|
-
When using extensions, keep in mind:
|
|
418
|
-
* Extensions can only be installed into one schema per database, so we will want to install it into a schema that is always available in the `schema_search_path`
|
|
419
|
-
* The schema and extension need to be created in the database *before* they are referenced in migrations, database.yml or apartment.
|
|
420
|
-
* There does not seem to be a way to create the schema and extension using standard rails migrations.
|
|
421
|
-
* Rails db:test:prepare deletes and recreates the database, so it needs to be easy for the extension schema to be recreated here.
|
|
422
|
-
|
|
423
|
-
#### 1. Ensure the extensions schema is created when the database is created
|
|
229
|
+
### Custom Elevator
|
|
424
230
|
|
|
425
231
|
```ruby
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
# This file is used to setup a shared extensions #
|
|
430
|
-
# within a dedicated schema. This gives us the #
|
|
431
|
-
# advantage of only needing to enable extensions #
|
|
432
|
-
# in one place. #
|
|
433
|
-
# #
|
|
434
|
-
# This task should be run AFTER db:create but #
|
|
435
|
-
# BEFORE db:migrate. #
|
|
436
|
-
##################################################
|
|
437
|
-
|
|
438
|
-
namespace :db do
|
|
439
|
-
desc 'Also create shared_extensions Schema'
|
|
440
|
-
task :extensions => :environment do
|
|
441
|
-
# Create Schema
|
|
442
|
-
ActiveRecord::Base.connection.execute 'CREATE SCHEMA IF NOT EXISTS shared_extensions;'
|
|
443
|
-
# Enable Hstore
|
|
444
|
-
ActiveRecord::Base.connection.execute 'CREATE EXTENSION IF NOT EXISTS HSTORE SCHEMA shared_extensions;'
|
|
445
|
-
# Enable UUID-OSSP
|
|
446
|
-
ActiveRecord::Base.connection.execute 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp" SCHEMA shared_extensions;'
|
|
447
|
-
# Grant usage to public
|
|
448
|
-
ActiveRecord::Base.connection.execute 'GRANT usage ON SCHEMA shared_extensions to public;'
|
|
232
|
+
class MyElevator < Apartment::Elevators::Generic
|
|
233
|
+
def parse_tenant_name(request)
|
|
234
|
+
request.host.split('.').first
|
|
449
235
|
end
|
|
450
236
|
end
|
|
451
|
-
|
|
452
|
-
Rake::Task["db:create"].enhance do
|
|
453
|
-
Rake::Task["db:extensions"].invoke
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
Rake::Task["db:test:purge"].enhance do
|
|
457
|
-
Rake::Task["db:extensions"].invoke
|
|
458
|
-
end
|
|
459
237
|
```
|
|
460
238
|
|
|
461
|
-
|
|
239
|
+
Then pass the class directly:
|
|
462
240
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
```yaml
|
|
466
|
-
# database.yml
|
|
467
|
-
...
|
|
468
|
-
adapter: postgresql
|
|
469
|
-
schema_search_path: "public,shared_extensions"
|
|
470
|
-
...
|
|
241
|
+
```ruby
|
|
242
|
+
config.elevator = MyElevator
|
|
471
243
|
```
|
|
472
244
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
1. Append `?schema_search_path=public,hstore` to your `DATABASE_URL` environment variable, by this you don't have to revise the `database.yml` file (which is impossible since Heroku regenerates a completely different and immutable `database.yml` of its own on each deploy)
|
|
476
|
-
2. Run `heroku pg:psql` from your command line
|
|
477
|
-
3. And then `DROP EXTENSION hstore;` (**Note:** This will drop all columns that use `hstore` type, so proceed with caution; only do this with a fresh PostgreSQL instance)
|
|
478
|
-
4. Next: `CREATE SCHEMA IF NOT EXISTS hstore;`
|
|
479
|
-
5. Finally: `CREATE EXTENSION IF NOT EXISTS hstore SCHEMA hstore;` and hit enter (`\q` to exit)
|
|
480
|
-
|
|
481
|
-
To double check, login to the console of your Heroku app and see if `Apartment.connection.schema_search_path` is `public,hstore`
|
|
245
|
+
## Pinned Models (Global Tables)
|
|
482
246
|
|
|
483
|
-
|
|
247
|
+
Models that belong to all tenants (users, companies, plans) are pinned to the default schema:
|
|
484
248
|
|
|
485
249
|
```ruby
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
250
|
+
class User < ApplicationRecord
|
|
251
|
+
include Apartment::Model
|
|
252
|
+
pin_tenant
|
|
253
|
+
end
|
|
490
254
|
```
|
|
491
255
|
|
|
492
|
-
|
|
256
|
+
Why `pin_tenant`:
|
|
493
257
|
|
|
494
|
-
|
|
495
|
-
|
|
258
|
+
- Declarative: the model declares its own tenancy, not a distant config list
|
|
259
|
+
- Zeitwerk-safe: no string-to-class resolution at boot time
|
|
260
|
+
- Composable: works with `connected_to(role: :reading)` for read replicas
|
|
496
261
|
|
|
497
|
-
|
|
262
|
+
Use `has_many :through` for associations between pinned and tenant models. `has_and_belongs_to_many` is not supported across schemas.
|
|
498
263
|
|
|
499
|
-
|
|
264
|
+
Pinned models work correctly inside `connected_to(role: :reading)` blocks. The pin bypasses Apartment's tenant routing; Rails' own role routing takes over.
|
|
500
265
|
|
|
501
|
-
|
|
502
|
-
psql -U postgres -d template1 -c "CREATE SCHEMA shared_extensions AUTHORIZATION some_username;"
|
|
503
|
-
psql -U postgres -d template1 -c "CREATE EXTENSION IF NOT EXISTS hstore SCHEMA shared_extensions;"
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
The *ideal* setup would actually be to install `hstore` into the `public` schema and leave the public
|
|
507
|
-
schema in the `search_path` at all times. We won't be able to do this though until public doesn't
|
|
508
|
-
also contain the tenanted tables, which is an open issue with no real milestone to be completed.
|
|
509
|
-
Happy to accept PR's on the matter.
|
|
266
|
+
For the edge case of models using `connects_to` with a separate database, see [Known Limitations](#known-limitations).
|
|
510
267
|
|
|
511
|
-
|
|
268
|
+
## Callbacks
|
|
512
269
|
|
|
513
|
-
|
|
514
|
-
of dbs to Apartment. You can make this dynamic by providing a Proc object to be called on migrations.
|
|
515
|
-
This object should yield an array of string representing each tenant name. Example:
|
|
270
|
+
Hook into tenant lifecycle events:
|
|
516
271
|
|
|
517
272
|
```ruby
|
|
518
|
-
|
|
519
|
-
|
|
273
|
+
Apartment::Adapters::AbstractAdapter.set_callback :create, :after do |adapter|
|
|
274
|
+
# runs after a new tenant is created
|
|
275
|
+
end
|
|
520
276
|
|
|
521
|
-
|
|
522
|
-
|
|
277
|
+
Apartment::Adapters::AbstractAdapter.set_callback :switch, :before do |adapter|
|
|
278
|
+
# runs before switching tenants
|
|
279
|
+
end
|
|
523
280
|
```
|
|
524
281
|
|
|
525
|
-
|
|
282
|
+
## Migrations
|
|
526
283
|
|
|
527
|
-
|
|
528
|
-
rake db:migrate
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
This just invokes `Apartment::Migrator.migrate(#{tenant_name})` for each tenant name supplied
|
|
532
|
-
from `Apartment.tenant_names`
|
|
284
|
+
Rake tasks:
|
|
533
285
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
286
|
+
- `apartment:create`: create all tenants from `tenants_provider`
|
|
287
|
+
- `apartment:drop`: drop all tenants
|
|
288
|
+
- `apartment:migrate`: run pending migrations on all tenants
|
|
289
|
+
- `apartment:seed`: seed all tenants
|
|
290
|
+
- `apartment:rollback`: rollback last migration on all tenants
|
|
537
291
|
|
|
538
|
-
|
|
292
|
+
The Railtie hooks the primary `db:migrate` task (when defined) so that tenant migrations run after the primary database migrates.
|
|
539
293
|
|
|
540
|
-
|
|
294
|
+
### Parallel Migrations
|
|
541
295
|
|
|
542
|
-
|
|
296
|
+
For applications with many schemas:
|
|
543
297
|
|
|
544
298
|
```ruby
|
|
545
|
-
|
|
546
|
-
config.parallel_migration_threads = 4
|
|
547
|
-
end
|
|
299
|
+
config.parallel_migration_threads = 4 # 0 = sequential (default)
|
|
548
300
|
```
|
|
549
301
|
|
|
550
|
-
|
|
302
|
+
Platform notes: parallel migrations use threads. On macOS, libpq has known fork-safety issues, so threads are preferred over processes. Parallel migrations disable PostgreSQL advisory locks; ensure your migrations are safe to run concurrently.
|
|
551
303
|
|
|
552
|
-
|
|
553
|
-
|--------|---------|-------------|
|
|
554
|
-
| `parallel_migration_threads` | `0` | Number of parallel workers. `0` disables parallelism (recommended default). |
|
|
555
|
-
| `parallel_strategy` | `:auto` | `:auto` detects platform, `:threads` forces thread-based, `:processes` forces fork-based. |
|
|
556
|
-
| `manage_advisory_locks` | `true` | Disables PostgreSQL advisory locks during parallel execution to prevent deadlocks. |
|
|
304
|
+
## Known Limitations
|
|
557
305
|
|
|
558
|
-
|
|
306
|
+
### `connects_to` with Separate Databases
|
|
559
307
|
|
|
560
|
-
|
|
308
|
+
If a model (or its abstract base class) uses `connects_to` to point at a completely different database (not just different roles on the same DB), Apartment's `connection_pool` patch will attempt to create a tenant pool for it.
|
|
561
309
|
|
|
562
|
-
|
|
563
|
-
- **macOS/Windows**: Uses thread-based parallelism (avoids libpq fork issues)
|
|
310
|
+
Workaround: add `include Apartment::Model` and `pin_tenant` on the abstract class or model that declares `connects_to` to a separate database.
|
|
564
311
|
|
|
565
|
-
|
|
312
|
+
The common pattern of `ApplicationRecord` using `connects_to` with multiple roles (writing/reading) on the same database works correctly; Apartment keys pools by `tenant:role` and respects Rails' role routing.
|
|
566
313
|
|
|
567
|
-
|
|
314
|
+
## ActionController::Live Streaming
|
|
568
315
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
**Use parallel migrations when:**
|
|
572
|
-
- You have many tenants and sequential migration time is problematic
|
|
573
|
-
- Your migrations only modify objects within each tenant's schema
|
|
574
|
-
- You've verified your migrations have no cross-schema side effects
|
|
575
|
-
|
|
576
|
-
**Stick with sequential execution when:**
|
|
577
|
-
- Migrations create or modify PostgreSQL extensions
|
|
578
|
-
- Migrations modify shared types, functions, or other database-wide objects
|
|
579
|
-
- Migrations have ordering dependencies that span tenants
|
|
580
|
-
- You're unsure whether your migrations are parallel-safe
|
|
581
|
-
|
|
582
|
-
##### Connection Pool Sizing
|
|
583
|
-
|
|
584
|
-
The `parallel_migration_threads` value should be less than your database connection pool size to avoid exhaustion errors. If you set `parallel_migration_threads: 8`, ensure your `pool` setting in `database.yml` is at least 10 to leave headroom.
|
|
585
|
-
|
|
586
|
-
##### Schema Dump After Migration
|
|
587
|
-
|
|
588
|
-
Apartment automatically dumps `schema.rb` after successful migrations, ensuring the dump comes from the public schema (the source of truth). This respects Rails' `dump_schema_after_migration` setting.
|
|
589
|
-
|
|
590
|
-
### Handling Environments
|
|
591
|
-
|
|
592
|
-
By default, when not using postgresql schemas, Apartment will prepend the environment to the tenant name
|
|
593
|
-
to ensure there is no conflict between your environments. This is mainly for the benefit of your development
|
|
594
|
-
and test environments. If you wish to turn this option off in production, you could do something like:
|
|
316
|
+
Apartment v4 handles tenant propagation across `ActionController::Live`'s spawned streaming thread automatically, under both `:thread` and `:fiber` isolation. Including `ActionController::Live` in your controller is sufficient — no additional configuration:
|
|
595
317
|
|
|
596
318
|
```ruby
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
## Tenants on different servers
|
|
601
|
-
|
|
602
|
-
You can store your tenants in different databases on one or more servers.
|
|
603
|
-
To do it, specify your `tenant_names` as a hash, keys being the actual tenant names,
|
|
604
|
-
values being a hash with the database configuration to use.
|
|
319
|
+
class StreamingController < ApplicationController
|
|
320
|
+
include ActionController::Live
|
|
605
321
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
port: 5555,
|
|
615
|
-
database: 'postgres' # this is not the name of the tenant's db
|
|
616
|
-
# but the name of the database to connect to, before creating the tenant's db
|
|
617
|
-
# mandatory in postgresql
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
# or using a lambda:
|
|
621
|
-
config.tenant_names = lambda do
|
|
622
|
-
Tenant.all.each_with_object({}) do |tenant, hash|
|
|
623
|
-
hash[tenant.name] = tenant.db_configuration
|
|
322
|
+
def show
|
|
323
|
+
response.headers['Content-Type'] = 'text/event-stream'
|
|
324
|
+
# Apartment::Tenant.current returns the request's tenant here,
|
|
325
|
+
# even though we're now executing on the OS thread Rails spawned
|
|
326
|
+
# for streaming.
|
|
327
|
+
response.stream.write("data: #{{ tenant: Apartment::Tenant.current }.to_json}\n\n")
|
|
328
|
+
ensure
|
|
329
|
+
response.stream.close
|
|
624
330
|
end
|
|
625
331
|
end
|
|
626
332
|
```
|
|
627
333
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
Both these gems have been forked as a side consequence of having a new gem name.
|
|
631
|
-
You can use them exactly as you were using before. They are, just like this one
|
|
632
|
-
a drop-in replacement.
|
|
633
|
-
|
|
634
|
-
See [apartment-sidekiq](https://github.com/rails-on-services/apartment-sidekiq)
|
|
635
|
-
or [apartment-activejob](https://github.com/rails-on-services/apartment-activejob).
|
|
636
|
-
|
|
637
|
-
## Callbacks
|
|
638
|
-
|
|
639
|
-
You can execute callbacks when switching between tenants or creating a new one, Apartment provides the following callbacks:
|
|
334
|
+
How it works: Apartment prepends `ActionController::Live#process` with a patch that backports [rails/rails#56902](https://github.com/rails/rails/pull/56902) to released Rails versions — it points `Thread.current.active_support_execution_state` at the request fiber's hash for the duration of the request, so Rails' own `share_with` carries all `CurrentAttributes` (apartment's tenant plus any app-defined ones) into the spawned streaming thread. User-spawned threads or fibers *inside* a Live action (`Thread.new`, `Async { }`, raw `Fiber.new`) escape the patch and need explicit `Apartment::Tenant.switch` wrapping. See the [upgrading guide](docs/upgrading-to-v4.md) and [`docs/designs/rails-boundary-tenancy.md`](docs/designs/rails-boundary-tenancy.md).
|
|
640
335
|
|
|
641
|
-
|
|
642
|
-
- after_create
|
|
643
|
-
- before_switch
|
|
644
|
-
- after_switch
|
|
336
|
+
## Background Workers
|
|
645
337
|
|
|
646
|
-
|
|
338
|
+
Use block-scoped switching in jobs:
|
|
647
339
|
|
|
648
340
|
```ruby
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
class AbstractAdapter
|
|
654
|
-
set_callback :switch, :before do |object|
|
|
655
|
-
...
|
|
656
|
-
end
|
|
341
|
+
class TenantJob < ApplicationJob
|
|
342
|
+
def perform(tenant, data)
|
|
343
|
+
Apartment::Tenant.switch(tenant) do
|
|
344
|
+
# process job
|
|
657
345
|
end
|
|
658
346
|
end
|
|
659
347
|
end
|
|
660
348
|
```
|
|
661
349
|
|
|
662
|
-
|
|
350
|
+
For automatic tenant propagation:
|
|
663
351
|
|
|
664
|
-
|
|
665
|
-
|
|
352
|
+
- [apartment-sidekiq](https://github.com/rails-on-services/apartment-sidekiq)
|
|
353
|
+
- [apartment-activejob](https://github.com/rails-on-services/apartment-activejob)
|
|
666
354
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
355
|
+
A job that forgets to switch runs in the default tenant — for `Rails.cache` and
|
|
356
|
+
other `Tenant.current`-derived resources that silently contaminate another
|
|
357
|
+
tenant's keyspace. Guard routed work with `Apartment::Tenant.require_tenant!`
|
|
358
|
+
(raises unless a real, non-default tenant is active) and pinned/global work with
|
|
359
|
+
`require_default_tenant!`. See [Tenant-Aware Caching](docs/caching.md) for the
|
|
360
|
+
routed-vs-pinned model and the two-store recipe.
|
|
671
361
|
|
|
672
|
-
##
|
|
362
|
+
## Convenience Methods
|
|
673
363
|
|
|
674
|
-
|
|
364
|
+
`Apartment.tenant_names` returns the current tenant list (delegates to `config.tenants_provider.call`). Preserves the v3 API so existing call sites work without changes.
|
|
675
365
|
|
|
676
|
-
|
|
366
|
+
`Apartment.excluded_models` returns the excluded models list (delegates to `config.excluded_models`). Deprecated in v4; use `Apartment::Model` + `pin_tenant` instead.
|
|
677
367
|
|
|
678
|
-
|
|
679
|
-
- Before opening a new issue, please check the [issue tracker](https://github.com/rails-on-services/apartment/issues) and our [Discussions board](https://github.com/rails-on-services/apartment/discussions) to see if the topic has already been reported or discussed. This helps us avoid duplication and focus on solving the issue efficiently.
|
|
368
|
+
## Troubleshooting
|
|
680
369
|
|
|
681
|
-
|
|
682
|
-
- Ensure your report includes a clear description of the problem, steps to reproduce, and relevant logs or error messages.
|
|
683
|
-
- If possible, provide a minimal reproducible example or a failing test case that demonstrates the issue.
|
|
370
|
+
If tenant switching raises unexpected errors, verify that `tenants_provider` returns valid tenant names and that the tenant exists in the database.
|
|
684
371
|
|
|
685
|
-
|
|
686
|
-
- For new features, open an issue to discuss your idea before starting development. This allows the maintainers and community to provide feedback and ensure the feature aligns with the project's goals.
|
|
687
|
-
- Please be as detailed as possible when describing the feature, its use case, and its potential impact on the existing functionality.
|
|
372
|
+
## Upgrading from v3
|
|
688
373
|
|
|
689
|
-
|
|
690
|
-
- Fork the repository and create a feature branch (`git checkout -b my-feature-branch`).
|
|
691
|
-
- Follow the existing code style and ensure your changes are well-documented and tested.
|
|
692
|
-
- Run the tests locally to verify that your changes do not introduce new issues.
|
|
693
|
-
- Use [Appraisal](https://github.com/thoughtbot/appraisal) to test against multiple Rails versions. Ensure all tests pass for supported Rails versions.
|
|
694
|
-
- Submit your pull request to the `development` branch, not `main`.
|
|
695
|
-
- Include a detailed description of your changes and reference any related issue numbers (e.g., "Fixes #123" or "Closes #456").
|
|
374
|
+
See the [upgrade guide](docs/upgrading-to-v4.md) for a complete list of breaking changes and migration steps.
|
|
696
375
|
|
|
697
|
-
|
|
698
|
-
- The maintainers will review your pull request and may provide feedback or request changes. We appreciate your patience during this process, as we strive to maintain a high standard for code quality.
|
|
699
|
-
- Once approved, your pull request will be merged into the `development` branch. Periodically, we merge the `development` branch into `main` for official releases.
|
|
376
|
+
## RuboCop cops
|
|
700
377
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
- If your contribution affects multiple versions of Rails, use Appraisal to verify compatibility across versions.
|
|
704
|
-
- Rake tasks (see the Rakefile) are available to help set up your test databases and run tests.
|
|
378
|
+
Apartment ships two optional RuboCop cops that enforce the block-form
|
|
379
|
+
tenant-switching discipline. Enable them in your application's `.rubocop.yml`:
|
|
705
380
|
|
|
706
|
-
|
|
381
|
+
```yaml
|
|
382
|
+
require: rubocop/apartment
|
|
383
|
+
inherit_gem:
|
|
384
|
+
ros-apartment: config/default.yml
|
|
385
|
+
```
|
|
707
386
|
|
|
708
|
-
|
|
387
|
+
- **`Apartment/NoDirectCurrentWrite`** (error) — bans assigning
|
|
388
|
+
`Apartment::Current.tenant` / `.previous_tenant` directly. Change tenant context
|
|
389
|
+
with `Apartment::Tenant.switch(tenant) { ... }` (or `with_default_tenant` for
|
|
390
|
+
global work), which guarantees a restore via `ensure`.
|
|
391
|
+
- **`Apartment/PreferBlockSwitch`** (warning) — nudges `Apartment::Tenant.switch!`
|
|
392
|
+
toward the block form. `reset` is not flagged.
|
|
709
393
|
|
|
710
|
-
|
|
394
|
+
Both match the qualified `Apartment::` receiver only. Scope them to your
|
|
395
|
+
application code with the standard `Exclude:` keys if needed. See
|
|
396
|
+
[`docs/designs/rubocop-cops.md`](docs/designs/rubocop-cops.md) for the rationale.
|
|
711
397
|
|
|
712
|
-
|
|
398
|
+
## Contributing
|
|
713
399
|
|
|
714
|
-
|
|
400
|
+
1. Check [existing issues](https://github.com/rails-on-services/apartment/issues) and [discussions](https://github.com/rails-on-services/apartment/discussions)
|
|
401
|
+
2. Fork and create a feature branch
|
|
402
|
+
3. Write tests: we don't merge without them
|
|
403
|
+
4. Run `bundle exec rspec spec/unit/` and `bundle exec rubocop`
|
|
404
|
+
5. Use [Appraisal](https://github.com/thoughtbot/appraisal) to test across Rails versions: `bundle exec appraisal rspec spec/unit/`
|
|
405
|
+
6. Submit PR to the `main` branch
|
|
715
406
|
|
|
716
407
|
## License
|
|
717
408
|
|
|
718
|
-
|
|
409
|
+
[MIT License](http://www.opensource.org/licenses/MIT)
|