-
-
Save stevenharman/1457584 to your computer and use it in GitHub Desktop.
| # This is a record, as in a vinyl. | |
| class Record < ActiveRecord::Base | |
| belongs_to :user | |
| belongs_to :album | |
| # explicitly stating what is needed | |
| def from_library_for_album(collector, album) | |
| where(user_id: collector).where(album_id: album) | |
| end | |
| # or, if we go more generic/idiomatic | |
| def from_library(collector, options={}) | |
| album = options.fetch(:album) { false } | |
| status = options.fetch(:status) { :mint } | |
| records = where(user_id: collector) | |
| records = records.where(album_id: album) if album | |
| records = records.where(status: status) if status | |
| records | |
| end | |
| end | |
| class Album < ActiveRecord::Base | |
| has_many :records | |
| belongs_to :artist | |
| end | |
| class User < ActiveRecord::Base | |
| has_many :records | |
| end | |
| # I prefer to keep AR as a private interface used only w/in the Model. | |
| class Library | |
| def initialize(collector, fetch_records=Record) | |
| @collector = collector | |
| @fetch_records = fetch_records | |
| end | |
| def fetch_records_for_album(album) | |
| fetch_records.from_library_for_album(collector, album) | |
| # ... or ... | |
| fetch_records.from_library(collector, album: album) | |
| # and what about more params? | |
| fetch_records.from_library_for_album_in_mint_condition(collector, album) | |
| # ... or ... | |
| fetch_records.from_library(collector, album: album, condition: :mint) | |
| end | |
| end | |
| class LibrariesController < ApplicationController | |
| def album | |
| library = Library.new(current_user) | |
| @album = Album.find(params[:id]) | |
| @records = library.fetch_records_for_album(@album) | |
| end | |
| end |
Yeah, the real problem is that AR is kinda broken by design. Yet so many things are written to work with it, going with a lesser-used ORM can be rough.
@steveklabnik,
Agreed. For this particular app, I really considered going with a Data Mapper approach, but in the end the thought of all of the friction scared me into sticking with AR. Perhaps, in hindsight, the friction I'm feeling in my design w/AR is MORE than would have been felt by going with a different ORM.
Fucking software, it's hard, man.
Fucking software, it's hard, man.
Word. Datamapper is okay, but the Ruby implementation tries to pretend it's AR too much, it has the same problems.
I don't have the time and energy to write a good ORM...
I feel like the DCI pattern could help you here... similar to your last code posting. (As we just discussed offline, but for posterity.)
Let data objects be primarily data objects. AR does an OK job with that anyway. That's the D.
The C would be your use case (I like the term activity better, but anyway). C is for context.
I are your interactions that you extend onto AR's domain/data objects at runtime inside your context.
The DCI pattern puts contextual analysis over the domain modelling desire "single source of truth," so I understand it's a different motivation but there might be more value in that POV?
There's some great discussion here, wish I could make it to CodeMash this year to partake in the booze filled continuation.
I agree AR definitely leaves a lot to be desired when it comes to separation of concerns. It's true that if you follow the idiomatic rules, things can remain quite testable purely because of the powerful stubbing capabilities in Ruby and RSpec. You don't have to wrap everything in a testable abstraction like we were all used to doing with C#.
But there are definitely things that still feel weird with AR and don't work quite as well if you try to build any abstraction layers around it. I'd consider Sequel as a possible alternative to using an ORM, although its really raw by comparison.
Or just try using MongoDB and get out of relational mapping all together. For side projects, I think its probably the best way to go.
@leongersing
As always, I much appreciate the thoughtful feedback! Overall I agree that it feels like a lot of indirection for little benefit, and certainly a broken, if not entirely missing, abstraction.
Re: the
Library- My intent was to use it to centralize the concept of exactly whatArtists,Albums, and individualRecordsa givenUser(collector) has. That is, I wanted there to be single, and obvious way to query his/her library. The desire my just be a knee jerk, and inappropriate, reaction to seeing this kind of querying sprinkled, and repeated, all over a code base. Take for instance the point you raised about going directly to theAlbuminstance to fetch it's records:That isn't exactly what we're after; that would return ALL instances of that album, across all collectors. Instead, we're after something a bit more complicated -
@album.records.collected_by(@current_user). If we roll with that, the domain concept of the actual records user has in his library is muddied; there is no one source of the truth. Does that make sense?In that vein, you mentioned named_scopes may be a cleaner and more idiomatic way to attack this issue. You're probably right and in looking at my real domain (which looks like the following), I think adding some more named scopes, and judiciously combining them across models via
Relation#mergewould go along way.UPDATE: I'm using a bit of my real domain to help illustrate, so
Beeris a moral equivalent ofRecordfrom the example above.The
Beer.from_cellarcould change toOr something, I suppose. Maybe I need more ☕ or 🍺 before free-wheeling something like this. At any rate, we'll get into it more in a couple of weeks at Codemash! Thanks again, buddy!