Last active
June 5, 2019 22:47
-
-
Save sergueif/9b428de5cab059a27ca0392dc9a0e091 to your computer and use it in GitHub Desktop.
Value persistence across active records with lazy-loading
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
class CreateValueRecords < ActiveRecord::Migration[5.1] | |
def change | |
create_table :value_records, id: false do |t| | |
t.jsonb :body, null: false, default: {} | |
t.string :key | |
t.timestamps | |
end | |
execute "ALTER TABLE value_records ADD PRIMARY KEY (key);" | |
end | |
end |
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
class TestCompressedTeam | |
def initialize(name, players=[]) | |
@name = name | |
@players = players | |
end | |
attr_reader :name, :players | |
def ==(o) | |
o.class == self.class && o.state == state | |
end | |
def compress | |
TestCompressedTeam.new( | |
@name, | |
@players.map{|p| @exchange.check_in(p) } | |
) | |
end | |
def distribute_exchange(exchange) | |
@players.each{|p| p.distribute_exchange(exchange) } | |
@exchange = exchange | |
end | |
protected | |
def state | |
[@name, @players] | |
end | |
end |
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
class TestPlayer | |
def initialize(name) | |
@name = name | |
end | |
attr_reader :name | |
def distribute_exchange(exchange) | |
@exchange = exchange | |
end | |
def ==(o) | |
o.class == self.class && o.state == state | |
end | |
protected | |
def state | |
[@name] | |
end | |
end |
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
class TestPlayer | |
def initialize(name) | |
@name = name | |
end | |
attr_reader :name | |
def ==(o) | |
o.class == self.class && o.state == state | |
end | |
protected | |
def state | |
[@name] | |
end | |
end |
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
class TestTeam | |
def initialize(name, players=[]) | |
@name = name | |
@players = players | |
end | |
attr_reader :name, :players | |
def ==(o) | |
o.class == self.class && o.state == state | |
end | |
def distribute_exchange(exchange) | |
@players.each{|p| p.distribute_exchange(exchange) } | |
@exchange = exchange | |
end | |
protected | |
def state | |
[@name, @players] | |
end | |
end |
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
class TestTeam | |
def initialize(name, players=[]) | |
@name = name | |
@players = players | |
end | |
attr_reader :name, :players | |
def ==(o) | |
o.class == self.class && o.state == state | |
end | |
protected | |
def state | |
[@name, @players] | |
end | |
end |
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
require 'digest' | |
class ValueRecord < ApplicationRecord | |
class LookupFailedException < Exception | |
end | |
class ValuePointer | |
def initialize(key) | |
@key = key | |
end | |
def distribute_exchange(exchange) | |
@exchange = exchange | |
end | |
def method_missing(meth, *args, &block) | |
@exchange.check_out(@key).send(meth, *args, &block) | |
end | |
end | |
def check_out(key) | |
@cache ||= {} | |
return @cache.fetch(key) if @cache.has_key?(key) | |
vr = ValueRecord.find(key) | |
if vr | |
return vr.value | |
else | |
raise LookupFailedException.new("unable to find key #{key}") | |
end | |
end | |
def check_in(val) | |
vr = ValueRecord.from_value(val) | |
vr.put! | |
ValuePointer.new(vr.key) | |
end | |
def compress | |
v_compressed = value.compress | |
ValueRecord.from_value(v_compressed) | |
end | |
def self.from_value(v) | |
serialized = Oj::dump(v, ignore: [ValueRecord]) | |
ValueRecord.new( | |
key: Digest::SHA256.base64digest(serialized), | |
body: JSON.parse(serialized) | |
) | |
end | |
def value | |
v = Oj::load(body.to_json) | |
v.distribute_exchange(self) | |
v | |
end | |
def put! | |
save! | |
rescue ActiveRecord::RecordNotUnique => e | |
if e.message.include? "PG::UniqueViolation: ERROR: duplicate key value violates unique constraint \"value_records_pkey\"" | |
return | |
else | |
raise e | |
end | |
end | |
end |
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
require 'rails_helper' | |
require_relative '../fixtures/test_team.rb' | |
RSpec.describe ValueRecord, type: :model do | |
it "can be made to and from a PORO Value" do | |
v = TestTeam.new("Blue Team") | |
vr = ValueRecord.from_value(v) | |
vr.save! | |
vr_again = ValueRecord.find(vr.key) | |
expect(vr_again.value).to eq(v) | |
end | |
end |
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
require 'rails_helper' | |
require_relative '../fixtures/test_player.rb' | |
require_relative '../fixtures/test_compressed_team.rb' | |
RSpec.describe ValueRecord, type: :model do | |
it "can compress values to value pointers" do | |
2.times do | |
name1 = "Player 1"*1000 | |
name2 = "Player 2"*1000 | |
v = TestCompressedTeam.new( | |
"Blue Team", | |
[ | |
TestPlayer.new(name1), | |
TestPlayer.new(name2) | |
]) | |
vr = ValueRecord.from_value(v) | |
vr.put! | |
vr_again = ValueRecord.find(vr.key) | |
vr_body_size = vr.body.to_json.size | |
vr_compressed = vr_again.compress | |
vr_compressed = vr_compressed.compress #test it's idempotent | |
vr_compressed.put! | |
vr_again = ValueRecord.find(vr_compressed.key) | |
vr_again_body_size = vr_again.body.to_json.size | |
v = vr_again.value | |
expect(v.name).to eq("Blue Team") | |
expect(v.players[0].name).to eq(name1) | |
expect(v.players[1].name).to eq(name2) | |
expect(vr_again_body_size).to be < vr_body_size | |
end | |
end | |
end |
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
RSpec.describe ValueRecord, type: :model do | |
it "can dereference value pointers values" do | |
p2 = TestPlayer.new("Player 2") | |
vr2 = ValueRecord.from_value(p2) | |
vr2.save! | |
v = TestTeam.new( | |
"Blue Team", | |
[ | |
TestPlayer.new("Player 1"), | |
ValueRecord::ValuePointer.new(vr2.key) | |
]) | |
vr = ValueRecord.from_value(v) | |
vr.save! | |
vr_again = ValueRecord.find(vr.key) | |
v = vr_again.value | |
expect(v.name).to eq("Blue Team") | |
expect(v.players[0].name).to eq("Player 1") | |
expect(v.players[1].name).to eq("Player 2") | |
end | |
end |
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
require 'digest' | |
class ValueRecord < ApplicationRecord | |
def self.from_value(v) | |
serialized = Oj::dump(v, ignore: [ValueRecord]) | |
ValueRecord.new( | |
key: Digest::SHA256.base64digest(serialized), | |
body: JSON.parse(serialized) | |
) | |
end | |
def value | |
Oj::load(body.to_json) | |
end | |
end |
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
require 'digest' | |
class ValueRecord < ApplicationRecord | |
class LookupFailedException < Exception | |
end | |
class ValuePointer | |
def initialize(key) | |
@key = key | |
end | |
def distribute_exchange(exchange) | |
@exchange = exchange | |
end | |
def method_missing(meth, *args, &block) | |
@exchange.check_out(@key).send(meth, *args, &block) | |
end | |
end | |
def check_out(key) | |
@cache ||= {} | |
return @cache.fetch(key) if @cache.has_key?(key) | |
vr = ValueRecord.find(key) | |
if vr | |
return vr.value | |
else | |
raise LookupFailedException.new("unable to find key #{key}") | |
end | |
end | |
def self.from_value(v) | |
serialized = Oj::dump(v, ignore: [ValueRecord]) | |
ValueRecord.new( | |
key: Digest::SHA256.base64digest(serialized), | |
body: JSON.parse(serialized) | |
) | |
end | |
def value | |
v = Oj::load(body.to_json) | |
v.distribute_exchange(self) | |
v | |
end | |
end |
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
require 'rails_helper' | |
require_relative '../fixtures/test_team.rb' | |
require_relative '../fixtures/test_player.rb' | |
RSpec.describe ValueRecord, type: :model do | |
it "can nest values" do | |
v = TestTeam.new( | |
"Blue Team", | |
[ | |
TestPlayer.new("Player 1"), | |
TestPlayer.new("Player 2") | |
]) | |
vr = ValueRecord.from_value(v) | |
vr.save! | |
vr_again = ValueRecord.find(vr.key) | |
expect(vr_again.value).to eq(v) | |
end | |
end |
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
require 'rails_helper' | |
require_relative '../fixtures/test_team.rb' | |
require_relative '../fixtures/test_player.rb' | |
require_relative '../fixtures/test_compressed_team.rb' | |
RSpec.describe ValueRecord, type: :model do | |
it "can dereference value pointers values" do | |
p2 = TestPlayer.new("Player 2") | |
vr2 = ValueRecord.from_value(p2) | |
vr2.save! | |
v = TestTeam.new( | |
"Blue Team", | |
[ | |
TestPlayer.new("Player 1"), | |
ValueRecord::ValuePointer.new(vr2.key) | |
]) | |
vr = ValueRecord.from_value(v) | |
vr.save! | |
vr_again = ValueRecord.find(vr.key) | |
v = vr_again.value | |
expect(v.name).to eq("Blue Team") | |
expect(v.players[0].name).to eq("Player 1") | |
expect(v.players[1].name).to eq("Player 2") | |
end | |
it "can compress values to value pointers" do | |
2.times do | |
name1 = "Player 1"*1000 | |
name2 = "Player 2"*1000 | |
v = TestCompressedTeam.new( | |
"Blue Team", | |
[ | |
TestPlayer.new(name1), | |
TestPlayer.new(name2) | |
]) | |
vr = ValueRecord.from_value(v) | |
vr.put! | |
vr_again = ValueRecord.find(vr.key) | |
vr_body_size = vr.body.to_json.size | |
vr_compressed = vr_again.compress | |
vr_compressed = vr_compressed.compress #test it's idempotent | |
vr_compressed.put! | |
vr_again = ValueRecord.find(vr_compressed.key) | |
vr_again_body_size = vr_again.body.to_json.size | |
v = vr_again.value | |
expect(v.name).to eq("Blue Team") | |
expect(v.players[0].name).to eq(name1) | |
expect(v.players[1].name).to eq(name2) | |
expect(vr_again_body_size).to be < vr_body_size | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment