-
Notifications
You must be signed in to change notification settings - Fork 87
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
When duping an object with translations, also dup the translations #84
Conversation
Thanks for the PR! You're right this is not a case that is currently handled, and the way you've done it using write/read makes sense. Regarding specs, there is a (backend-independent) duplication spec, I think it could go there first. In principle it shouldn't need to be tested against every backend since it's just built up from reads and writes, but need to think a bit more about that. To test against all backends, it would need to go in the shared examples (see |
I'll have a more careful look later today. |
Thinking about it some more, this handling is not necessarily needed. For backends where the translation is stored in the model itself (e.g., translatable columns), I don't think anything needs to be done, as the model's dup should take care of it. I don't think what I'm doing will do any harm, but I think the implications are worth thinking about. |
Yes I agree, which is why I need to think about it. I feel like it shouldn't be the default, maybe somehow as an option, but not sure how that would fit naturally. |
I thought about this, and in the end if something like this is going to be added, it would have to be a backend-specific thing (done in the setup do |attributes, options|
# ...
dup_method = Module.new do
define_method :initialize_dup do |other|
# ... do something special applied to attributes
super
end
end
include dup_method
end This way only backends where it's relevant to duplicate values would do it, and since it's backend-specific it could bypass I'm still not sure it makes sense, but that's the way I would probably envision doing it, if it seems important to have. |
I personally don't see the need for initialize_dup behaviour to be configurable by the user of the gem, as it makes things more complicated for little gain. Either the gem should handle it, or decide it is up to the consumer of the gem. The main reason I think this behaviour should be included in mobility is that one of the core goals of the gem is to provide a unified API regardless of the storage backend. However, without mobility handling dup, the API is not consistent across the backends. The argument against this is that dup normally creates a shallow copy of an object. I think this is part of the reason why However, the documentation of dup notes
So it isn't as though there's a strict rule against us handle duping in mobility. Given all of this, I'd favour adding the behaviour to the backends that don't handle |
d7fe154
to
38efb3f
Compare
After experimenting with this some more, it looks like the workaround proposed in the PR behaves a bit differently than anticipated in some circumstances. In my own application, I'm using the table backend, and working around it by adding the following to any models that use translation: def initialize_dup(source)
super
self.translations = source.translations.map(&:dup)
end So this further cements for me that we should fix this on a per backend basis. Can I update this PR to fix it for the table backend like the above, or are you still wanting to consider how to handle this? |
@pwim Please go ahead and fix for the table backend, that would be great. |
KeyValue backends require more work to get it working properly, so skip implementing for now.
38efb3f
to
1692696
Compare
I've rebased this PR, and implemented dup support for the ActiveRecord Table backend. All the other backends, except the KeyValue backends, seem to work automatically. Surprisingly, even the Sequel Table backend does, so perhaps they have some built in support. Using the same technique for the KeyValue backend didn't work. I guess because it is setting up multiple associations to the same table. So for now, I've left it unimplemented. |
Thanks! Will have a look on the weekend. |
self.send("#{association_name}=", source.send(association_name).map(&:dup)) | ||
end | ||
end | ||
include callback_methods |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is in general the right way to do this, the only thing I'd point out is that if you define your attributes separately, e.g.:
class Post < ApplicationRecord
extend Mobility
translates :title
translates :content
translates :author
end
Then this method will actually super
through the same code three times. This might seem a bit pedantic, but in Mobility I try to ensure that defining attributes separately or together essentially does the same thing, so try to avoid this type of thing (even where the result, like here, is the same, since assigning the same thing any number times does not change anything).
This code path can be avoided by defining a constant for the module which includes the (camel-cased) name of the association as well as the backend, and then including that:
module_name = "MobilityTable#{association_name.camelcase}"
unless const_defined?(module_name) do
callback_methods = Module.new do
# ...
end
include const_set(module_name, callback_methods)
end
This way, even if you call the code multiple times, you will only include the module once (unless the association name is different, in which case you should include it separately anyway.
There may be a simpler/better way to do this...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, your approach is a good way of doing it. Strictly speaking, I don't think you need the const_defined?
check, as including the same module multiple times won't do anything, but I guess it is safer to keep the check.
The other approach would be to have setup include a single module, that generates an initialize_dup method that handles associations. Something like
setup do |_attributes, options|
self.mobility_association_names << association_name
include InitializeDup
end
module InitializeDup
def initialize_dup(source)
super
self.class.mobility_association_names.each do |association_name|
self.send("#{association_name}=", source.send(association_name).map(&:dup))
end
end
end
I gather though you've designed mobility with avoiding adding such state to the classes it uses it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strictly speaking, I don't think you need the const_defined? check, as including the same module multiple times won't do anything, but I guess it is safer to keep the check.
Yes that's true, I just thought if it's already defined there's no point re-defining it - but agree better without since it saves a couple lines which only apply to an edge case anyway.
I gather though you've designed mobility with avoiding adding such state to the classes it uses it.
Yes exactly, the goal is to add as little to the model class as possible, and a name like mobility_association_names
would potentially conflict with other backends (which could be used in the same model).
It's overkill for 99% of current use-cases, but I want to leave the door open to use any combination of backends, applied any number of times, in the same model without fear that they would conflict with either each other in any way.
The tradeoff is that you have to define a constant on the model. I'm happy with any constant name as long as it includes the backend name and the associaiton name.
Ok, I went with your suggestion of defining a constant for the module. I was wrong to suggest that we could drop the check of whether it is defined as it will produce a |
Thanks for the great PR! Just merged it. |
I noticed mobility doesn't support duping translations like globalize does.
I took a stab at implementing it, but not sure if this is the right approach.
Also, it wasn't clear for me where to put the test. I'm assuming you want to test something like this against every backend. Where should I add real tests?