PoEAA - 10. Data Source Architectual Patterns
@vividmuimui
2017/08/24 PoEAA読書会資料
RailsののActiveRecord。これも、このActiveRecordパターンを適用したもの。
「10. Data Source Architectual
Patterns」には以下の4種類があるが、
その中で一番シンプルなパターン。
この両方を1つのオブジェクトに詰め込んだパターンで、4つのパターンのうちとてもシンプルなパターン。
基本的には次のようなメソッドを持つ
このパターンは便利だが、DBが存在することを隠蔽できていない
As a result you usually see fewer of the other object -relational
mapping patterns present when you’re using ActiveRecord.
ActiveRecordは、RowDataGatewayパターンとよく似ている
RowDataGatewayはデータベースアクセスしか持たないが、ActiveRecordはデータベースアクセスとドメインロジックを持っている
このパターンでは、find系のクラスメソッドが多くなる傾向にあるが、ほかクラスに分けてはいけないということはない
ほかのパターンと併用しつつ、ActiveRecordはViewやQuery用に使うのも良い
Active
Recordは単なるCRUDするだけのようなドメインロジックが複雑すぎないケースではいい選択肢になる
1つのレコードに対する、Derivations and
validations(データの取得と検証?)がうまく出来る
Domain Modelの設計時の主な選択肢として、
ActiveRecordとDataMapperがあがるが、ActiveRecordはシンプルで理解しやすく作るのが簡単なのがメリット
ドメインロジックが複雑になってくると、relationships, collections,
inheritanceなどがすぐに欲しくなるだろう
それらをActiveRecordに適用するのは難易度が高いので、DataMapperを使ったほうがよい
ActiveRecordの短所の1つに、オブジェクトデザインとDBデザインが密結合しているため、プロジェクトの成長に合わせてリファクタしてくのが難しいという点がある
javaのサンプルが本に載っているので、rubyで実装してみた
(DBのコネクション部分はrailsのActiveRecordに頼っちゃいました
)
# Gemfile
source 'https://rubygems.org'
gem "activerecord"
gem "sqlite3"
require "active_record"
class CreatPeoples < ActiveRecord::Migration[5.1]
def change
create_table :people do |t|
t.string :lastname
t.string :firstname
t.integer :number_of_dependents
end
end
end
CreatPeoples.migrate(:up)
people
テーブルは、
id, lastname, firstname, number_of_dependents
の4つのカラムを持っている
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.establish_connection(
adapter: "sqlite3",
database: "demo.sqlite3"
)
sqliteにつなげるように設定する
class People
FIND_SQL = "SELECT id, lastname, firstname, number_of_dependents FROM people WHERE id = ?;"
class << self
# キャッシュ周りのロジックは省略しました
def find(id)
# SQLをいい感じに組み立てて実行
row_hash = con.select_one(ActiveRecord::Base.send(:sanitize_sql_array, [FIND_SQL, id]))
new(row_hash.symbolize_keys) if row_hash # レコードが見つかったら自身のインスタンスを作成する
end
def con
@con ||= ActiveRecord::Base.connection
end
end
attr_accessor :id, :lastname, :firstname, :number_of_dependents
def initialize(id: nil, lastname: nil, firstname: nil, number_of_dependents: nil)
@id, @lastname, @firstname, @number_of_dependents = id, lastname, firstname, number_of_dependents
end
end
実行してみると
(main)> People.find(1)
(0.2ms) SELECT id, lastname, firstname, number_of_dependents FROM people WHERE id = 1;
=> #<People:0x007fa92b2adb00 @firstname="foo", @id=1, @lastname="bar", @number_of_dependents=100>
動く
class People
UPDATE_SQL = "UPDATE people SET lastname = :lastname, firstname = :firstname, number_of_dependents = :number_of_dependents WHERE id = :id;"
def update
self.class.con.execute(ActiveRecord::Base.send(:sanitize_sql_array, [UPDATE_SQL, attributes]))
end
def attributes
{ id: id, firstname: firstname, lastname: lastname, number_of_dependents: number_of_dependents }
end
end
クエリを組み立てて実行するだけ。実行してみると
(main)> people = People.find(1)
(0.2ms) SELECT id, lastname, firstname, number_of_dependents FROM people WHERE id = 1;
=> #<People:0x007fa92a47efc0 @firstname="foo", @id=1, @lastname="bar", @number_of_dependents=100>
// lastnameを書き換え
(main)> people.lastname = "hoge"
=> "hoge"
// update
(main)> people.update
(0.8ms) UPDATE people SET lastname = 'hoge', firstname = 'foo', number_of_dependents = 100 WHERE id = 1;
=> []
// 取得し直してみるとlastnameが変わっているのがわかる
(main)> People.find(1)
(0.2ms) SELECT id, lastname, firstname, number_of_dependents FROM people WHERE id = 1;
=> #<People:0x007fa92d82a7e8 @firstname="foo", @id=1, @lastname="hoge", @number_of_dependents=100>
class People
INSERT_SQL = "INSERT INTO people(id, lastname, firstname, number_of_dependents) VALUES (:id, :lastname, :firstname, :number_of_dependents);"
def insert
self.class.con.execute(ActiveRecord::Base.send(:sanitize_sql_array, [INSERT_SQL, attributes]))
end
end
クエリを組み立てて実行するだけ。(保存済みかどうかみたいのは考慮してない)
実行してみると
// newでインスタンスを作成
(main)> people = People.new(id: 2, firstname: "hoge", lastname: "huga", number_of_dependents: 200)
=> #<People:0x007fa92b3d6590 @firstname="hoge", @id=2, @lastname="huga", @number_of_dependents=200>
// insert
(main)> people.insert
(9.0ms) INSERT INTO people(id, lastname, firstname, number_of_dependents) VALUES (2, 'huga', 'hoge', 200);
=> []
// チートして用意した .all で確認すると、 レコードが増えていることがわかる
(main)> People.all
People Load (0.2ms) SELECT "people".* FROM "people"
=> [#<People:0x007fa92a6ab618 id: 1, lastname: "hoge", firstname: "foo", number_of_dependents: 100>,
#<People:0x007fa92a698018 id: 2, lastname: "huga", firstname: "hoge", number_of_dependents: 200>]
class People
def exemption
base = 1500
dependent = 750
base + dependent * number_of_dependents
end
end
本のサンプルがよく分かんなかったので、適当なメソッドを用意しました
実行してみると
(main)> People.find(1).exemption
(0.2ms) SELECT id, lastname, firstname, number_of_dependents FROM people WHERE id = 1;
=> 76500
(main)> People.find(2).exemption
(0.1ms) SELECT id, lastname, firstname, number_of_dependents FROM people WHERE id = 2;
=> 151500
案外簡単にさくっとActiveRecordパターンを実装出来た
(クエリビルダーとかコネクション周りでrailsのActiveRecord使ったけど。。)
require "active_record"
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.establish_connection(
adapter: "sqlite3",
database: "demo.sqlite3"
)
class Registory
class << self
def person(id) ;end
def add_person(person); end
end
end
class People
FIND_SQL = "SELECT id, lastname, firstname, number_of_dependents FROM people WHERE id = ?;"
UPDATE_SQL = "UPDATE people SET lastname = :lastname, firstname = :firstname, number_of_dependents = :number_of_dependents WHERE id = :id;"
INSERT_SQL = "INSERT INTO people(id, lastname, firstname, number_of_dependents) VALUES (:id, :lastname, :firstname, :number_of_dependents);"
class << self
# キャッシュ周りのロジックは省略しました
def find(id)
# SQLをいい感じに組み立てて実行
row_hash = con.select_one(ActiveRecord::Base.send(:sanitize_sql_array, [FIND_SQL, id]))
new(row_hash.symbolize_keys) if row_hash # レコードが見つかったら自身のインスタンスを作成する
end
def con
@con ||= ActiveRecord::Base.connection
end
end
attr_accessor :id, :lastname, :firstname, :number_of_dependents
def initialize(id: nil, lastname: nil, firstname: nil, number_of_dependents: nil)
@id, @lastname, @firstname, @number_of_dependents = id, lastname, firstname, number_of_dependents
end
def update
self.class.con.execute(ActiveRecord::Base.send(:sanitize_sql_array, [UPDATE_SQL, attributes]))
end
def insert
self.class.con.execute(ActiveRecord::Base.send(:sanitize_sql_array, [INSERT_SQL, attributes]))
end
def exemption
base = 1500
dependent = 750
base + dependent * number_of_dependents
end
def attributes
{
id: id,
firstname: firstname,
lastname: lastname,
number_of_dependents: number_of_dependents
}
end
end
ActiveRecord::Base.connection.execute("drop table people;") rescue nil
class CreatPeoples < ActiveRecord::Migration[5.1]
def change
create_table :people do |t|
t.string :lastname
t.string :firstname
t.integer :number_of_dependents
end
end
end
CreatPeoples.migrate(:up)
# 初期データを用意
People.new(id: 1, firstname: "foo", lastname: "bar", number_of_dependents: 100).insert
# select
people = People.find(1)
# update
people = People.find(1)
people.lastname = "hoge"
people.update
People.find(1)
# insert
people = People.new(id: 2, firstname: "hoge", lastname: "huga", number_of_dependents: 200)
people.insert
class CheatPeople < ActiveRecord::Base
self.table_name = "people"
end
CheatPeople.all
# ビジネスロジック
People.find(1).exemption
People.find(2).exemption