sowing 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.ja.md +187 -0
- data/README.md +63 -6
- data/lib/sowing.rb +4 -0
- data/lib/sowing/definition_proxy.rb +11 -0
- data/lib/sowing/runner.rb +28 -35
- data/lib/sowing/selector.rb +45 -0
- data/lib/sowing/strategies/abstract_strategy.rb +10 -3
- data/lib/sowing/strategies/active_record_abstract.rb +9 -21
- data/lib/sowing/strategies/active_record_csv.rb +0 -2
- data/lib/sowing/strategies/active_record_yaml.rb +0 -2
- data/lib/sowing/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: faf7e09c35174fbde72fba78bfc3a1dc5b8968f6
|
4
|
+
data.tar.gz: a6550d15d1c8aecac286f4ee321e43afa743776b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2226164bac954535578bae7ecccb005167b806e1f8b7f0e3ba63bfd1c23490053806c01b9ebfc187bd30181fec1e7a0f6ab0605c57a23450f45b3e7a6a450bf7
|
7
|
+
data.tar.gz: 037cc850febfe410c29df71d7e88180134f8c0ade5905493b8cb9ec9851cfa8a6becb2930fb1cafe0670ebb12c7986af9a79cdf259a72f2a5e50d68fff433539
|
data/CHANGELOG.md
ADDED
data/README.ja.md
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
# Sowing
|
2
|
+
|
3
|
+
seed データの投入、および data migration 時のデータ投入のためのライブラリ。
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'sowing'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install sowing
|
20
|
+
|
21
|
+
## コンセプト
|
22
|
+
|
23
|
+
- seedに関わらずどこからでも使える
|
24
|
+
- CSV, YAML からデータを投入できる
|
25
|
+
- 投入しようとするデータが既にある場合、何もしないか更新するか選択できる
|
26
|
+
- Relational Data の投入
|
27
|
+
- ~~Pure Ruby~~ (現状はまだ Rails 依存のコードがあるが、将来的には Pure Ruby による実装にしたい)
|
28
|
+
- inspired by [sprig](https://github.com/vigetlabs/sprig)
|
29
|
+
|
30
|
+
## 使い方
|
31
|
+
|
32
|
+
Sowing を `db/seeds.rb` で使用する例を説明します。
|
33
|
+
|
34
|
+
Directories structure example:
|
35
|
+
|
36
|
+
```
|
37
|
+
db
|
38
|
+
├── seeds
|
39
|
+
│ ├── books.yml
|
40
|
+
│ └── users.csv
|
41
|
+
└── seeds.rb
|
42
|
+
```
|
43
|
+
|
44
|
+
`db/seeds/users.csv`:
|
45
|
+
|
46
|
+
```csv
|
47
|
+
first_name,last_name
|
48
|
+
Carlotta,Wilkinson
|
49
|
+
中平,薫
|
50
|
+
```
|
51
|
+
|
52
|
+
`db/seeds/books.yml`:
|
53
|
+
|
54
|
+
```yaml
|
55
|
+
item1:
|
56
|
+
name: 'Refactoring: Improving the Design of Existing Code'
|
57
|
+
author: 'Martin Fowler'
|
58
|
+
item2:
|
59
|
+
name: 'Webを支える技術'
|
60
|
+
author: '山本 陽平'
|
61
|
+
```
|
62
|
+
|
63
|
+
以上のファイルがあるとき `db/seeds.rb` を次のように定義します。
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
require 'sowing'
|
67
|
+
|
68
|
+
runner = Sowing::Runner.new
|
69
|
+
|
70
|
+
runner.create(User)
|
71
|
+
runner.create(Book)
|
72
|
+
```
|
73
|
+
|
74
|
+
Sowing では `Sowing::Runner` のインスタンスを使うことでデータ投入を行います。
|
75
|
+
`#create` メソッドの引数にデータを投入したい Model を渡すと、 sowing は Model を snake cake の複数形に変換した名前で CSV か YAML ファイル を `db/seeds` 以下に探しに行きます。
|
76
|
+
例えば `User` の場合、 `users.(csv|yaml|yml)` を探します。 (見つからない場合例外を出します)
|
77
|
+
CSV か YAML ファイルを探索するディレクトリ (data directory) は次の方法で変更できます。
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
# 方法1: Sowing::Runner.new のオプションで指定する
|
81
|
+
sowing = Sowing::Runner.new(data_directory: 'db/seeds/development')
|
82
|
+
|
83
|
+
# 方法2: configure を変更する
|
84
|
+
Sowing::Configuration.configure do |config|
|
85
|
+
config.default_data_directory = 'db/seeds/development'
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
### seeds を Rails.env によって変更する
|
90
|
+
|
91
|
+
sowing は Rails の `rake db:seed` 機能に関して何もしません。
|
92
|
+
単にデータ投入の機能を提供するだけです。
|
93
|
+
そのため、 `Rails.env` によってシードデータとして読み込みたいデータを変更したい場合には、次のようにしてください。
|
94
|
+
|
95
|
+
Directories structure example:
|
96
|
+
|
97
|
+
```
|
98
|
+
db
|
99
|
+
├── seeds/
|
100
|
+
│ ├── development/
|
101
|
+
│ │ ├── books.yml
|
102
|
+
│ │ └── users.csv
|
103
|
+
│ ├── development.rb
|
104
|
+
│ ├── production/
|
105
|
+
│ ├── production.rb
|
106
|
+
│ ├── test/
|
107
|
+
│ └── test.rb
|
108
|
+
└── seeds.rb
|
109
|
+
|
110
|
+
```
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
# db/seeds.rb
|
114
|
+
path = Rails.root.join('db', 'seeds', "#{Rails.env}.rb")
|
115
|
+
load path if path.exist?
|
116
|
+
|
117
|
+
# db/seeds/development.rb
|
118
|
+
require 'sowing'
|
119
|
+
|
120
|
+
sowing = Sowing::Runner.new(data_directory: 'db/seeds/development')
|
121
|
+
|
122
|
+
sowing.create(User)
|
123
|
+
sowing.create(Book)
|
124
|
+
```
|
125
|
+
|
126
|
+
## Sowing::Runner の機能
|
127
|
+
|
128
|
+
`Sowing::Runner` の instance methods には次のものがあります。
|
129
|
+
|
130
|
+
### create(klass, filename: nil)
|
131
|
+
|
132
|
+
引数に与えた `klass` のデータを新規作成します。
|
133
|
+
`filename` を指定しない場合、`klass` をsnake cake の複数形に変換した名前で CSV か YAML ファイル を `db/seeds` 以下に探しに行きます。(例えば、`klass` が `User` の場合、 `users.(csv|yaml|yml)` を探します。)
|
134
|
+
`filename` を指定する場合、 `new_users.csv` のように拡張子も含めて指定してください。
|
135
|
+
|
136
|
+
### create_or_skip(klass, finding_key, filename: nil)
|
137
|
+
|
138
|
+
`#create` とほぼ同じですが、 `finding_key` で指定したカラムでDBを探索して一致するデータが見つかった場合、何もしません。
|
139
|
+
同じスクリプトを複数回実行する可能性がある場合、 `create` より `create_or_skip` を使用することを推奨します。
|
140
|
+
|
141
|
+
例:
|
142
|
+
|
143
|
+
```
|
144
|
+
# 1度も下記のスクリプトを実行していないことを前提とする
|
145
|
+
|
146
|
+
runner = Sowing::Runner.new
|
147
|
+
|
148
|
+
# 1度目のデータ投入なので、users.csv からデータを投入する
|
149
|
+
runner.create_or_skip(User, :first_name)
|
150
|
+
|
151
|
+
# 上記で投入されたデータが存在するため、何もしない。
|
152
|
+
runner.create_or_skip(User, :first_name)
|
153
|
+
```
|
154
|
+
|
155
|
+
### create_or_update(klass, finding_key, filename: nil)
|
156
|
+
|
157
|
+
`#create_or_skip` とほぼ同じですが、`finding_key` で指定したカラムでDBを探索して一致するデータが見つかった場合、
|
158
|
+
データを更新します。
|
159
|
+
|
160
|
+
例:
|
161
|
+
|
162
|
+
```
|
163
|
+
# 1度も下記のスクリプトを実行していないことを前提とする
|
164
|
+
|
165
|
+
runner = Sowing::Runner.new
|
166
|
+
|
167
|
+
# 1度目のデータ投入なので、users.csv からデータを投入する
|
168
|
+
runner.create_or_skip(User, :first_name)
|
169
|
+
|
170
|
+
# users.csv と update_users.csv で同じ first_name のデータがある場合は update_users.csv の内容で上書きする
|
171
|
+
# update_users.csv にしかないデータは新規作成する
|
172
|
+
runner.create_or_skip(User, :first_name, filename: 'update_users.csv')
|
173
|
+
```
|
174
|
+
|
175
|
+
## Development
|
176
|
+
|
177
|
+
### Run tests
|
178
|
+
|
179
|
+
$ bundle exec ruby test/run-test.rb
|
180
|
+
|
181
|
+
## Contributing
|
182
|
+
|
183
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kbaba1001/sowing.
|
184
|
+
|
185
|
+
## License
|
186
|
+
|
187
|
+
MIT
|
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# Sowing
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
3
|
+
seed data handling for Rails apps.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
@@ -22,14 +20,73 @@ Or install it yourself as:
|
|
22
20
|
|
23
21
|
## Usage
|
24
22
|
|
25
|
-
|
23
|
+
db/seeds/
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
require 'sowing'
|
27
|
+
|
28
|
+
runner = Sowing::Runner.new
|
29
|
+
|
30
|
+
# if exist db/seeds/users.(csv|yaml|yml), read data
|
31
|
+
runner.create(User)
|
32
|
+
|
33
|
+
runner.create_or_skip(User, :first_name)
|
34
|
+
runner.create_or_update(User, :first_name)
|
35
|
+
|
36
|
+
# change data root directory
|
37
|
+
sowing = Sowing::Runner.new(data_directory: Rails.root.join('db/seeds/development'))
|
38
|
+
```
|
39
|
+
|
40
|
+
### Relational Data
|
41
|
+
|
42
|
+
For example, the following files exist:
|
43
|
+
|
44
|
+
`users.csv`
|
45
|
+
|
46
|
+
```csv
|
47
|
+
first_name,last_name
|
48
|
+
Carlotta,Wilkinson
|
49
|
+
中平,薫
|
50
|
+
```
|
51
|
+
|
52
|
+
`profiles.csv`
|
53
|
+
|
54
|
+
```csv
|
55
|
+
user_id,address,phone
|
56
|
+
"first_name: Carlotta, last_name: Wilkinson","2001 N Clark St, Chicago, IL 60614 America","+1 111-222-3333"
|
57
|
+
"first_name: 中平, last_name: 薫","東京都新宿区新宿1-1-1","090-1111-2222"
|
58
|
+
```
|
59
|
+
|
60
|
+
And, the ruby code using sowing:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
require 'sowing'
|
64
|
+
|
65
|
+
runner = Sowing::Runner.new
|
66
|
+
|
67
|
+
runner.create(User)
|
68
|
+
runner.create(Profile) do
|
69
|
+
mapping :user_id do |cel|
|
70
|
+
# cel #=> {'first_name' => 'Carlotta', 'last_name' => 'Wilkinson'}
|
71
|
+
|
72
|
+
User.find_by(
|
73
|
+
first_name: cel['first_name'],
|
74
|
+
last_name: cel['last_name']
|
75
|
+
).id # Must be return user id
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
26
79
|
|
27
80
|
## Development
|
28
81
|
|
29
82
|
### Run tests
|
30
83
|
|
31
|
-
$ bundle exec
|
84
|
+
$ bundle exec ruby test/run-test.rb
|
32
85
|
|
33
86
|
## Contributing
|
34
87
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
88
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kbaba1001/sowing.
|
89
|
+
|
90
|
+
## License
|
91
|
+
|
92
|
+
MIT
|
data/lib/sowing.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
module Sowing
|
2
|
+
class DataFileNotFound < StandardError; end
|
3
|
+
class StrategyNotFound < StandardError; end
|
2
4
|
end
|
3
5
|
|
4
6
|
require 'pathname'
|
@@ -8,4 +10,6 @@ require_relative 'sowing/strategies'
|
|
8
10
|
require_relative 'sowing/version'
|
9
11
|
|
10
12
|
require_relative 'sowing/configuration'
|
13
|
+
require_relative 'sowing/definition_proxy'
|
14
|
+
require_relative 'sowing/selector'
|
11
15
|
require_relative 'sowing/runner'
|
data/lib/sowing/runner.rb
CHANGED
@@ -3,57 +3,50 @@ class Sowing::Runner
|
|
3
3
|
|
4
4
|
def initialize(data_directory: nil)
|
5
5
|
@data_directory = Pathname(data_directory || Sowing::Configuration.config.default_data_directory)
|
6
|
+
@selector = Sowing::Selector.new(@data_directory)
|
6
7
|
end
|
7
8
|
|
8
|
-
def create(klass, filename: nil)
|
9
|
-
|
10
|
-
|
9
|
+
def create(klass, filename: nil, &block)
|
10
|
+
file, strategy = @selector.find(klass, filename)
|
11
|
+
|
12
|
+
create_rows(file, strategy, &block).each do |row|
|
13
|
+
strategy.create(klass, row)
|
11
14
|
end
|
12
15
|
end
|
13
16
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
+
def create_or_skip(klass, finding_key, filename: nil, &block)
|
18
|
+
file, strategy = @selector.find(klass, filename)
|
19
|
+
|
20
|
+
create_rows(file, strategy, &block).each do |row|
|
21
|
+
strategy.create_or_skip(klass, row, finding_key)
|
17
22
|
end
|
18
23
|
end
|
19
24
|
|
20
|
-
def create_or_update(klass, finding_key, filename: nil)
|
21
|
-
|
22
|
-
|
25
|
+
def create_or_update(klass, finding_key, filename: nil, &block)
|
26
|
+
file, strategy = @selector.find(klass, filename)
|
27
|
+
|
28
|
+
create_rows(file, strategy, &block).each do |row|
|
29
|
+
strategy.create_or_update(klass, row, finding_key)
|
23
30
|
end
|
24
31
|
end
|
25
32
|
|
26
33
|
private
|
27
34
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
if file.exist?
|
32
|
-
yield(file, select_strategy(file.extname[1..-1]))
|
33
|
-
else
|
34
|
-
raise "not found: #{file}"
|
35
|
-
end
|
36
|
-
|
37
|
-
return
|
38
|
-
end
|
35
|
+
def create_rows(file, strategy, &block)
|
36
|
+
proxy = Sowing::DefinitionProxy.new
|
37
|
+
proxy.instance_exec(&block) if block_given?
|
39
38
|
|
40
|
-
|
41
|
-
|
39
|
+
strategy.read_data(file).map {|row|
|
40
|
+
proxy.mappings.each do |key, pproc|
|
41
|
+
# TODO if i can skip, i want to skip. Doing so will increase performance
|
42
|
+
row[key.to_s] = pproc[string_to_hash(row.fetch(key.to_s))]
|
43
|
+
end
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
-
else
|
46
|
-
raise "not found: #{pathanme}.(#{Sowing::Configuration.config.extensions.join('|')})"
|
47
|
-
end
|
45
|
+
row
|
46
|
+
}
|
48
47
|
end
|
49
48
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
if strategy_klass
|
54
|
-
strategy_klass.new
|
55
|
-
else
|
56
|
-
raise "strategy not found: extension #{ext}"
|
57
|
-
end
|
49
|
+
def string_to_hash(str)
|
50
|
+
str.delete(' ').split(/[:,]/).each_slice(2).to_h
|
58
51
|
end
|
59
52
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class Sowing::Selector
|
2
|
+
attr_reader :data_directory
|
3
|
+
|
4
|
+
def initialize(data_directory)
|
5
|
+
@data_directory = data_directory
|
6
|
+
end
|
7
|
+
|
8
|
+
# @return [Array<Pathname, Sowing::Strategies>]
|
9
|
+
def find(klass, filename = nil)
|
10
|
+
if filename
|
11
|
+
find_file_from_filename(filename)
|
12
|
+
else
|
13
|
+
file_file_from_convention(klass)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def find_file_from_filename(filename)
|
20
|
+
file = data_directory.join(filename)
|
21
|
+
|
22
|
+
raise DataFileNotFound.new("not found: #{file}") unless file.exist?
|
23
|
+
|
24
|
+
[file, select_strategy(file.extname[1..-1])]
|
25
|
+
end
|
26
|
+
|
27
|
+
def file_file_from_convention(klass)
|
28
|
+
pathname = data_directory.join(klass.to_s.underscore.pluralize)
|
29
|
+
ext = Sowing::Configuration.config.extensions.find {|ext| Pathname("#{pathname}.#{ext}").exist? }
|
30
|
+
|
31
|
+
raise DataFileNotFound.new("not found: #{pathanme}.(#{Sowing::Configuration.config.extensions.join('|')})") unless ext
|
32
|
+
|
33
|
+
[Pathname("#{pathname}.#{ext}"), select_strategy(ext)]
|
34
|
+
end
|
35
|
+
|
36
|
+
def select_strategy(ext)
|
37
|
+
strategy_klass = Sowing::Configuration.config.strategies[ext]
|
38
|
+
|
39
|
+
if strategy_klass
|
40
|
+
strategy_klass.new
|
41
|
+
else
|
42
|
+
raise StrategyNotFound.new("strategy not found: extension #{ext}")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -1,21 +1,28 @@
|
|
1
1
|
module Sowing
|
2
2
|
module Strategies
|
3
3
|
class AbstractStrategy
|
4
|
-
def create(klass,
|
4
|
+
def create(klass, row)
|
5
5
|
raise NotImplementedError
|
6
6
|
end
|
7
7
|
|
8
|
-
def
|
8
|
+
def create_or_skip(klass, row, finding_key)
|
9
9
|
raise NotImplementedError
|
10
10
|
end
|
11
11
|
|
12
|
-
def create_or_update(klass,
|
12
|
+
def create_or_update(klass, row, finding_key)
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Enumerable] object
|
17
|
+
def read_data(filename)
|
13
18
|
raise NotImplementedError
|
14
19
|
end
|
15
20
|
|
16
21
|
private
|
17
22
|
|
18
23
|
def print_object_info(object)
|
24
|
+
return if ENV['SOWING_QUIET']
|
25
|
+
|
19
26
|
print 'create: '
|
20
27
|
p object
|
21
28
|
end
|
@@ -1,37 +1,25 @@
|
|
1
1
|
module Sowing
|
2
2
|
module Strategies
|
3
3
|
class ActiveRecordAbstract < AbstractStrategy
|
4
|
-
def create(klass,
|
5
|
-
|
6
|
-
object = klass.create!(row.to_hash)
|
4
|
+
def create(klass, row)
|
5
|
+
object = klass.create!(row.to_hash)
|
7
6
|
|
8
|
-
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
def create_or_do_nothing(klass, filename, finding_key)
|
13
|
-
read_data(filename).each do |row|
|
14
|
-
klass.find_or_initialize_by(finding_key => row[finding_key.to_s]) do |object|
|
15
|
-
object.update!(row.to_hash)
|
16
|
-
|
17
|
-
print_object_info(object)
|
18
|
-
end
|
19
|
-
end
|
7
|
+
print_object_info(object)
|
20
8
|
end
|
21
9
|
|
22
|
-
def
|
23
|
-
|
24
|
-
object = klass.find_or_initialize_by(finding_key => row[finding_key.to_s])
|
10
|
+
def create_or_skip(klass, row, finding_key)
|
11
|
+
klass.find_or_initialize_by(finding_key => row[finding_key.to_s]) do |object|
|
25
12
|
object.update!(row.to_hash)
|
26
13
|
|
27
14
|
print_object_info(object)
|
28
15
|
end
|
29
16
|
end
|
30
17
|
|
31
|
-
|
18
|
+
def create_or_update(klass, row, finding_key)
|
19
|
+
object = klass.find_or_initialize_by(finding_key => row[finding_key.to_s])
|
20
|
+
object.update!(row.to_hash)
|
32
21
|
|
33
|
-
|
34
|
-
raise NotImplementedError
|
22
|
+
print_object_info(object)
|
35
23
|
end
|
36
24
|
end
|
37
25
|
end
|
data/lib/sowing/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sowing
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- kbaba1001
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-08-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -144,14 +144,18 @@ extensions: []
|
|
144
144
|
extra_rdoc_files: []
|
145
145
|
files:
|
146
146
|
- ".gitignore"
|
147
|
+
- CHANGELOG.md
|
147
148
|
- Gemfile
|
149
|
+
- README.ja.md
|
148
150
|
- README.md
|
149
151
|
- Rakefile
|
150
152
|
- bin/console
|
151
153
|
- bin/setup
|
152
154
|
- lib/sowing.rb
|
153
155
|
- lib/sowing/configuration.rb
|
156
|
+
- lib/sowing/definition_proxy.rb
|
154
157
|
- lib/sowing/runner.rb
|
158
|
+
- lib/sowing/selector.rb
|
155
159
|
- lib/sowing/strategies.rb
|
156
160
|
- lib/sowing/strategies/abstract_strategy.rb
|
157
161
|
- lib/sowing/strategies/active_record_abstract.rb
|