jgaskins / perpetuity

Persistence gem for Ruby objects using the Data Mapper pattern
http://jgaskins.org/blog/2012/04/20/data-mapper-vs-active-record/
MIT License
250 stars 14 forks source link

Allow mapping object attributes to different DB fields #15

Open jgaskins opened 11 years ago

jgaskins commented 11 years ago

In document databases like MongoDB, it's common practice to abbreviate keys in objects to reduce the size of its footprint. We can support that without forcing users to abbreviate attribute names by offering an option to map attributes to differently named database fields/keys. Example:

class ArticleMapper < Perpetuity::Mapper
  map Article

  attribute :title, maps_to: 't'
  attribute :body, maps_to: 'b'
  attribute :comments, maps_to: 'c'
end
jgaskins commented 11 years ago

I'm not 100% set on the use of :maps_to as a parameter name. I'm sure there's something better we can use. I just used it for illustrative purposes.

acook commented 10 years ago

Also when dealing with legacy/preexisting datastores this comes in handy.

Ruby already has a mapping syntax, maybe we should use it?

class ArticleMapper < Perpetuity::Mapper
  map Article

  attribute title: 't'
  attribute body: 'b'
  attribute comments: 'c'
end
jgaskins commented 10 years ago

I think I get too verbose with my explanations sometimes, so TL;DR: I'd much prefer to keep the attribute name on its own as the first positional argument.

The unabridged version follows:

I like the idea, but I don't like the implications. :-\ It changes the first argument from a symbol to a hash, which means that both the attribute name argument and the options hash become mashed together. This, in turn, means that the options hash can be either the first or second argument, which isn't difficult to make work, but allowing positional arguments to be different things is super confusing from an outside perspective.

For example, let's say you're using existing data that was on ActiveRecord before, but you've changed one argument from created_at to timestamp because it works better for your domain (or whatever). Among the rest of the attributes, it looks like this:

Perpetuity.generate_mapper_for Article do
  attribute :title, type: String
  attribute :body, type: String
  attribute timestamp: 'created_at', type: Time
end

This disturbs the aesthetics a bit — suddenly there's no colon there at the beginning of one of the attributes. Also, someone who isn't familiar would probably be both surprised and confused about what's going on, whereas something like attribute :timestamp, type: Time, stored_as: :created_at tells them a lot more.

acook commented 10 years ago

It's actually worse because then you can end up with namespace collisions between option keys and attribute names.

Given that, I like map_to. Its concise and intuitive.

jgaskins commented 10 years ago

Another good point.

acook commented 10 years ago

Representable/roar and ActiveRecord's polymorphic associations both use as for name mapping, so perhaps keeping with that semantic would be more intuitive for new users.

class ArticleMapper < Perpetuity::Mapper
  map Article

  attribute title, as: 'name'
  attribute body, as: 'content'
  attribute comments, as: 'responses'
end

... vs ...

class ArticleMapper < Perpetuity::Mapper
  map Article

  attribute title, map_to: 'name'
  attribute body, map_to: 'content'
  attribute comments, map_to: 'responses'
end

Another downside ofmap_to or maps_to is potential confusion with Enumerable#map and friends.

Apologies not just being declarative with my original opinion and defending it to the death :wink:, but as I get new data/ponder I do alter my model based on the result.

dkubb commented 10 years ago

@acook coming in late to this risks sounding like an armchair interface designer, but in my opinion "as" sounds more like assignment to something. So in this case I would expect that you're mapping title to name, and Article#name would hold whatever value the "title" is in the database. Based on the example you linked I believe it has the same semantics in roar.

My assumption is that the syntax is meant to define an attribute in the Article object named title. I don't have any really good suggestions for the option name. In DM1 we used :field, which I guess is fine since I didn't see any confusion.

jgaskins commented 10 years ago

My assumption is that the syntax is meant to define an attribute in the Article object named title.

Indeed. The first argument will be what it is on the object. The optional argument will be how it's stored in the DB.

I mentioned :stored_as as an ad-lib example above and I'm starting to dig it. It may end up as either that or :map_to unless someone has a better name. Any good naming ideas I've had on this project have been pure luck. :-)