pg_multitenant_schemas 0.1.3 → 0.2.0
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/.ruby-version +1 -0
- data/CHANGELOG.md +46 -0
- data/README.md +269 -16
- data/docs/README.md +77 -0
- data/docs/configuration.md +340 -0
- data/docs/context.md +292 -0
- data/docs/errors.md +498 -0
- data/docs/integration_testing.md +454 -0
- data/docs/migrator.md +291 -0
- data/docs/rails_integration.md +468 -0
- data/docs/schema_switcher.md +182 -0
- data/docs/tenant_resolver.md +394 -0
- data/docs/testing.md +358 -0
- data/examples/context_management.rb +198 -0
- data/examples/migration_workflow.rb +50 -0
- data/examples/rails_integration/controller_examples.rb +368 -0
- data/examples/schema_operations.rb +124 -0
- data/lib/pg_multitenant_schemas/configuration.rb +4 -4
- data/lib/pg_multitenant_schemas/migration_display_reporter.rb +30 -0
- data/lib/pg_multitenant_schemas/migration_executor.rb +81 -0
- data/lib/pg_multitenant_schemas/migration_schema_operations.rb +54 -0
- data/lib/pg_multitenant_schemas/migration_status_reporter.rb +65 -0
- data/lib/pg_multitenant_schemas/migrator.rb +89 -0
- data/lib/pg_multitenant_schemas/schema_switcher.rb +40 -66
- data/lib/pg_multitenant_schemas/tasks/advanced_tasks.rake +21 -0
- data/lib/pg_multitenant_schemas/tasks/basic_tasks.rake +20 -0
- data/lib/pg_multitenant_schemas/tasks/pg_multitenant_schemas.rake +53 -143
- data/lib/pg_multitenant_schemas/tasks/tenant_tasks.rake +65 -0
- data/lib/pg_multitenant_schemas/tenant_task_helpers.rb +102 -0
- data/lib/pg_multitenant_schemas/version.rb +1 -1
- data/lib/pg_multitenant_schemas.rb +10 -5
- data/pg_multitenant_schemas.gemspec +10 -9
- data/rails_integration/app/controllers/application_controller.rb +6 -0
- data/rails_integration/app/models/tenant.rb +6 -0
- metadata +39 -17
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Configuration - Gem Settings and Options
|
|
2
|
+
|
|
3
|
+
**File**: `lib/pg_multitenant_schemas/configuration.rb`
|
|
4
|
+
|
|
5
|
+
## 📋 Overview
|
|
6
|
+
|
|
7
|
+
The `Configuration` class manages all configurable aspects of the PG Multitenant Schemas gem. It provides a centralized way to customize behavior, set defaults, and integrate with different Rails applications.
|
|
8
|
+
|
|
9
|
+
## 🎯 Purpose
|
|
10
|
+
|
|
11
|
+
- **Centralized Settings**: Single location for all gem configuration
|
|
12
|
+
- **Framework Integration**: Seamless Rails integration options
|
|
13
|
+
- **Customization**: Flexible options for different deployment scenarios
|
|
14
|
+
- **Defaults Management**: Sensible defaults with override capabilities
|
|
15
|
+
|
|
16
|
+
## 🔧 Configuration Options
|
|
17
|
+
|
|
18
|
+
### Core Settings
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
PgMultitenantSchemas.configure do |config|
|
|
22
|
+
# Default schema to use (usually 'public')
|
|
23
|
+
config.default_schema = 'public'
|
|
24
|
+
|
|
25
|
+
# ActiveRecord connection class
|
|
26
|
+
config.connection_class = 'ApplicationRecord'
|
|
27
|
+
|
|
28
|
+
# Tenant model class name
|
|
29
|
+
config.tenant_model = 'Tenant'
|
|
30
|
+
|
|
31
|
+
# Schema naming strategy
|
|
32
|
+
config.schema_prefix = nil # No prefix by default
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Connection Management
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
PgMultitenantSchemas.configure do |config|
|
|
40
|
+
# Connection class for database operations
|
|
41
|
+
config.connection_class = 'ApplicationRecord' # Default
|
|
42
|
+
# config.connection_class = 'TenantRecord' # Custom connection
|
|
43
|
+
# config.connection_class = 'ReadOnlyRecord' # Read-only operations
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Tenant Resolution
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
PgMultitenantSchemas.configure do |config|
|
|
51
|
+
# Model used for tenant lookup
|
|
52
|
+
config.tenant_model = 'Tenant' # Default
|
|
53
|
+
# config.tenant_model = 'Organization'
|
|
54
|
+
# config.tenant_model = 'Account'
|
|
55
|
+
|
|
56
|
+
# Attribute used for schema naming
|
|
57
|
+
config.tenant_schema_attribute = :subdomain # Default
|
|
58
|
+
# config.tenant_schema_attribute = :slug
|
|
59
|
+
# config.tenant_schema_attribute = :code
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Schema Management
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
PgMultitenantSchemas.configure do |config|
|
|
67
|
+
# Default schema for non-tenant operations
|
|
68
|
+
config.default_schema = 'public'
|
|
69
|
+
|
|
70
|
+
# Schema naming prefix (optional)
|
|
71
|
+
config.schema_prefix = 'tenant_' # Results in 'tenant_acme', 'tenant_beta'
|
|
72
|
+
# config.schema_prefix = nil # Results in 'acme', 'beta'
|
|
73
|
+
|
|
74
|
+
# Schema exclusion patterns
|
|
75
|
+
config.excluded_schemas = ['information_schema', 'pg_catalog', 'pg_toast']
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 🏗️ Implementation Details
|
|
80
|
+
|
|
81
|
+
### Configuration Class Structure
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
module PgMultitenantSchemas
|
|
85
|
+
class Configuration
|
|
86
|
+
attr_accessor :default_schema,
|
|
87
|
+
:connection_class,
|
|
88
|
+
:tenant_model,
|
|
89
|
+
:tenant_schema_attribute,
|
|
90
|
+
:schema_prefix,
|
|
91
|
+
:excluded_schemas
|
|
92
|
+
|
|
93
|
+
def initialize
|
|
94
|
+
@default_schema = 'public'
|
|
95
|
+
@connection_class = 'ApplicationRecord'
|
|
96
|
+
@tenant_model = 'Tenant'
|
|
97
|
+
@tenant_schema_attribute = :subdomain
|
|
98
|
+
@schema_prefix = nil
|
|
99
|
+
@excluded_schemas = default_excluded_schemas
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Dynamic Configuration Loading
|
|
106
|
+
|
|
107
|
+
The configuration supports dynamic loading and environment-specific settings:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# Load from environment variables
|
|
111
|
+
config.default_schema = ENV['PG_MULTITENANT_DEFAULT_SCHEMA'] || 'public'
|
|
112
|
+
|
|
113
|
+
# Environment-specific configuration
|
|
114
|
+
if Rails.env.development?
|
|
115
|
+
config.schema_prefix = 'dev_'
|
|
116
|
+
elsif Rails.env.test?
|
|
117
|
+
config.schema_prefix = 'test_'
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 🔄 Usage Patterns
|
|
122
|
+
|
|
123
|
+
### Basic Configuration
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# config/initializers/pg_multitenant_schemas.rb
|
|
127
|
+
PgMultitenantSchemas.configure do |config|
|
|
128
|
+
config.default_schema = 'public'
|
|
129
|
+
config.tenant_model = 'Organization'
|
|
130
|
+
config.connection_class = 'ApplicationRecord'
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Advanced Configuration
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# config/initializers/pg_multitenant_schemas.rb
|
|
138
|
+
PgMultitenantSchemas.configure do |config|
|
|
139
|
+
# Core settings
|
|
140
|
+
config.default_schema = 'shared'
|
|
141
|
+
config.tenant_model = 'Account'
|
|
142
|
+
config.tenant_schema_attribute = :slug
|
|
143
|
+
|
|
144
|
+
# Schema naming
|
|
145
|
+
config.schema_prefix = Rails.env.production? ? nil : "#{Rails.env}_"
|
|
146
|
+
|
|
147
|
+
# Connection management
|
|
148
|
+
config.connection_class = 'TenantRecord'
|
|
149
|
+
|
|
150
|
+
# Exclusions
|
|
151
|
+
config.excluded_schemas += ['analytics', 'logs']
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Environment-Specific Configuration
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
# config/initializers/pg_multitenant_schemas.rb
|
|
159
|
+
PgMultitenantSchemas.configure do |config|
|
|
160
|
+
case Rails.env
|
|
161
|
+
when 'development'
|
|
162
|
+
config.schema_prefix = 'dev_'
|
|
163
|
+
config.default_schema = 'dev_public'
|
|
164
|
+
when 'test'
|
|
165
|
+
config.schema_prefix = 'test_'
|
|
166
|
+
config.default_schema = 'test_public'
|
|
167
|
+
when 'staging'
|
|
168
|
+
config.schema_prefix = 'staging_'
|
|
169
|
+
config.default_schema = 'public'
|
|
170
|
+
when 'production'
|
|
171
|
+
config.schema_prefix = nil
|
|
172
|
+
config.default_schema = 'public'
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## 🎛️ Configuration Validation
|
|
178
|
+
|
|
179
|
+
The gem includes configuration validation to catch common issues:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
def validate_configuration!
|
|
183
|
+
raise ConfigurationError, "tenant_model must be defined" if tenant_model.blank?
|
|
184
|
+
raise ConfigurationError, "default_schema cannot be blank" if default_schema.blank?
|
|
185
|
+
|
|
186
|
+
# Validate tenant model exists
|
|
187
|
+
begin
|
|
188
|
+
tenant_model.constantize
|
|
189
|
+
rescue NameError
|
|
190
|
+
raise ConfigurationError, "Tenant model '#{tenant_model}' not found"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Validate connection class
|
|
194
|
+
begin
|
|
195
|
+
connection_class.constantize
|
|
196
|
+
rescue NameError
|
|
197
|
+
raise ConfigurationError, "Connection class '#{connection_class}' not found"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## 🔧 Integration Examples
|
|
203
|
+
|
|
204
|
+
### Custom Tenant Models
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
# For custom tenant model structure
|
|
208
|
+
class Organization < ApplicationRecord
|
|
209
|
+
has_many :users
|
|
210
|
+
|
|
211
|
+
def schema_name
|
|
212
|
+
"org_#{slug}"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Configuration
|
|
217
|
+
PgMultitenantSchemas.configure do |config|
|
|
218
|
+
config.tenant_model = 'Organization'
|
|
219
|
+
config.tenant_schema_attribute = :schema_name
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Multiple Database Setup
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
# For applications with multiple databases
|
|
227
|
+
class TenantRecord < ApplicationRecord
|
|
228
|
+
connects_to database: { writing: :tenant_primary, reading: :tenant_replica }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
PgMultitenantSchemas.configure do |config|
|
|
232
|
+
config.connection_class = 'TenantRecord'
|
|
233
|
+
end
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Custom Schema Naming
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
# Custom schema naming logic
|
|
240
|
+
module SchemaNameBuilder
|
|
241
|
+
def self.build_name(tenant)
|
|
242
|
+
case tenant.tier
|
|
243
|
+
when 'enterprise'
|
|
244
|
+
"ent_#{tenant.slug}"
|
|
245
|
+
when 'premium'
|
|
246
|
+
"prem_#{tenant.slug}"
|
|
247
|
+
else
|
|
248
|
+
"std_#{tenant.slug}"
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
PgMultitenantSchemas.configure do |config|
|
|
254
|
+
config.schema_name_builder = SchemaNameBuilder
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## 🚨 Important Considerations
|
|
259
|
+
|
|
260
|
+
### Configuration Timing
|
|
261
|
+
|
|
262
|
+
- Configure the gem **before** Rails application initialization
|
|
263
|
+
- Place configuration in `config/initializers/`
|
|
264
|
+
- Ensure configuration runs before model loading
|
|
265
|
+
|
|
266
|
+
### Thread Safety
|
|
267
|
+
|
|
268
|
+
- Configuration is **read-only** after initialization
|
|
269
|
+
- Safe to access from multiple threads
|
|
270
|
+
- No runtime configuration changes supported
|
|
271
|
+
|
|
272
|
+
### Performance Impact
|
|
273
|
+
|
|
274
|
+
- Configuration lookups are fast (cached)
|
|
275
|
+
- No database queries for configuration access
|
|
276
|
+
- Minimal memory overhead
|
|
277
|
+
|
|
278
|
+
## 🔍 Configuration Debugging
|
|
279
|
+
|
|
280
|
+
### Check Current Configuration
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
# Display current configuration
|
|
284
|
+
config = PgMultitenantSchemas.configuration
|
|
285
|
+
puts "Default Schema: #{config.default_schema}"
|
|
286
|
+
puts "Tenant Model: #{config.tenant_model}"
|
|
287
|
+
puts "Connection Class: #{config.connection_class}"
|
|
288
|
+
puts "Schema Prefix: #{config.schema_prefix}"
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Validate Configuration
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
# Validate configuration is correct
|
|
295
|
+
begin
|
|
296
|
+
PgMultitenantSchemas.configuration.validate_configuration!
|
|
297
|
+
puts "✅ Configuration is valid"
|
|
298
|
+
rescue PgMultitenantSchemas::ConfigurationError => e
|
|
299
|
+
puts "❌ Configuration error: #{e.message}"
|
|
300
|
+
end
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## 🔗 Related Components
|
|
304
|
+
|
|
305
|
+
- **[Context](context.md)**: Uses configuration for default schema
|
|
306
|
+
- **[SchemaSwitcher](schema_switcher.md)**: Uses connection class configuration
|
|
307
|
+
- **[TenantResolver](tenant_resolver.md)**: Uses tenant model configuration
|
|
308
|
+
- **[Rails Integration](rails_integration.md)**: Framework-specific configuration
|
|
309
|
+
|
|
310
|
+
## 📝 Configuration Templates
|
|
311
|
+
|
|
312
|
+
### Standard Rails App
|
|
313
|
+
```ruby
|
|
314
|
+
PgMultitenantSchemas.configure do |config|
|
|
315
|
+
config.default_schema = 'public'
|
|
316
|
+
config.tenant_model = 'Tenant'
|
|
317
|
+
config.connection_class = 'ApplicationRecord'
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### SaaS Application
|
|
322
|
+
```ruby
|
|
323
|
+
PgMultitenantSchemas.configure do |config|
|
|
324
|
+
config.default_schema = 'shared'
|
|
325
|
+
config.tenant_model = 'Account'
|
|
326
|
+
config.tenant_schema_attribute = :subdomain
|
|
327
|
+
config.schema_prefix = Rails.env.production? ? nil : "#{Rails.env}_"
|
|
328
|
+
end
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Enterprise Application
|
|
332
|
+
```ruby
|
|
333
|
+
PgMultitenantSchemas.configure do |config|
|
|
334
|
+
config.default_schema = 'master'
|
|
335
|
+
config.tenant_model = 'Organization'
|
|
336
|
+
config.tenant_schema_attribute = :schema_name
|
|
337
|
+
config.connection_class = 'TenantRecord'
|
|
338
|
+
config.excluded_schemas += ['audit', 'analytics', 'logs']
|
|
339
|
+
end
|
|
340
|
+
```
|
data/docs/context.md
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Context - Thread-Safe Tenant Management
|
|
2
|
+
|
|
3
|
+
**File**: `lib/pg_multitenant_schemas/context.rb`
|
|
4
|
+
|
|
5
|
+
## 📋 Overview
|
|
6
|
+
|
|
7
|
+
The `Context` class provides thread-safe tenant context management, maintaining current tenant and schema state across request lifecycle. It's the high-level interface for tenant switching and context management.
|
|
8
|
+
|
|
9
|
+
## 🎯 Purpose
|
|
10
|
+
|
|
11
|
+
- **Thread Safety**: Maintains separate tenant context per thread
|
|
12
|
+
- **Context Management**: Tracks current tenant and schema state
|
|
13
|
+
- **Automatic Restoration**: Ensures context is properly restored after operations
|
|
14
|
+
- **High-Level API**: Provides convenient methods for tenant switching
|
|
15
|
+
|
|
16
|
+
## 🔧 Key Methods
|
|
17
|
+
|
|
18
|
+
### Current State Management
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# Get/Set current tenant
|
|
22
|
+
Context.current_tenant
|
|
23
|
+
Context.current_tenant = tenant_object
|
|
24
|
+
|
|
25
|
+
# Get/Set current schema
|
|
26
|
+
Context.current_schema
|
|
27
|
+
Context.current_schema = 'schema_name'
|
|
28
|
+
|
|
29
|
+
# Reset to default state
|
|
30
|
+
Context.reset!
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Tenant Switching
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# Switch to specific tenant
|
|
37
|
+
Context.switch_to_tenant(tenant_object)
|
|
38
|
+
|
|
39
|
+
# Switch to specific schema
|
|
40
|
+
Context.switch_to_schema('schema_name')
|
|
41
|
+
|
|
42
|
+
# Execute block in tenant context
|
|
43
|
+
Context.with_tenant(tenant_or_schema) do
|
|
44
|
+
# Code executes in tenant context
|
|
45
|
+
User.all # Queries tenant's users
|
|
46
|
+
end
|
|
47
|
+
# Automatically restores previous context
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Schema Management
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# Create tenant schema
|
|
54
|
+
Context.create_tenant_schema(tenant_or_schema)
|
|
55
|
+
|
|
56
|
+
# Drop tenant schema
|
|
57
|
+
Context.drop_tenant_schema(tenant_or_schema, cascade: true)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 🏗️ Implementation Details
|
|
61
|
+
|
|
62
|
+
### Thread-Local Storage
|
|
63
|
+
|
|
64
|
+
Context uses Ruby's `Thread.current` to store tenant state:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
def current_tenant
|
|
68
|
+
Thread.current[:pg_multitenant_current_tenant]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def current_schema
|
|
72
|
+
Thread.current[:pg_multitenant_current_schema] ||
|
|
73
|
+
PgMultitenantSchemas.configuration.default_schema
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This ensures:
|
|
78
|
+
- **Isolation**: Each thread has independent tenant context
|
|
79
|
+
- **Concurrency**: Multiple requests can have different tenant contexts
|
|
80
|
+
- **Safety**: No cross-request tenant bleeding
|
|
81
|
+
|
|
82
|
+
### Context Restoration
|
|
83
|
+
|
|
84
|
+
The `with_tenant` method provides automatic context restoration:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
def with_tenant(tenant_or_schema)
|
|
88
|
+
# Store current state
|
|
89
|
+
previous_tenant = current_tenant
|
|
90
|
+
previous_schema = current_schema
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
# Switch to new context
|
|
94
|
+
switch_to_schema(schema_name)
|
|
95
|
+
self.current_tenant = tenant
|
|
96
|
+
yield if block_given?
|
|
97
|
+
ensure
|
|
98
|
+
# Always restore previous context
|
|
99
|
+
restore_previous_context(previous_tenant, previous_schema)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Flexible Input Handling
|
|
105
|
+
|
|
106
|
+
Context methods accept multiple input types:
|
|
107
|
+
|
|
108
|
+
- **Tenant Objects**: `tenant.subdomain` is used as schema name
|
|
109
|
+
- **String/Symbol**: Used directly as schema name
|
|
110
|
+
- **Nil**: Switches to default schema
|
|
111
|
+
|
|
112
|
+
## 🔄 Usage Patterns
|
|
113
|
+
|
|
114
|
+
### Basic Tenant Switching
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# Using tenant object
|
|
118
|
+
tenant = Tenant.find_by(subdomain: 'acme')
|
|
119
|
+
PgMultitenantSchemas::Context.switch_to_tenant(tenant)
|
|
120
|
+
|
|
121
|
+
# Using schema name directly
|
|
122
|
+
PgMultitenantSchemas::Context.switch_to_schema('acme_corp')
|
|
123
|
+
|
|
124
|
+
# Check current state
|
|
125
|
+
puts PgMultitenantSchemas::Context.current_schema # => "acme_corp"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Block-Based Context
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# Execute code in tenant context
|
|
132
|
+
PgMultitenantSchemas::Context.with_tenant('acme_corp') do
|
|
133
|
+
# All database operations happen in acme_corp schema
|
|
134
|
+
users = User.all
|
|
135
|
+
orders = Order.where(status: 'pending')
|
|
136
|
+
|
|
137
|
+
# Create new records in tenant schema
|
|
138
|
+
User.create!(email: 'user@acme.com')
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Context automatically restored to previous state
|
|
142
|
+
puts PgMultitenantSchemas::Context.current_schema # => "public"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Nested Contexts
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
# Nested tenant contexts work correctly
|
|
149
|
+
PgMultitenantSchemas::Context.with_tenant('tenant_a') do
|
|
150
|
+
puts "In tenant A: #{Context.current_schema}"
|
|
151
|
+
|
|
152
|
+
PgMultitenantSchemas::Context.with_tenant('tenant_b') do
|
|
153
|
+
puts "In tenant B: #{Context.current_schema}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
puts "Back in tenant A: #{Context.current_schema}"
|
|
157
|
+
end
|
|
158
|
+
puts "Back in original context: #{Context.current_schema}"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Error Handling
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
# Context is restored even if errors occur
|
|
165
|
+
PgMultitenantSchemas::Context.with_tenant('acme_corp') do
|
|
166
|
+
raise "Something went wrong!"
|
|
167
|
+
rescue => e
|
|
168
|
+
puts "Error: #{e.message}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Context is still properly restored
|
|
172
|
+
puts PgMultitenantSchemas::Context.current_schema # => "public"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## 🔗 Integration Points
|
|
176
|
+
|
|
177
|
+
### Controller Integration
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
class ApplicationController < ActionController::Base
|
|
181
|
+
around_action :set_tenant_context
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def set_tenant_context
|
|
186
|
+
tenant = resolve_tenant_from_request
|
|
187
|
+
PgMultitenantSchemas::Context.with_tenant(tenant) do
|
|
188
|
+
yield
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Background Jobs
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
class TenantJob < ApplicationJob
|
|
198
|
+
def perform(tenant_id, *args)
|
|
199
|
+
tenant = Tenant.find(tenant_id)
|
|
200
|
+
PgMultitenantSchemas::Context.with_tenant(tenant) do
|
|
201
|
+
# Job executes in tenant context
|
|
202
|
+
process_tenant_data
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Model Callbacks
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
class Order < ApplicationRecord
|
|
212
|
+
after_create :send_notification
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
def send_notification
|
|
217
|
+
# This runs in the same tenant context as the creation
|
|
218
|
+
current_tenant = PgMultitenantSchemas::Context.current_tenant
|
|
219
|
+
NotificationMailer.order_created(self, current_tenant).deliver_later
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## ⚙️ Configuration
|
|
225
|
+
|
|
226
|
+
Context behavior is influenced by global configuration:
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
PgMultitenantSchemas.configure do |config|
|
|
230
|
+
config.default_schema = 'public' # Default context
|
|
231
|
+
config.connection_class = 'ApplicationRecord'
|
|
232
|
+
end
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## 🚨 Important Considerations
|
|
236
|
+
|
|
237
|
+
### Thread Safety
|
|
238
|
+
|
|
239
|
+
- **Safe**: Each thread maintains independent context
|
|
240
|
+
- **Isolation**: No cross-thread context interference
|
|
241
|
+
- **Connection Pools**: Works correctly with Rails connection pooling
|
|
242
|
+
|
|
243
|
+
### Memory Management
|
|
244
|
+
|
|
245
|
+
- Context data is cleaned up when threads end
|
|
246
|
+
- No memory leaks from tenant context storage
|
|
247
|
+
- Minimal memory overhead per thread
|
|
248
|
+
|
|
249
|
+
### Performance
|
|
250
|
+
|
|
251
|
+
- Context switching is very fast
|
|
252
|
+
- No database queries required for context management
|
|
253
|
+
- Minimal CPU and memory overhead
|
|
254
|
+
|
|
255
|
+
## 🔍 Debugging
|
|
256
|
+
|
|
257
|
+
### Check Current Context
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
# Current tenant and schema
|
|
261
|
+
puts "Tenant: #{PgMultitenantSchemas::Context.current_tenant&.subdomain}"
|
|
262
|
+
puts "Schema: #{PgMultitenantSchemas::Context.current_schema}"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Debug Context Stack
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
# Add logging to track context changes
|
|
269
|
+
module PgMultitenantSchemas
|
|
270
|
+
class Context
|
|
271
|
+
class << self
|
|
272
|
+
alias_method :original_switch_to_schema, :switch_to_schema
|
|
273
|
+
|
|
274
|
+
def switch_to_schema(schema_name)
|
|
275
|
+
Rails.logger.debug "Switching to schema: #{schema_name}"
|
|
276
|
+
original_switch_to_schema(schema_name)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## 🔗 Related Components
|
|
284
|
+
|
|
285
|
+
- **[SchemaSwitcher](schema_switcher.md)**: Low-level schema operations used by Context
|
|
286
|
+
- **[TenantResolver](tenant_resolver.md)**: Identifies tenants for context switching
|
|
287
|
+
- **[Rails Integration](rails_integration.md)**: Framework integration using Context
|
|
288
|
+
- **[Configuration](configuration.md)**: Context configuration options
|
|
289
|
+
|
|
290
|
+
## 📝 Examples
|
|
291
|
+
|
|
292
|
+
See [examples/context_management.rb](../examples/) for complete usage examples.
|