Skip to content

Instantly share code, notes, and snippets.

@emmanuel
Forked from dkubb/test.rb
Created July 31, 2011 06:16
Show Gist options
  • Save emmanuel/1116482 to your computer and use it in GitHub Desktop.
Save emmanuel/1116482 to your computer and use it in GitHub Desktop.
UoW code spike
# NOTE: this is a code spike, and the following code will probably
# not make it into dm-core in it's current form. I wanted to see if it
# were possible to use a topographical sort to ensure parents were
# saved before children, before/after filters were fired in the correct
# order, and foreign keys set before children are saved, but after parents
# are saved.
# The hooks should fire in the following order:
# https://gist.github.com/6666d2818b14296a28ab
require 'tsort'
require 'rubygems'
require 'dm-core'
require 'dm-validations'
module DataMapper
class Session
attr_reader :before_hooks, :dependencies, :after_hooks
def self.scope
original = Thread.current[:dm_session]
session = Thread.current[:dm_session] ||= new
yield session
# commit in the outer-most block
session.commit if original.nil?
rescue Exception
session.rollback if original.nil?
raise
ensure
Thread.current[:dm_session] = original
end
def initialize(&block)
@before_hooks = CommandDependencies.new
@dependencies = CommandDependencies.new
@after_hooks = CommandDependencies.new
end
def valid?
dependencies.valid?
end
def <<(resource)
if command = resource.save_command
command.add_to_session(self)
else
# TODO: remove from the dependencies list
# - would need to remove *all* references from all commands
end
self
end
def concat(resources)
resources.each { |resource| self << resource }
self
end
def include?(resource)
dependencies.include?(resource.save_command)
end
def destroy(resource)
Destroy.new(resource).add_to_session(self)
self
end
def commit
before_hooks.call
dependencies.call
after_hooks.call
freeze
end
def rollback
# TODO: undo changes made in this session
dependencies.clear
freeze
end
private
class Command
attr_reader :resource, :parents
def initialize(resource)
@resource = resource
@parents = resource.send(:parent_associations).map do |parent|
parent.save_command
end
end
def valid?
resource.valid?
end
def call
raise NotImplementedError, "#{self.class}#call not implemented"
end
def ==(other)
kind_of?(other.class) && resource == other.resource
end
def eql?(other)
instance_of?(other.class) && resource.eql?(other.resource)
end
def hash
resource.object_id.hash
end
def add_to_session(session)
session.dependencies << self
end
end
module HookableCommand
def initialize(*)
super
@before_hooks = [ BeforeHook.new(resource, "before_#{command_name}_hook") ]
@after_hooks = [ AfterHook.new(resource, "after_#{command_name}_hook") ]
end
def add_to_session(session)
add_hook_dependencies(session, :before_hooks)
super
add_hook_dependencies(session, :after_hooks)
end
private
attr_reader :before_hooks, :after_hooks
def add_hook_dependencies(session, name)
hooks = send(name)
# make hooks dependent on the parent(s) hooks to ensure they
# are executed in the same order as the parent commands
parents.each do |parent|
hooks.each { |hook| hook.parents.concat(parent.send(name)) }
end
# add hooks to dependencies
session.send(name).concat(hooks)
end
def command_name
self.class.name.split('::').last.downcase
end
end
class Save < Command
include HookableCommand
def self.new(resource)
if equal?(Save)
klass = resource.new? ? Create : Update
klass.new(resource)
else
super
end
end
def initialize(*)
super
before_hooks.unshift BeforeHook.new(resource, 'before_save_hook')
after_hooks.push AfterHook.new(resource, 'after_save_hook')
end
def call
resource.persisted_state = resource.persisted_state.commit
end
end
class Create < Save
def add_to_session(session)
super
# make setting the FK dependent on saving the parent, and
# make the current command dependent on the FK being set
resource.send(:parent_relationships).each do |relationship|
parent = relationship.get!(resource)
foreign_key = SetForeignKey.new(resource, relationship)
foreign_key.parents << parent.save_command
parents << foreign_key
session.dependencies << foreign_key
end
end
end
class Update < Save; end
class Destroy < Command
include HookableCommand
def initialize(*)
super
@parents.clear # XXX: hack, reset what the parent class sets
end
def call
resource.persisted_state = resource.persisted_state.delete.commit
end
end
class SetForeignKey < Command
attr_reader :relationship
def initialize(resource, relationship)
super(resource)
@relationship = relationship
end
def call
resource.__send__("#{relationship.name}=", relationship.get(resource))
end
def ==(other)
super && relationship == other.relationship
end
def eql?(other)
super && relationship.eql?(other.relationship)
end
end
class Hook < Command
attr_reader :name
def initialize(resource, name)
super(resource)
@name = name
@parents.clear # XXX: hack, reset what the parent class sets
end
def call
resource.send(name)
true
end
def ==(other)
super && name == other.name
end
def eql?(other)
super && name.eql?(other.name)
end
end
class BeforeHook < Hook; end
class AfterHook < Hook; end
class CommandDependencies
include Enumerable, TSort
def initialize
@commands = []
@index_for = Hash.new do |hash, command|
hash[command] = commands.index(command)
end
end
def clear
@commands.clear
@index_for.clear
self
end
def <<(command)
commands << command
self
end
def concat(commands)
commands.each { |command| self << command }
self
end
def valid?
all? { |node| node.valid? }
end
def each
tsort_each { |node| yield node }
self
end
def call
all? { |node| node.call }
end
private
attr_reader :commands, :index_for
def tsort_each_node(&block)
commands.sort_by(&insertion_order).each(&block)
end
def tsort_each_child(node, &block)
# tsort places child nodes before parent nodes, yet this makes
# no sense from a UoW pov. Parents should always be saved first.
# I have no idea why this method in tsort is named this way.
if commands.include?(node) && node.respond_to?(:parents)
node.parents.sort_by(&insertion_order).each(&block)
end
end
def insertion_order
lambda { |command| index_for[command] || raise("XXX: DEBUG: unknown command #{command.inspect}") }
end
end
end
module Resource
def save
Session.scope do |session|
# only allow a resource to be saved once
return if session.include?(self)
# add parents to the UoW
parent_associations.each { |parent| parent.save }
# add resource to the UoW
session << self
# add children to the UoW
child_associations.flatten.each { |child| child.save }
end
end
def destroy
Session.scope { |session| session.destroy(self) }
end
def save_command
Session::Save.new(self)
end
end
end
DataMapper::Logger.new($stdout, :debug)
DataMapper.setup(:default, 'sqlite3::memory:')
class Person
include DataMapper::Resource
property :id, Serial
property :name, String, :length => 1..50, :required => true, :unique => true, :unique_index => true
belongs_to :parent, self, :required => false
has n, :children, self, :inverse => :parent
before(:save) { puts "Before Saving #{name}" }
after(:save) { puts "After Saving #{name}" }
before(:create) { puts "Before Creating #{name}" }
after(:create) { puts "After Creating #{name}" }
before(:update) { puts "Before Updating #{name}" }
after(:update) { puts "After Updating #{name}" }
before(:destroy) { puts "Before Destroying #{name}" }
after(:destroy) { puts "After Destroying #{name}" }
end
DataMapper.auto_migrate!
parent = Person.new(:name => 'Dan Kubb')
parent.children.new(:name => 'Alex Kubb')
parent.children.new(:name => 'Katie Kubb')
puts '-' * 80
parent.save
puts '-' * 80
parent.attributes = { :name => 'Barbara-Ann Kubb' }
parent.children(:name => 'Alex Kubb').each { |child| child.name = 'Alexander Kubb' }
parent.children(:name => 'Katie Kubb').each { |child| child.name = 'Katherine Kubb' }
parent.save
puts '-' * 80
parent.children.destroy
parent.destroy
__END__
OUTPUT:
~ (0.000151) SELECT sqlite_version(*)
~ (0.000179) DROP TABLE IF EXISTS "people"
~ (0.000025) PRAGMA table_info("people")
~ (0.000457) CREATE TABLE "people" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "name" VARCHAR(50) NOT NULL, "parent_id" INTEGER)
~ (0.000146) CREATE INDEX "index_people_parent" ON "people" ("parent_id")
~ (0.000128) CREATE UNIQUE INDEX "unique_people_name" ON "people" ("name")
--------------------------------------------------------------------------------
Before Saving Dan Kubb
Before Creating Dan Kubb
Before Saving Alex Kubb
Before Creating Alex Kubb
Before Saving Katie Kubb
Before Creating Katie Kubb
~ (0.000107) INSERT INTO "people" ("name") VALUES ('Dan Kubb')
~ (0.000063) INSERT INTO "people" ("name", "parent_id") VALUES ('Alex Kubb', 1)
~ (0.000055) INSERT INTO "people" ("name", "parent_id") VALUES ('Katie Kubb', 1)
After Creating Dan Kubb
After Saving Dan Kubb
After Creating Alex Kubb
After Saving Alex Kubb
After Creating Katie Kubb
After Saving Katie Kubb
--------------------------------------------------------------------------------
Before Saving Barbara-Ann Kubb
Before Updating Barbara-Ann Kubb
Before Saving Alexander Kubb
Before Updating Alexander Kubb
Before Saving Katherine Kubb
Before Updating Katherine Kubb
~ (0.000123) UPDATE "people" SET "name" = 'Barbara-Ann Kubb' WHERE "id" = 1
~ (0.000054) UPDATE "people" SET "name" = 'Alexander Kubb' WHERE "id" = 2
~ (0.000052) UPDATE "people" SET "name" = 'Katherine Kubb' WHERE "id" = 3
After Updating Barbara-Ann Kubb
After Saving Barbara-Ann Kubb
After Updating Alexander Kubb
After Saving Alexander Kubb
After Updating Katherine Kubb
After Saving Katherine Kubb
--------------------------------------------------------------------------------
Before Destroying Barbara-Ann Kubb
~ (0.000064) DELETE FROM "people" WHERE "id" = 1
After Destroying Barbara-Ann Kubb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment