Cheating on ActiveRecord with DataMapper

Avdi Grimm (@avdi)

New Feature!

  • We're integrating with Google Sheet!
  • …yeah, I'm gonna need you to have that done by Friday…
    google-sheet.png

Awww, CRUD

  1. Create
  2. Read
  3. Update
  4. Delete
  5. Profit!

Adapt and Survive

module DataMapper
  module Adapters
    class GoogleSheetAdapter < 
          DataMapper::Adapters::AbstractAdapter
      # ...
    end
  end  
end

Create    Incremental

def create(resources)
  # ...
end

Create - Group by table

def create(resources)
  table_groups = group_resources_by_table(resources)
  table_groups.each do |table, resources|
    # ...
  end
end

Create - Initialize serial fields

Initializing serial fields…

resources.each do |resource|
  initialize_serial(resource, 
                    worksheet_record_count(table) + 1)
  # ...
end

…makes this to work:

class User
  property :id, Serial
end

Create - post record

resources.each do |resource|
  # ...
  post_resource_to_worksheet(resource, table)
end

Create - return count

def create(resources)
  # ...
  resources.size
end

Create - done!

Now we can do this:

class LogEntry
  include DataMapper::Resource
  property :exercise, String
  property :reps, Integer
end
LogEntry.create(:exercise => "push-ups", :reps => 30)

Read    Incremental

def read(query)
  # ...
end

Read - fetch the table

def read(query)
  worksheet_name = query.model.storage_name(name)
  list = worksheet_as_list(worksheet_name)
  # ...
end

Read - transform data

DataMapper expects an Enumerable of {Property => value} hashes to be returned.

def read(query)
  # ...
  resource_hashes = list_to_hashes(list, query.fields)
  # ...
end

Read - almost done

We can already do this:

LogEntry.all # => all entries
LogEntry.first # => first entry in the table  

Read - filter results

def read(query)
  # ...
  query.filter_records(resource_hashes)
end

Read - done!

Now we can do this:

LogEntry.all(:reps.gt => 30, 
             :limit   => 10, 
             :order   => [:reps.desc])

Read - progressive enhancement

Optimize as-needed.

# ...
when GreaterThanComparison          then ">"
when LessThanComparison             then "<"
when GreaterThanOrEqualToComparison then ">="
# ...

Update    Incremental

def update(attributes, collection)
  # ...
end

Update - put updated records

def update(attributes, collection)
  each_resource_with_edit_url(collection) do 
    |resource, edit_url|

    put_updated_resource(edit_url, resource)
  end
  # ...
end

Update - return a count

def update(attributes, collection)
  # ...
  collection.size
end

Update - done!

Now we can do this:

log_entry.update(:comment => "increase weight")

Delete    Incremental

def delete(collection)
  # ...
end

Delete - delete records

def delete(collection)
  each_resource_with_edit_url(collection) do 
    |resource, edit_url|

    connection.delete(edit_url, 'If-Match' => "*")
  end
  # ...
end

Delete - return a count

def delete(collection)
  # ...
  collection.size
end

Delete - done!

Now we can do this:

log_entry.destroy
LogEntry.destroy # clear the table

Customize table naming

def field_naming_convention
  ->(property){
    property.name.to_s.gsub(/[^[:alnum:]]+/, '').downcase
  }
end

Create/delete tables

def create_model_storage(model)
  # ...
end

def upgrade_model_storage(model)
  # ...
end

def destroy_model_storage(model)
  # ...
end

Auto-migration

DataMapper.auto_migrate!
# or:
DataMapper.auto_upgrade!

Very handy for testing/development.

New feature!

  • SQL is the new NoSQL
  • Need all the Sheet data back in the DB
  • ASAP!

Copying records across repositories

DataMapper.setup(:other, 'sqlite::memory:')
DataMapper.repository(:other).auto_migrate!
LogEntry.copy(:default, :other)

Who cares about writing adapters?

DataMapper's killer app is that it lowers the bar for experimenting with different forms of data storage.

The Rosetta Stone of data

  • dm-mongo-adapter
  • dm-redis-adapter
  • dm-rdf-adapter
  • dm-imap-adapter
  • dm-adapter-simpledb
  • dm-tokyo-adapter
  • etc…

Friendly to adapter writers

  • Easy things easy, hard things possible
  • AST representation of queries
  • Incrementally add features, optimizations
  • Great documentation, readable code
  • Lots of conveniences
  • Plugins give you free functionality

Plugins

  • Validation
  • DB constraints
  • Pagination
  • State machine
  • Tagging
  • Aggregation
  • Versioning
  • Trees/lists/nested sets
  • etc…

About DataMapper

Based on the Data Mapper pattern poeaa1.jpg

A layer of Mappers that moves data between objects and a database while keeping them independent of each other and the mapper itself.

A Mature Library

  • In development since early 2008 (?)
  • Hit 1.0 in June
  • API is now stable

Everything's a lazy scope

entries = LogEntry.all
pushups = entries.all(:name => "push-ups")
squats  = entries.all(:name => "squats")
combined = pushups & squats
combined.to_a # NOW the datastore is queried

The Identity Map

ActiveRecord:

big_bird = Monster.create!(:name => "Big Bird")
snuffy   = Monster.create!(:name => "Snuffleupagus")
snuffy.friends << big_bird
snuffy.friends.first.name # => "Big Bird"
snuffy.friends.first.equal?(big_bird) # => false

The Identity Map

DataMapper:

big_bird = Monster.create(:name => "Big Bird")
snuffy   = Monster.create(:name => "Snuffleupagus")
snuffy.friends << big_bird
snuffy.friends.first.name # => "Big Bird"
snuffy.friends.first.equal?(big_bird) # => true

Natural-key friendly

class ZipCode
  property :code, String, :key => true
  property :state, String
end
# ...
ZipCode.get("17361")

Legacy-friendly

class LogEntry
  storage_names[:legacy] = 'tblEntry'
  # ...
end

Awesome community

#datamapper on Freenode

As much DataMapper as you want

  • Use alongside ActiveRecord
  • Or replace ActiveRecord entirely
    rails new project_name \
      -m http://datamapper.org/templates/rails.rb
    

Thank You!