Reputation: 1783
I have a multi-tenant rails application, which allows clients to use their own custom TLDs. So I can have:
www.clientA.com
www.clientB.com
www.clientC.com
etc....
For better or worse, my database (postgres) has a tenants table, which has approximately 60 columns with various settings & configurations for each tenant. Some are simply flags, and some are large text values.
In application_controller.rb
I have some logic to parse the URL, query the tenants table based on the domain and instantiate a @current_tenant
object. That @current_tenant
object is available throughout the lifecycle of the page. This happens on every. single. page. of my app.
While I have some heavy caching in place, this design still feels very wrong to me, and I am sure it can be improved.
Is there a best practice for this? What is the best pattern to handle the @current_tenant object? I am concerned about memory management.
Upvotes: 0
Views: 331
Reputation: 2534
If you don't mind adding extra gems to your application, I would recommend using apartment. This was done exactly to serve your purpose : handle rails application with multiple tenants.
This would present two advantages to your problem:
handling @current_tenant
(or whatever you wish to name it) is done through a middleware, so you won't need to set it in your ApplicationController
. Take a look at the Switch on domain section of the README
to see how it is done. (Note: apartment
uses Apartment.current_tenant
to refer to your @current_tenant
.
The best part: in most case, you will not need @current_tenant
anymore since apartment
will scope all requests on the appropriate postgresql schema.
Upvotes: 1
Reputation: 9455
your current_tenant
is no different from current_user
that is ubiquitous in Rails apps.
stop microoptimizing. unless you benchmarked it and it shows to be a major bottleneck (which it is not, I can assure you).
any minuscule performance improvement (if at all) will be offset by the increased complexity of code, caching problems and what not.
do. not. do. that. ;)
one thing though, do NOT do a before_filter that assigns @current_tenant
, instead add a current_tenant
method to the application_controller
(or one of its concerns) that will cache the result for the remainder of the request:
def current_tenant
@current_tenant ||= ....
end
Upvotes: 2
Reputation: 1848
I designed an application that is doing basically exactly what you have described. Since you are loading a single object (even if on every page request), just make sure the query only returns the one row (current tenant) and doesn't do a crazy amount of joins. A single row query with a LIMIT applied to it is not going to bring down your site, even if requested hundreds of times a second. And regardless, if you are getting that type of traffic, you will have to scale your server anyways.
One thing that can be done to help, is to make sure your search column is indexed in the database. If you are finding the current tenant by url, index the url column.
Here is an example of what I did. I globalized the variable so this information is available in all controllers/models/views.
In my application controller:
before_filter :allocate_site
private
def allocate_site
url = request.host_with_port
url.slice! "www."
# load the current site by URL
$current_site = Site.find_by({:url => url, :deleted => false})
# if the current site doesn't exist, we are going to create a placeholder
# for the URL hitting the server.
if $current_site.nil?
$current_site = Site.new
$current_site.name = 'New Site'
$current_site.url = url
$current_site.save
end
end
Upvotes: 1
Reputation: 78443
(Note: I haven't touched Rails for 3 years, so take this answer with the appropriate fistful of salt.)
Is there a best practice for this? What is the best pattern to handle the @current_tenant object?
The best way I've seen this kind of stuff implemented is in PHP, rather than Ruby on Rails. More specifically, in the Symfony framework.
In a nutshell, a Symfony app's layout is like so:
/app <-- app-specific Kernel, Console, config, cache, logs, etc.
/src <-- app-specific source files
/vendor <-- vendored source files
/web <-- public web folder for the app
To run multiple apps from the same code base, you'd basically go:
/app
/app2
/...
/src
/vendor
/web
/web2
/...
... and then point each domain to a different /web folder.
As I understand, it's possible to re-organize a Rail project's directory structure to something more or less equivalent. See this related answer in particular:
https://stackoverflow.com/a/10480207/417194
From there, you could theoretically boot up one instance of your app per tenant, each with their separate resources, hard-coded defaults, etc., while continuing to use a shared code base.
The next best option is, imo, what you're currently doing. Use memcached or equivalent to quickly map specific domain names to tenants, and fall back to a database query as necessary. I imagine you're doing this already, since you've "some heavy caching in place".
As an aside, you might find this related question, and the ones linked within it, interesting:
Multiple applications using a single code base in ruby
(And FWIW, I ended up not sticking with PHP for multi-tenant apps. There was no magic bullet last I played with Ruby or Rails.)
With respect to memory usage, methinks don't worry too much about it: strictly speaking, you're looking up and creating the tenant object a single time per request. Even if doing so was dreadfully slow, cursory monitoring of where your app is actually spending time will reveal that it's negligible compared to innocuous looking pieces of code that get run a gazillion times per request.
Upvotes: 1
Reputation: 526
You could also cache the @current_tenant
object using Rails cache, ie:
def current_tenant( host = request.host )
Rails.cache.fetch(host, expires_in: 5.minutes) do
Tenant.find_by(tld: host)
end
end
Upvotes: 1