New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Multitenancy #17
Comments
This allows others to modify the behaviour, whereas if it's a proc, such changes aren't easily done. See #17.
Related to #17, this is so if others modify the behaviour of the tag model, they can change the validations that are in play. It's worth noting that adding a custom tag_validations class/object/proc (whatever it is, it must respond to call and take the tag model as an argument) will only work in Rails, due to the lazy loading of models. In non-Rails apps, the model will be loaded with the rest of the gem, and changing tag_validations will have no impact. I'm open to suggestions on how to better manage this.
Hey Andy Been mulling over this on and off, and finally got a chance to do something about it. I've not taken your code wholesale into the gem - am still loath to have official support for multi-tenancy in Gutentag itself - but there are two changes which should help your modifications: Via 0cbc601 the after_save callback in a taggable model has changed from a Proc to a private method ( Via 51bd8f2 you can customise the validations being added to the Gutentag.tag_validations = lambda { |klass|
klass.validates :name, presence: true,
uniqueness: {case_sensitive: false, scope: :tenant_id}
} That second change is only Rails-friendly, due to Rails' lazy loading of models. It will not work with Sinatra or other frameworks (though I'm certainly welcome to feedback on how it could be better structured to do so). These should help clean up your code a bit… are there other aspects that you feel could be managed better to allow your modifications to happen more cleanly? |
Hey Pat, Those two changes greatly clean up my code – thanks! I was able to delete 36 lines of ugly customisation :) The only other modification I wonder about is how best to add scopes to Gutentag::Tag.class_eval do
scope :by_tenant_id, ->(tenant_id) { where tenant_id: tenant_id }
end Would you do it this way or another way? |
Gutentag::Tag.scope :by_tenant_id, ->(tenant_id) { Gutentag::Tag.where tenant_id: tenant_id } The catch with that is that I'm not sure it would work appropriately if chained on top of another scope. Not the end of the world, but given it then becomes less obvious/normal, I'd probably go with the class_eval call instead. At this point, I can't think of any way to abstract this behaviour out neatly so it's easier to inject. |
That's all good to know, thanks. I can't see any further way to simplify my multitenancy changes. For the sake of completeness, here they are: Gutentag.tag_validations = lambda { |klass|
klass.validates :name,
presence: true,
uniqueness: {case_sensitive: false, scope: :tenant_id}
}
Gutentag::Tag.class_eval do
attr_accessible :tenant_id if ActiveRecord::VERSION::MAJOR == 3
scope :by_tenant_id, ->(tenant_id) { where tenant_id: tenant_id }
def self.find_by_name_and_tenant_id(name, tenant_id)
where(tenant_id: tenant_id).find_by_name(name)
end
def self.find_or_create_by_name_and_tenant_id(name, tenant_id)
find_by_name_and_tenant_id(name, tenant_id) || create(name: name, tenant_id: tenant_id)
end
end
module Taggable
def self.included(base)
base.has_many_tags
end
def tenant_id
raise NotImplementedError, 'taggable must implement'
end
private
# Override Gutentag::ActiveRecord#persist_tags so I can use a `tenant_id`-aware tagger.
def persist_tags
persister = Gutentag::Persistence.new(self)
persister.tagger = TenantTagger.new(tenant_id)
persister.persist
end
class TenantTagger
def initialize(tenant_id)
@tenant_id = tenant_id
end
def find_by_name(name)
Gutentag::Tag.find_by_name_and_tenant_id(name, tenant_id)
end
def find_or_create(name)
Gutentag::Tag.find_or_create_by_name_and_tenant_id(name, tenant_id)
end
private
attr_reader :tenant_id
end
end Thanks for all the help! |
I extracted this into a gem: gutentag-multitenancy. Thanks again for the help! |
Great stuff! :) |
Hi Pat,
I have patched Gutentag to support multitenancy, i.e. a self-contained set of tags for each account in my Rails app, and I thought you might like to see how. It's been running happily in production for a few weeks.
First I added a scoping column to
Gutentag::Tag
which I called:tenant_id
. This involved modifying the migration:Then I patched the class to take
:tenant_id
into account:The only tricky part was updating the uniqueness validator to scope by
:tenant_id
.Then I needed to tell
Gutentag::Persistence
to use a custom tagger which is aware of a tenant's scope. Unfortunately this was also a little tricky because it's only ever instantiated inside theafter_save
callback set up by thehas_many_tags
method – which is difficult to reach inside.I created a module which a taggable model can include instead of calling
has_many_tags
:Finally, here is the custom tagger:
Client code can use it like this:
Overall I was pleased by how little I had to change conceptually. The implementation was a little messy because it isn't straightforward to modify validators or after-save callbacks.
I know you don't necessarily want to support multitenancy, but I wonder whether we could rearrange Gutentag's code at all, wihtout changing its function, to make extending for multitenancy easier?
Thanks for such a cleanly and clearly written gem!
Cheers,
Andy
Cross-ref: #9.
The text was updated successfully, but these errors were encountered: