master_data_tool 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/Gemfile +10 -0
- data/README.md +146 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/master_data_tool +57 -0
- data/lib/generators/master_data_tool/install/install_generator.rb +39 -0
- data/lib/generators/master_data_tool/install/templates/create_master_data_statuses.rb.erb +17 -0
- data/lib/master_data_tool/config.rb +21 -0
- data/lib/master_data_tool/dump/executor.rb +73 -0
- data/lib/master_data_tool/dump.rb +3 -0
- data/lib/master_data_tool/import/executor.rb +123 -0
- data/lib/master_data_tool/import.rb +3 -0
- data/lib/master_data_tool/master_data.rb +158 -0
- data/lib/master_data_tool/master_data_status.rb +40 -0
- data/lib/master_data_tool/report/core.rb +21 -0
- data/lib/master_data_tool/report/default_printer.rb +14 -0
- data/lib/master_data_tool/report/import_report.rb +85 -0
- data/lib/master_data_tool/report/print_affected_table_report.rb +18 -0
- data/lib/master_data_tool/report/printer.rb +11 -0
- data/lib/master_data_tool/report/verify_report.rb +32 -0
- data/lib/master_data_tool/report.rb +8 -0
- data/lib/master_data_tool/version.rb +5 -0
- data/lib/master_data_tool.rb +31 -0
- data/sig/master_data_tool.rbs +4 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f743857cb2c8d71b0382648bda8a4a528f025bd385fa0d1016ac956fbca87a27
|
4
|
+
data.tar.gz: f47f109a2b5224224706aa9bfc36f9805fb5dab367105b9fa0e211125b50e9e4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 29e2f8763b3c512723b7fb1f0320e1bdec9e1e7636cad315bb55e275f54dedb2a0c7e10ecb26c97d760cf24c1a865212aed9bda5360928865db2cfad8f03c067
|
7
|
+
data.tar.gz: 19622b43daf9f7be96204024aa9e9ec8b42b02e401aa356af809d60b44bea2519d7944e9eb7e5c468b326821c6b803443351332860a5f5a1db761a98d6086621
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
# MasterDataTool
|
2
|
+
|
3
|
+
[](https://github.com/taka0125/master_data_tool/actions)
|
4
|
+
|
5
|
+
システムが稼働する上で最初から必要なデータ(マスタデータ)を管理するツール
|
6
|
+
|
7
|
+
以下の機能を提供する
|
8
|
+
|
9
|
+
- CSVからテーブルにデータを入れる
|
10
|
+
- dry-runができる
|
11
|
+
- 新規・更新・変更なし・削除がわかる
|
12
|
+
- CSVのハッシュ値をDBに記録し差分があったテーブルのみ取り込みを実行する
|
13
|
+
- 既存DBからCSVとしてデータをダンプする
|
14
|
+
|
15
|
+
## 前提条件
|
16
|
+
|
17
|
+
- マスタデータの更新は同時並行で実行されない
|
18
|
+
- `db/fixtures/#{table_name}.csv` の命名規則
|
19
|
+
|
20
|
+
## インストール
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'master_data_tool'
|
24
|
+
```
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
$ bundle install
|
29
|
+
|
30
|
+
Or install it yourself as:
|
31
|
+
|
32
|
+
$ gem install master_data_tool
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
### マスタデータの投入
|
37
|
+
|
38
|
+
| option | default | 内容 |
|
39
|
+
|----------------------| --- |--------------------------------------|
|
40
|
+
| --dry-run | true | dry-runモードで実行する(データ変更は行わない) |
|
41
|
+
| --verify | true | データ投入後に全テーブル・全レコードのバリデーションチェックを行う |
|
42
|
+
| --only-import-tables | [] | 指定したテーブルのみデータ投入を行う |
|
43
|
+
| --only-verify-tables | [] | 指定したテーブルのみ投入後のバリデーションチェックを行う |
|
44
|
+
| --skip-no-change | true | CSVファイルに更新がないテーブルをスキップする |
|
45
|
+
|
46
|
+
```bash
|
47
|
+
bundle exec master_data_tool import
|
48
|
+
```
|
49
|
+
|
50
|
+
は以下のオプションを指定したものと一緒
|
51
|
+
|
52
|
+
```bash
|
53
|
+
bundle exec thor master_data_tool import \
|
54
|
+
--dry-run=true \
|
55
|
+
--verify=true \
|
56
|
+
--only-import-tables="" \
|
57
|
+
--only-verify-tables="" \
|
58
|
+
--skip-no-change=true
|
59
|
+
```
|
60
|
+
|
61
|
+
### ダンプ
|
62
|
+
|
63
|
+
| option | default | 内容 |
|
64
|
+
|-----------------------|---------|---------------|
|
65
|
+
| --ignore-empty-table | true | 空のテーブルを無視する |
|
66
|
+
| --ignore-tables | [] | 指定したテーブルを無視する |
|
67
|
+
| --ignore-column-names | [] | 指定したカラムを無視する |
|
68
|
+
| --verbose | false | 詳細表示 |
|
69
|
+
|
70
|
+
```bash
|
71
|
+
bundle exec master_data_tool dump
|
72
|
+
```
|
73
|
+
|
74
|
+
は以下のオプションを指定したものと一緒
|
75
|
+
|
76
|
+
```bash
|
77
|
+
bundle exec master_data_tool dump \
|
78
|
+
--ignore-empty-table=true \
|
79
|
+
--ignore-tables="" \
|
80
|
+
--ignore-column-names="" \
|
81
|
+
--verbose=false
|
82
|
+
```
|
83
|
+
|
84
|
+
## マイグレーション
|
85
|
+
|
86
|
+
`master_data_statuses` というテーブルにCSVファイルのハッシュ値を記録し差分更新に利用する
|
87
|
+
|
88
|
+
```
|
89
|
+
bundle exec rails generate master_data_tool:install
|
90
|
+
```
|
91
|
+
|
92
|
+
を実行するとマイグレーションファイルが生成される。
|
93
|
+
|
94
|
+
ridgepoleの場合は以下のような定義で実行する
|
95
|
+
|
96
|
+
```
|
97
|
+
create_table 'master_data_statuses', id: :bigint, unsigned: true, force: :cascade, comment: "マスタデータの状態管理用テーブル" do |t|
|
98
|
+
t.string "name", limit: 255, null: false, comment: 'テーブル名'
|
99
|
+
t.string "version", limit: 255, null: false, comment: 'ハッシュ値'
|
100
|
+
t.datetime "created_at", null: false, comment: '作成日時'
|
101
|
+
t.datetime "updated_at", null: false, comment: '更新日時'
|
102
|
+
end
|
103
|
+
|
104
|
+
add_index 'master_data_statuses', ["name"], name: "idx_master_data_statuses_1", unique: true, using: :btree
|
105
|
+
add_index 'master_data_statuses', ["name", "version"], name: "idx_master_data_statuses_2", using: :btree
|
106
|
+
```
|
107
|
+
|
108
|
+
|
109
|
+
## Tips
|
110
|
+
### マスタデータ投入でどうなるか?を調べる
|
111
|
+
|
112
|
+
```
|
113
|
+
RAILS_ENV=development bundle exec master_data_tool import > /tmp/dry-run.txt
|
114
|
+
```
|
115
|
+
|
116
|
+
- 影響を受けるテーブル
|
117
|
+
|
118
|
+
```
|
119
|
+
grep 'operation:affected_table' /tmp/dry-run.txt
|
120
|
+
```
|
121
|
+
|
122
|
+
- 更新されるレコード
|
123
|
+
|
124
|
+
```
|
125
|
+
grep 'operation:import' /tmp/dry-run.txt | grep 'label:detail' | grep 'status:updated'
|
126
|
+
```
|
127
|
+
|
128
|
+
- 削除されるレコード
|
129
|
+
|
130
|
+
```
|
131
|
+
grep 'operation:import' /tmp/dry-run.txt | grep 'label:detail' | grep 'status:deleted'
|
132
|
+
```
|
133
|
+
|
134
|
+
- 追加されるレコード
|
135
|
+
|
136
|
+
```
|
137
|
+
grep 'operation:import' /tmp/dry-run.txt | grep 'label:detail' | grep 'status:new'
|
138
|
+
```
|
139
|
+
|
140
|
+
## TODO
|
141
|
+
|
142
|
+
- upsert_allに移行する
|
143
|
+
|
144
|
+
## Contributing
|
145
|
+
|
146
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/taka0125/master_data_tool.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "master_data_tool"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'thor'
|
5
|
+
require 'master_data_tool'
|
6
|
+
|
7
|
+
environment_path = "#{Dir.pwd}/config/environment"
|
8
|
+
require environment_path
|
9
|
+
|
10
|
+
module MasterDataTool
|
11
|
+
class CLI < Thor
|
12
|
+
option :dry_run, default: true, type: :boolean
|
13
|
+
option :verify, default: true, type: :boolean
|
14
|
+
option :only_import_tables, default: [], type: :array
|
15
|
+
option :only_verify_tables, default: [], type: :array
|
16
|
+
option :skip_no_change, default: true, type: :boolean
|
17
|
+
desc 'import', 'import'
|
18
|
+
def import
|
19
|
+
dry_run = options[:dry_run]
|
20
|
+
verify = options[:verify]
|
21
|
+
only_import_tables = options[:only_import_tables]
|
22
|
+
only_verify_tables = options[:only_verify_tables]
|
23
|
+
skip_no_change = options[:skip_no_change]
|
24
|
+
|
25
|
+
executor = MasterDataTool::Import::Executor.new(
|
26
|
+
dry_run: dry_run,
|
27
|
+
verify: verify,
|
28
|
+
only_import_tables: only_import_tables,
|
29
|
+
only_verify_tables: only_verify_tables,
|
30
|
+
skip_no_change: skip_no_change
|
31
|
+
)
|
32
|
+
executor.execute
|
33
|
+
end
|
34
|
+
|
35
|
+
option :ignore_empty_table, default: true, type: :boolean
|
36
|
+
option :ignore_tables, default: [], type: :array
|
37
|
+
option :ignore_column_names, default: [], type: :array
|
38
|
+
option :verbose, default: false, type: :boolean
|
39
|
+
desc 'dump', 'dump'
|
40
|
+
def dump
|
41
|
+
ignore_empty_table = options[:ignore_empty_table]
|
42
|
+
ignore_tables = options[:ignore_tables]
|
43
|
+
ignore_column_names = options[:ignore_column_names]
|
44
|
+
verbose = options[:verbose]
|
45
|
+
|
46
|
+
executor = MasterDataTool::Dump::Executor.new(
|
47
|
+
ignore_empty_table: ignore_empty_table,
|
48
|
+
ignore_tables: ignore_tables,
|
49
|
+
ignore_column_names: ignore_column_names,
|
50
|
+
verbose: verbose
|
51
|
+
)
|
52
|
+
executor.execute
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
MasterDataTool::CLI.start(ARGV)
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "rails/generators/active_record"
|
3
|
+
|
4
|
+
module MasterDataTool
|
5
|
+
class InstallGenerator < ::Rails::Generators::Base
|
6
|
+
include ::Rails::Generators::Migration
|
7
|
+
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def self.next_migration_number(dirname)
|
11
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_migration_file
|
15
|
+
migration_dir = File.expand_path("db/migrate")
|
16
|
+
template = 'create_master_data_statuses'
|
17
|
+
|
18
|
+
if self.class.migration_exists?(migration_dir, template)
|
19
|
+
::Kernel.warn "Migration already exists: #{template}"
|
20
|
+
else
|
21
|
+
migration_template(
|
22
|
+
"#{template}.rb.erb",
|
23
|
+
"db/migrate/#{template}.rb",
|
24
|
+
migration_version: migration_version
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def migration_version
|
32
|
+
format(
|
33
|
+
"[%d.%d]",
|
34
|
+
ActiveRecord::VERSION::MAJOR,
|
35
|
+
ActiveRecord::VERSION::MINOR
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateMasterDataStatuses < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def self.up
|
3
|
+
create_table :master_data_statuses do |t|
|
4
|
+
t.string :name, limit: 255, null: false, comment: 'テーブル名'
|
5
|
+
t.string :version, limit: 255, null: false, comment: 'ハッシュ値'
|
6
|
+
t.datetime :created_at, null: false, comment: '作成日時'
|
7
|
+
t.datetime :updated_at, null: false, comment: '更新日時'
|
8
|
+
end
|
9
|
+
|
10
|
+
add_index :master_data_statuses, %i[name], name: 'idx_master_data_statuses_1', unique: true
|
11
|
+
add_index :master_data_statuses, %i[name version], name: 'idx_master_data_statuses_2'
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
drop_table :master_data_statuses
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'active_support/configurable'
|
2
|
+
|
3
|
+
module MasterDataTool
|
4
|
+
class Config
|
5
|
+
include ActiveSupport::Configurable
|
6
|
+
|
7
|
+
config_accessor :master_data_dir
|
8
|
+
config_accessor :dump_ignore_tables
|
9
|
+
config_accessor :dump_ignore_columns
|
10
|
+
config_accessor :logger
|
11
|
+
|
12
|
+
def self.default_config
|
13
|
+
new.tap do |config|
|
14
|
+
config.master_data_dir = Rails.root.join('db/fixtures')
|
15
|
+
config.dump_ignore_tables = %w[]
|
16
|
+
config.dump_ignore_columns = %w[]
|
17
|
+
config.logger = Rails.logger
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MasterDataTool
|
4
|
+
module Dump
|
5
|
+
class Executor
|
6
|
+
Error = Struct.new(:table, :exception)
|
7
|
+
|
8
|
+
DEFAULT_IGNORE_TABLES = %w[ar_internal_metadata schema_migrations master_data_statuses]
|
9
|
+
DEFAULT_IGNORE_COLUMNS = %w[created_at updated_at]
|
10
|
+
|
11
|
+
def initialize(ignore_empty_table: true, ignore_tables: [], ignore_column_names: [], verbose: false)
|
12
|
+
@ignore_empty_table = ignore_empty_table
|
13
|
+
@ignore_tables = DEFAULT_IGNORE_TABLES + Array(MasterDataTool.config.dump_ignore_tables) + ignore_tables
|
14
|
+
@ignore_column_names = DEFAULT_IGNORE_COLUMNS + Array(MasterDataTool.config.dump_ignore_columns) + ignore_column_names
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute
|
18
|
+
[].tap do |errors|
|
19
|
+
ApplicationRecord.connection.tables.each do |table|
|
20
|
+
if @ignore_tables.include?(table)
|
21
|
+
print_message "[ignore] #{table}"
|
22
|
+
|
23
|
+
next
|
24
|
+
end
|
25
|
+
|
26
|
+
dump_to_csv(table)
|
27
|
+
rescue => e
|
28
|
+
errors << Error.new(table, e)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def print_message(message)
|
36
|
+
return unless @verbose
|
37
|
+
|
38
|
+
puts message
|
39
|
+
end
|
40
|
+
|
41
|
+
def dump_to_csv(table)
|
42
|
+
model_klass = Rails.const_get(table.classify)
|
43
|
+
if ignore?(model_klass)
|
44
|
+
print_message "[ignore] #{table}"
|
45
|
+
|
46
|
+
return
|
47
|
+
end
|
48
|
+
|
49
|
+
csv_path = Pathname.new(MasterDataTool.config.master_data_dir).join("#{table}.csv")
|
50
|
+
CSV.open(csv_path, 'w', force_quotes: true) do |csv|
|
51
|
+
headers = model_klass.column_names - @ignore_column_names
|
52
|
+
|
53
|
+
csv << headers
|
54
|
+
|
55
|
+
model_klass.all.find_each do |record|
|
56
|
+
items = []
|
57
|
+
headers.each do |name|
|
58
|
+
items << record[name]
|
59
|
+
end
|
60
|
+
|
61
|
+
csv << items
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def ignore?(model_klass)
|
67
|
+
return false unless @ignore_empty_table
|
68
|
+
|
69
|
+
model_klass.count < 1
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MasterDataTool
|
4
|
+
module Import
|
5
|
+
class Executor
|
6
|
+
def initialize(dry_run: true, verify: true, only_import_tables: [], only_verify_tables: [], skip_no_change: true, report_printer: MasterDataTool::Report::DefaultPrinter.new)
|
7
|
+
@dry_run = dry_run
|
8
|
+
@verify = verify
|
9
|
+
@only_import_tables = Array(only_import_tables)
|
10
|
+
@only_verify_tables = Array(only_verify_tables)
|
11
|
+
@skip_no_change = skip_no_change
|
12
|
+
@report_printer = report_printer
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute
|
16
|
+
ApplicationRecord.transaction do
|
17
|
+
master_data_list = build_master_data_list
|
18
|
+
|
19
|
+
import_all!(master_data_list)
|
20
|
+
verify_all!(master_data_list) if @verify
|
21
|
+
save_master_data_statuses!(master_data_list)
|
22
|
+
|
23
|
+
print_affected_tables(master_data_list)
|
24
|
+
|
25
|
+
raise DryRunError if @dry_run
|
26
|
+
|
27
|
+
master_data_list
|
28
|
+
end
|
29
|
+
rescue DryRunError
|
30
|
+
puts "[DryRun] end"
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def build_master_data_list
|
36
|
+
[].tap do |master_data_list|
|
37
|
+
extract_master_data_csv_paths.each do |path|
|
38
|
+
table_name = MasterDataTool.resolve_table_name(path)
|
39
|
+
load_skip = load_skip_table?(table_name, path)
|
40
|
+
|
41
|
+
model_klass = Rails.const_get(table_name.classify)
|
42
|
+
master_data = MasterData.new(path, model_klass)
|
43
|
+
master_data.load unless load_skip
|
44
|
+
|
45
|
+
master_data_list << master_data
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# 1. 変更があるかどうかのチェックをスキップした
|
51
|
+
# 2. 変更があるかどうかのチェックを実行し、変更がないので処理をスキップした
|
52
|
+
# 3. 変更があるかどうかのチェックを実行し、変更があるので実行した
|
53
|
+
# の3パターンがある
|
54
|
+
def import_all!(master_data_list)
|
55
|
+
master_data_list.each do |master_data|
|
56
|
+
next unless master_data.loaded?
|
57
|
+
|
58
|
+
report = master_data.import!(dry_run: @dry_run)
|
59
|
+
report.print(@report_printer)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def verify_all!(master_data_list)
|
64
|
+
master_data_list.each do |master_data|
|
65
|
+
next if verify_skip_table?(master_data.table_name)
|
66
|
+
|
67
|
+
report = master_data.verify!(dry_run: @dry_run)
|
68
|
+
report.print(@report_printer)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def save_master_data_statuses!(master_data_list)
|
73
|
+
records = []
|
74
|
+
master_data_list.each do |master_data|
|
75
|
+
records << MasterDataTool::MasterDataStatus.build(master_data.csv_path)
|
76
|
+
end
|
77
|
+
|
78
|
+
MasterDataTool::MasterDataStatus.import_records!(records, dry_run: @dry_run)
|
79
|
+
end
|
80
|
+
|
81
|
+
def print_affected_tables(master_data_list)
|
82
|
+
master_data_list.each do |master_data|
|
83
|
+
next unless master_data.loaded?
|
84
|
+
next unless master_data.affected?
|
85
|
+
|
86
|
+
report = master_data.print_affected_table
|
87
|
+
report&.print(@report_printer)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def load_skip_table?(table_name, csv_path)
|
92
|
+
return load_skip_table_when_target_all_table?(table_name) unless @skip_no_change
|
93
|
+
|
94
|
+
load_skip_table_when_target_changed_table?(table_name, csv_path)
|
95
|
+
end
|
96
|
+
|
97
|
+
def load_skip_table_when_target_changed_table?(table_name, csv_path)
|
98
|
+
unless @only_import_tables.empty?
|
99
|
+
return true if @only_import_tables.exclude?(table_name)
|
100
|
+
end
|
101
|
+
|
102
|
+
!MasterDataTool::MasterDataStatus.master_data_will_change?(csv_path)
|
103
|
+
end
|
104
|
+
|
105
|
+
def load_skip_table_when_target_all_table?(table_name)
|
106
|
+
return false if @only_import_tables.empty?
|
107
|
+
|
108
|
+
@only_import_tables.exclude?(table_name)
|
109
|
+
end
|
110
|
+
|
111
|
+
def verify_skip_table?(table_name)
|
112
|
+
return false if @only_verify_tables.empty?
|
113
|
+
|
114
|
+
@only_verify_tables.exclude?(table_name)
|
115
|
+
end
|
116
|
+
|
117
|
+
def extract_master_data_csv_paths
|
118
|
+
pattern = Pathname.new(MasterDataTool.config.master_data_dir).join('*.csv').to_s
|
119
|
+
Pathname.glob(pattern).select(&:file?)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MasterDataTool
|
4
|
+
module Import
|
5
|
+
class MasterData
|
6
|
+
attr_reader :csv_path, :model_klass, :columns, :new_records, :updated_records, :no_change_records, :deleted_records
|
7
|
+
attr_reader :before_count, :after_count
|
8
|
+
|
9
|
+
def initialize(csv_path, model_klass)
|
10
|
+
@csv_path = csv_path
|
11
|
+
@model_klass = model_klass
|
12
|
+
|
13
|
+
@loaded = false
|
14
|
+
|
15
|
+
@columns = []
|
16
|
+
@new_records = []
|
17
|
+
@updated_records = []
|
18
|
+
@no_change_records = []
|
19
|
+
@deleted_records = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def load
|
23
|
+
csv = CSV.read(@csv_path, headers: true, skip_blanks: true)
|
24
|
+
old_records_by_id = @model_klass.all.index_by(&:id)
|
25
|
+
|
26
|
+
csv_records_by_id = build_records_from_csv(csv, old_records_by_id)
|
27
|
+
deleted_ids = old_records_by_id.keys - csv_records_by_id.keys
|
28
|
+
|
29
|
+
@columns = csv.headers
|
30
|
+
|
31
|
+
csv_records_by_id.each do |_, record|
|
32
|
+
if record.new_record?
|
33
|
+
@new_records << record
|
34
|
+
|
35
|
+
next
|
36
|
+
end
|
37
|
+
|
38
|
+
if record.has_changes_to_save?
|
39
|
+
@updated_records << record
|
40
|
+
|
41
|
+
next
|
42
|
+
end
|
43
|
+
|
44
|
+
@no_change_records << record
|
45
|
+
end
|
46
|
+
|
47
|
+
deleted_ids.each do |id|
|
48
|
+
@deleted_records << old_records_by_id[id]
|
49
|
+
end
|
50
|
+
|
51
|
+
@loaded = true
|
52
|
+
end
|
53
|
+
|
54
|
+
def import_records
|
55
|
+
new_records + updated_records + no_change_records
|
56
|
+
end
|
57
|
+
|
58
|
+
def affected_records
|
59
|
+
new_records + updated_records + deleted_records
|
60
|
+
end
|
61
|
+
|
62
|
+
def new_records
|
63
|
+
raise MasterDataTool::NotLoadedError unless @loaded
|
64
|
+
|
65
|
+
@new_records
|
66
|
+
end
|
67
|
+
|
68
|
+
def updated_records
|
69
|
+
raise MasterDataTool::NotLoadedError unless @loaded
|
70
|
+
|
71
|
+
@updated_records
|
72
|
+
end
|
73
|
+
|
74
|
+
def no_change_records
|
75
|
+
raise MasterDataTool::NotLoadedError unless @loaded
|
76
|
+
|
77
|
+
@no_change_records
|
78
|
+
end
|
79
|
+
|
80
|
+
def deleted_records
|
81
|
+
raise MasterDataTool::NotLoadedError unless @loaded
|
82
|
+
|
83
|
+
@deleted_records
|
84
|
+
end
|
85
|
+
|
86
|
+
def loaded?
|
87
|
+
@loaded
|
88
|
+
end
|
89
|
+
|
90
|
+
def affected?
|
91
|
+
return @affected if instance_variable_defined?(:@affected)
|
92
|
+
@affected = affected_records.any?
|
93
|
+
end
|
94
|
+
|
95
|
+
def before_count
|
96
|
+
@before_count ||= updated_records.count + no_change_records.count + deleted_records.count
|
97
|
+
end
|
98
|
+
|
99
|
+
def after_count
|
100
|
+
@after_count ||= updated_records.count + no_change_records.count + new_records.count
|
101
|
+
end
|
102
|
+
|
103
|
+
def table_name
|
104
|
+
@model_klass.table_name
|
105
|
+
end
|
106
|
+
|
107
|
+
def import!(dry_run: true)
|
108
|
+
raise MasterDataTool::NotLoadedError unless @loaded
|
109
|
+
|
110
|
+
MasterDataTool::Report::ImportReport.new(self).tap do |report|
|
111
|
+
return report if dry_run
|
112
|
+
return report unless affected?
|
113
|
+
|
114
|
+
@model_klass.delete_all
|
115
|
+
|
116
|
+
# マスターデータ間の依存がある場合に投入順制御するのは大変なのでこのタイミングでのバリデーションはしない
|
117
|
+
@model_klass.import(import_records, validate: false, on_duplicate_key_update: @columns, timestamps: true)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def verify!(dry_run: true)
|
122
|
+
MasterDataTool::Report::VerifyReport.new(self).tap do |report|
|
123
|
+
@model_klass.all.find_each do |record|
|
124
|
+
valid = record.valid?
|
125
|
+
report.append(MasterDataTool::Report::VerifyReport.build_verify_record_report(self, record, valid))
|
126
|
+
next if dry_run
|
127
|
+
|
128
|
+
raise MasterDataTool::VerifyFailed.new("[#{table_name}] id = #{record.id} is invalid") unless valid
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def print_affected_table
|
134
|
+
return unless loaded?
|
135
|
+
return unless affected?
|
136
|
+
|
137
|
+
MasterDataTool::Report::PrintAffectedTableReport.new(self)
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def build_records_from_csv(csv, old_records_by_id)
|
143
|
+
{}.tap do |records|
|
144
|
+
csv.each do |row|
|
145
|
+
id = row['id'].to_i
|
146
|
+
record = old_records_by_id[id] || @model_klass.new(id: id)
|
147
|
+
|
148
|
+
csv.headers.each do |key|
|
149
|
+
record[key.to_s] = row[key]
|
150
|
+
end
|
151
|
+
|
152
|
+
records[id] = record
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'activerecord-import'
|
5
|
+
|
6
|
+
module MasterDataTool
|
7
|
+
class MasterDataStatus < ::ActiveRecord::Base
|
8
|
+
self.table_name = 'master_data_statuses'
|
9
|
+
|
10
|
+
validates :name,
|
11
|
+
presence: true
|
12
|
+
|
13
|
+
validates :version,
|
14
|
+
presence: true
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def build(csv_path)
|
18
|
+
version = decide_version(csv_path)
|
19
|
+
new(name: MasterDataTool.resolve_table_name(csv_path), version: version)
|
20
|
+
end
|
21
|
+
|
22
|
+
def import_records!(records, dry_run: true)
|
23
|
+
if dry_run
|
24
|
+
pp records
|
25
|
+
else
|
26
|
+
import!(records, validate: true, on_duplicate_key_update: %w[name version], timestamps: true)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def master_data_will_change?(csv_path)
|
31
|
+
new_version = decide_version(csv_path)
|
32
|
+
!where(name: MasterDataTool.resolve_table_name(csv_path), version: new_version).exists?
|
33
|
+
end
|
34
|
+
|
35
|
+
def decide_version(csv_path)
|
36
|
+
OpenSSL::Digest::SHA256.hexdigest(File.open(csv_path).read)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MasterDataTool
|
4
|
+
module Report
|
5
|
+
module Core
|
6
|
+
def initialize(master_data)
|
7
|
+
@master_data = master_data
|
8
|
+
end
|
9
|
+
|
10
|
+
def print(printer)
|
11
|
+
raise NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def convert_to_ltsv(items)
|
17
|
+
items.map { |k, v| "#{k}:#{v}" }.join("\t")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MasterDataTool
|
4
|
+
module Report
|
5
|
+
class ImportReport
|
6
|
+
include Core
|
7
|
+
|
8
|
+
attr_reader :reports
|
9
|
+
|
10
|
+
def initialize(master_data)
|
11
|
+
super(master_data)
|
12
|
+
@reports = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def print(printer)
|
16
|
+
reports.each do |_, report|
|
17
|
+
if report.is_a?(Array)
|
18
|
+
report.each { |r| printer.print(convert_to_ltsv(r)) }
|
19
|
+
else
|
20
|
+
printer.print(convert_to_ltsv(report))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def reports
|
26
|
+
@reports ||= count_report.merge(new_records_report, updated_records_report, no_change_records_report, deleted_records_report)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def count_report
|
32
|
+
label = :count_report
|
33
|
+
{}.tap do |report|
|
34
|
+
report[label] = []
|
35
|
+
report[label] << {operation: :import, label: :count, table_name: @master_data.table_name, before: @master_data.before_count, after: @master_data.after_count}
|
36
|
+
report[label] << {operation: :import, label: :affected, table_name: @master_data.table_name, affected: @master_data.affected?}
|
37
|
+
report[label] << {operation: :import, label: :new_count, table_name: @master_data.table_name, count: @master_data.new_records.count}
|
38
|
+
report[label] << {operation: :import, label: :updated_count, table_name: @master_data.table_name, count: @master_data.updated_records.count}
|
39
|
+
report[label] << {operation: :import, label: :no_change_count, table_name: @master_data.table_name, count: @master_data.no_change_records.count}
|
40
|
+
report[label] << {operation: :import, label: :deleted_count, table_name: @master_data.table_name, count: @master_data.deleted_records.count}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def new_records_report
|
45
|
+
label = :new_records_report
|
46
|
+
{}.tap do |report|
|
47
|
+
report[label] = []
|
48
|
+
@master_data.new_records.each do |record|
|
49
|
+
report[label] << {operation: :import, label: :detail, table_name: @master_data.table_name, status: :new, id: record.id}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def updated_records_report
|
55
|
+
label = :updated_records_report
|
56
|
+
{}.tap do |report|
|
57
|
+
report[label] = []
|
58
|
+
@master_data.updated_records.each do |record|
|
59
|
+
report[label] << {operation: :import, label: :detail, table_name: @master_data.table_name, status: :updated, id: record.id, detail: record.changes_to_save}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def no_change_records_report
|
65
|
+
label = :no_change_records_report
|
66
|
+
{}.tap do |report|
|
67
|
+
report[label] = []
|
68
|
+
@master_data.no_change_records.each do |record|
|
69
|
+
report[label] << {operation: :import, label: :detail, table_name: @master_data.table_name, status: :no_change, id: record.id}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def deleted_records_report
|
75
|
+
label = :deleted_records_report
|
76
|
+
{}.tap do |report|
|
77
|
+
report[label] = []
|
78
|
+
@master_data.deleted_records.each do |record|
|
79
|
+
report[label] << {operation: :import, label: :detail, table_name: @master_data.table_name, status: :deleted, id: record.id}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MasterDataTool
|
4
|
+
module Report
|
5
|
+
class PrintAffectedTableReport
|
6
|
+
include Core
|
7
|
+
|
8
|
+
def initialize(master_data)
|
9
|
+
super(master_data)
|
10
|
+
@reports = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def print(printer)
|
14
|
+
printer.print(convert_to_ltsv({operation: :affected_table, table_name: @master_data.table_name}))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MasterDataTool
|
4
|
+
module Report
|
5
|
+
class VerifyReport
|
6
|
+
include Core
|
7
|
+
|
8
|
+
attr_reader :reports
|
9
|
+
|
10
|
+
def initialize(master_data)
|
11
|
+
super(master_data)
|
12
|
+
@reports = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def append(verify_record_report)
|
16
|
+
@reports << verify_record_report
|
17
|
+
end
|
18
|
+
|
19
|
+
def print(printer)
|
20
|
+
@reports.each do |report|
|
21
|
+
printer.print(convert_to_ltsv(report))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def build_verify_record_report(master_data, record, valid)
|
27
|
+
{operation: :verify, table_name: master_data.table_name, valid: valid, id: record.id}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "report/printer"
|
4
|
+
require_relative "report/default_printer"
|
5
|
+
require_relative "report/core"
|
6
|
+
require_relative "report/import_report"
|
7
|
+
require_relative "report/verify_report"
|
8
|
+
require_relative "report/print_affected_table_report"
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'csv'
|
4
|
+
require_relative "master_data_tool/version"
|
5
|
+
require_relative "master_data_tool/config"
|
6
|
+
require_relative "master_data_tool/master_data_status"
|
7
|
+
require_relative "master_data_tool/master_data"
|
8
|
+
require_relative "master_data_tool/report"
|
9
|
+
require_relative "master_data_tool/dump/executor"
|
10
|
+
require_relative "master_data_tool/import"
|
11
|
+
|
12
|
+
module MasterDataTool
|
13
|
+
class Error < StandardError; end
|
14
|
+
class DryRunError < StandardError; end
|
15
|
+
class VerifyFailed < StandardError; end
|
16
|
+
class NotLoadedError < StandardError; end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def config
|
20
|
+
@config ||= Config.default_config
|
21
|
+
end
|
22
|
+
|
23
|
+
def configure
|
24
|
+
yield config
|
25
|
+
end
|
26
|
+
|
27
|
+
def resolve_table_name(csv_path)
|
28
|
+
csv_path.relative_path_from(config.master_data_dir).to_s.delete_suffix('.csv')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: master_data_tool
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Takahiro Ooishi
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-02-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec-rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 5.2.6.2
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 5.2.6.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mysql2
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: psych
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.1'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rails
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 5.1.7
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 5.1.7
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: thor
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: activerecord-import
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: システムが稼働する上で最初から必要なデータ(マスタデータ)を管理するツールです。
|
112
|
+
email:
|
113
|
+
- taka0125@gmail.com
|
114
|
+
executables:
|
115
|
+
- master_data_tool
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- ".rspec"
|
120
|
+
- Gemfile
|
121
|
+
- README.md
|
122
|
+
- Rakefile
|
123
|
+
- bin/console
|
124
|
+
- bin/setup
|
125
|
+
- exe/master_data_tool
|
126
|
+
- lib/generators/master_data_tool/install/install_generator.rb
|
127
|
+
- lib/generators/master_data_tool/install/templates/create_master_data_statuses.rb.erb
|
128
|
+
- lib/master_data_tool.rb
|
129
|
+
- lib/master_data_tool/config.rb
|
130
|
+
- lib/master_data_tool/dump.rb
|
131
|
+
- lib/master_data_tool/dump/executor.rb
|
132
|
+
- lib/master_data_tool/import.rb
|
133
|
+
- lib/master_data_tool/import/executor.rb
|
134
|
+
- lib/master_data_tool/master_data.rb
|
135
|
+
- lib/master_data_tool/master_data_status.rb
|
136
|
+
- lib/master_data_tool/report.rb
|
137
|
+
- lib/master_data_tool/report/core.rb
|
138
|
+
- lib/master_data_tool/report/default_printer.rb
|
139
|
+
- lib/master_data_tool/report/import_report.rb
|
140
|
+
- lib/master_data_tool/report/print_affected_table_report.rb
|
141
|
+
- lib/master_data_tool/report/printer.rb
|
142
|
+
- lib/master_data_tool/report/verify_report.rb
|
143
|
+
- lib/master_data_tool/version.rb
|
144
|
+
- sig/master_data_tool.rbs
|
145
|
+
homepage: https://github.com/taka0125/master_data_tool
|
146
|
+
licenses: []
|
147
|
+
metadata:
|
148
|
+
homepage_uri: https://github.com/taka0125/master_data_tool
|
149
|
+
source_code_uri: https://github.com/taka0125/master_data_tool
|
150
|
+
post_install_message:
|
151
|
+
rdoc_options: []
|
152
|
+
require_paths:
|
153
|
+
- lib
|
154
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: 2.6.0
|
159
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
160
|
+
requirements:
|
161
|
+
- - ">="
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
requirements: []
|
165
|
+
rubygems_version: 3.0.3
|
166
|
+
signing_key:
|
167
|
+
specification_version: 4
|
168
|
+
summary: マスタデータの管理ツール
|
169
|
+
test_files: []
|