has_one from has_many: when an association can be fetched in more than one place.

Hi,
I came across a certain use case that could need some fine tuning. Let’s say I have a song Model. This song is available in many formats. So, the song has_many formats (mp3, flac, …). The song also has the original where the other might have been generated from. So, the song has_one original. This is my current state of affairs:
class Song < ActiveRecord::Base
has_many :formats
has_one :original, class_name: “Format”, conditions: { tag: “original” }
end
So far, so good. Problem is, now I have my original made available in two different places, and if I update one, it is not reproduced on the other, until I synchronize everything from the database (not to the database).
s = Song.first
s.formats.select{|f| f.tag == “original” } # original original
s.original #original original
s.original = Format.create(… tag: “original”) # changed original
s.formats.select{|f| f.tag == “original” } # original original (!!)
s.reload
s.formats.select{|f| f.tag == “original” } # changed original
And this sucks, specially when I start playing around with validations, I end up tucking more than I would like to.
So the question is: how well could one devise such an association which is contained somewhere else? Something like:
class Song < ActiveRecord::Base
has_many :formats
has_one :original, class_name: “Format”, conditions: { tag: “original” }, on: :formats

end

And from then on this “has one” would be a proxy to an instance contained in an has_many collection.

So, what would speak against such a feature? I’m all hears

If you want to reload the association cache you can pass true as argument.

s.formats(true).select { |f| f.tag == "original" }

Will always get what you want.

Thanks for the suggestion, that’s what I meant by tucking stuff. My point is, stuff is already somewhere right in memory, why couldn’t it be set right there? I’m exactly trying to avoid the DB roundtrips and the possible spaghetti code stemming from such a situation.

Terça-feira, 19 de Fevereiro de 2013 17:04:50 UTC+1, Rafael Mendonça França escreveu:

Because what is on memory is a cached version of you association. It is cached to avoid unnecessary queries on the database.

If you want to invalidate the cache you have to tell Rails to do it.

Yes, explicitly instead of implicitly. I agree with you, and the purpose here was not to discuss the role of the model layer in Rails. I wanted to discuss the possible implementation, and using your abstraction, of an association as a cached version of a cached version of a DB row state, in which the cached versions would communicate with one another through observation, instead of two cached versions of the same DB state which can theoretically update the DB state concurrently in an optimistic locking model in which the last update wins and we all lose.

Terça-feira, 19 de Fevereiro de 2013 17:18:09 UTC+1, Rafael Mendonça França escreveu:

I think if you use the :through option in the has_one association will do what you want.

You are looking at one particular case where the IdentityMap[1] pattern would be a solution. There are plenty of other cases, including reciprocal associations (which is currently partly addressed by the :inverse_of option on association). There was a Ruby Summer of Code project for doing an IdentityMap in Rails 2 or 3 years ago, but it turned out to be unworkable. I don’t know if that was just the particular implementation, or if there are more fundamental issues that made that approach incompatible.

[1] http://martinfowler.com/eaaCatalog/identityMap.html

@Rafael, nop, that does not work, and has nothing to do with what I’m trying to achieve. :through option checks for an association with that name in the refered :through association. Explaining with my example: :through tells me each format has an original. I’m saying that one of the formats is the original.

@John Susser, that’s actually quite interesting. I don’t know the stand of things concerning the IdentityMap implementation on ActiveRecord. Can you lead me to relevant documentation about the issue? (Assume that I’ll be googling about it as well :slight_smile: ).

Terça-feira, 19 de Fevereiro de 2013 17:59:23 UTC+1, Rafael Mendonça França escreveu: