master_data_tool 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in master_data_tool.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
data/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # MasterDataTool
2
+
3
+ [![Build Status](https://github.com/taka0125/master_data_tool/workflows/Ruby/badge.svg?branch=main)](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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dump/executor"
@@ -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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "import/executor"
@@ -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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MasterDataTool
4
+ module Report
5
+ class DefaultPrinter
6
+ include Printer
7
+
8
+ def print(message)
9
+ MasterDataTool.config.logger.info message
10
+ puts message
11
+ end
12
+ end
13
+ end
14
+ 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MasterDataTool
4
+ module Report
5
+ module Printer
6
+ def print(message)
7
+ raise NotImplementedError
8
+ end
9
+ end
10
+ end
11
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MasterDataTool
4
+ VERSION = "0.1.0"
5
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ module MasterDataTool
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ 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: []