-
-
Save emmanuel/1116482 to your computer and use it in GitHub Desktop.
UoW code spike
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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