dekiru-data_migration 1.1.1 → 1.3.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 +4 -4
- data/.vscode/settings.json +5 -0
- data/README.md +44 -1
- data/lib/dekiru/data_migration/migration.rb +31 -2
- data/lib/dekiru/data_migration/operator.rb +4 -1
- data/lib/dekiru/data_migration/version.rb +1 -1
- data/skills/data-migration-script/SKILL.md +17 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b46a5373c84760557f19d957cdc79d285e1c11812090007b4ccc2dfd7d81cb82
|
|
4
|
+
data.tar.gz: a3ba1a45dc98df3ba559adefcfbda99ccab9cc76e64ea83cd94f3f815d5541df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3febccaa7208f1375ef412fcb77c48450d613733900130b6f388e4ae5e099aa0dc4787ab6877cc13769d442cf21ceb21ed058e364423e6a862d49ec3ecc151c9
|
|
7
|
+
data.tar.gz: 6e39a7f6148affaa5e50461e32cf546cde57dcda1e6409a705c6ff617904116c70689d5e71df6233f8fd73e4a430074efbcdfe2c4875b3b8d8adfbaf681633ca
|
data/README.md
CHANGED
|
@@ -74,6 +74,49 @@ end
|
|
|
74
74
|
DemoMigration.run
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
### Batch Processing with `migrate_batch`
|
|
78
|
+
|
|
79
|
+
When you need to update or delete a large number of records efficiently, you can define `migrate_batch` instead of `migrate_record`. It processes records in batches using ActiveRecord's `in_batches`, yielding each batch as an `ActiveRecord::Relation` so you can run a single `update_all` / `delete_all` query per batch. The progress bar advances per batch rather than per record.
|
|
80
|
+
|
|
81
|
+
`migrate_batch` and `migrate_record` are mutually exclusive — if you implement `migrate_batch`, the batch path is used automatically.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# scripts/20230118_deactivate_stale_users.rb
|
|
85
|
+
class DeactivateStaleUsersMigration < Dekiru::DataMigration::Migration
|
|
86
|
+
def migration_targets
|
|
87
|
+
User.where(active: true).where("last_login_at < ?", 1.year.ago)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def migrate_batch(relation)
|
|
91
|
+
relation.update_all(active: false)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
DeactivateStaleUsersMigration.run
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
If you use the block-based `Operator` directly, pass `in_batches` to `each_with_progress`:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# scripts/deactivate_stale_users.rb
|
|
102
|
+
Dekiru::DataMigration::Operator.execute('Deactivate stale users') do
|
|
103
|
+
targets = User.where(active: true).where("last_login_at < ?", 1.year.ago)
|
|
104
|
+
|
|
105
|
+
log "Target user count: #{targets.count}"
|
|
106
|
+
each_with_progress(targets.in_batches) do |batch|
|
|
107
|
+
batch.update_all(active: false)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### Specifying the batch size
|
|
113
|
+
|
|
114
|
+
Pass `batch_size` to `run` to control how many records are fetched per batch. It applies to both paths — `in_batches(of:)` for `migrate_batch` and `find_each(batch_size:)` for `migrate_record`. When omitted, ActiveRecord's default of 1000 is used.
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
DeactivateStaleUsersMigration.run(batch_size: 500)
|
|
118
|
+
```
|
|
119
|
+
|
|
77
120
|
### Testing Migration Classes
|
|
78
121
|
|
|
79
122
|
The class-based approach makes it easy to write unit tests:
|
|
@@ -286,7 +329,7 @@ Available options:
|
|
|
286
329
|
Outputs log messages. Properly handled even during progress bar display.
|
|
287
330
|
|
|
288
331
|
### `find_each_with_progress(scope, options = {}, &block)`
|
|
289
|
-
Executes `find_each` with a progress bar for ActiveRecord scopes.
|
|
332
|
+
Executes `find_each` with a progress bar for ActiveRecord scopes. Pass `batch_size:` in `options` to control how many records are fetched per batch (forwarded to `find_each(batch_size:)`).
|
|
290
333
|
|
|
291
334
|
### `each_with_progress(enum, options = {}, &block)`
|
|
292
335
|
Executes processing with a progress bar for any Enumerable objects.
|
|
@@ -4,10 +4,15 @@ module Dekiru
|
|
|
4
4
|
module DataMigration
|
|
5
5
|
# Base class for data migration with testable method separation
|
|
6
6
|
class Migration
|
|
7
|
+
attr_accessor :batch_size
|
|
8
|
+
|
|
7
9
|
def self.run(options = {})
|
|
8
10
|
migration = new
|
|
9
11
|
title = migration.title
|
|
10
12
|
|
|
13
|
+
options = options.dup
|
|
14
|
+
migration.batch_size = options.delete(:batch_size)
|
|
15
|
+
|
|
11
16
|
Operator.execute(title, options) do
|
|
12
17
|
migration.instance_variable_set(:@operator_context, self)
|
|
13
18
|
migration.migrate
|
|
@@ -24,8 +29,10 @@ module Dekiru
|
|
|
24
29
|
log "Target count: #{targets.count}"
|
|
25
30
|
confirm?
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
if migrate_batch_overridden?
|
|
33
|
+
migrate_in_batches(targets)
|
|
34
|
+
else
|
|
35
|
+
migrate_each_record(targets)
|
|
29
36
|
end
|
|
30
37
|
|
|
31
38
|
log "Migration completed"
|
|
@@ -39,8 +46,30 @@ module Dekiru
|
|
|
39
46
|
raise NotImplementedError, "#{self.class}#migrate_record must be implemented"
|
|
40
47
|
end
|
|
41
48
|
|
|
49
|
+
def migrate_batch(relation)
|
|
50
|
+
raise NotImplementedError, "#{self.class}#migrate_batch must be implemented"
|
|
51
|
+
end
|
|
52
|
+
|
|
42
53
|
private
|
|
43
54
|
|
|
55
|
+
def migrate_in_batches(targets)
|
|
56
|
+
batches = batch_size ? targets.in_batches(of: batch_size) : targets.in_batches
|
|
57
|
+
each_with_progress(batches) do |batch|
|
|
58
|
+
migrate_batch(batch)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def migrate_each_record(targets)
|
|
63
|
+
options = batch_size ? { batch_size: batch_size } : {}
|
|
64
|
+
find_each_with_progress(targets, options) do |record|
|
|
65
|
+
migrate_record(record)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def migrate_batch_overridden?
|
|
70
|
+
self.class.instance_method(:migrate_batch).owner != Dekiru::DataMigration::Migration
|
|
71
|
+
end
|
|
72
|
+
|
|
44
73
|
def confirm?
|
|
45
74
|
if @operator_context
|
|
46
75
|
@operator_context.send(:confirm?)
|
|
@@ -82,7 +82,10 @@ module Dekiru
|
|
|
82
82
|
def find_each_with_progress(target_scope, options = {}, &block)
|
|
83
83
|
# `LocalJumpError: no block given (yield)` が出る場合、 find_each メソッドが enumerator を返していない可能性があります
|
|
84
84
|
# 直接 each_with_progress を使うか、 find_each が enumerator を返すように修正してください
|
|
85
|
-
|
|
85
|
+
options = options.dup
|
|
86
|
+
batch_size = options.delete(:batch_size)
|
|
87
|
+
scope = batch_size ? target_scope.find_each(batch_size: batch_size) : target_scope.find_each
|
|
88
|
+
each_with_progress(scope, options, &block)
|
|
86
89
|
end
|
|
87
90
|
|
|
88
91
|
private
|
|
@@ -29,7 +29,7 @@ bin/rails generate maintenance_script <PascalCaseName>
|
|
|
29
29
|
|
|
30
30
|
### 2. Edit the generated file
|
|
31
31
|
|
|
32
|
-
Implement `migration_targets` and `migrate_record` in the generated file.
|
|
32
|
+
Implement `migration_targets` and `migrate_record` (or `migrate_batch` instead, for bulk-processing a large number of records) in the generated file.
|
|
33
33
|
|
|
34
34
|
### Common Operation Patterns
|
|
35
35
|
|
|
@@ -66,6 +66,21 @@ def migrate_record(record)
|
|
|
66
66
|
end
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
**Bulk update/delete a large number of records (batch processing)**:
|
|
70
|
+
|
|
71
|
+
Implement `migrate_batch` instead of `migrate_record` to receive each batch as an `ActiveRecord::Relation` via `in_batches`, allowing you to run `update_all` / `delete_all` in a single query. Use this to process a large number of records efficiently.
|
|
72
|
+
```ruby
|
|
73
|
+
def migration_targets
|
|
74
|
+
User.where(active: true).where("last_login_at < ?", 1.year.ago)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def migrate_batch(relation)
|
|
78
|
+
relation.update_all(active: false) # one query per batch
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Pass `batch_size` to `run` to change the batch size (default 1000). It applies to both `migrate_batch` (`in_batches`) and `migrate_record` (`find_each`): `MyMigration.run(batch_size: 500)`.
|
|
83
|
+
|
|
69
84
|
## Execution and Verification Commands
|
|
70
85
|
|
|
71
86
|
```bash
|
|
@@ -83,4 +98,5 @@ bin/rails runner "p TargetModel.where(...).count"
|
|
|
83
98
|
|
|
84
99
|
- `purge` deletes synchronously (immediately removes from storage). Use `purge_later` for async deletion
|
|
85
100
|
- `migration_targets` must return an ActiveRecord relation (processed in batches via `find_each`)
|
|
101
|
+
- `migrate_batch`'s `update_all` / `delete_all` issue SQL directly without running callbacks or validations. `updated_at` is not auto-updated either, so include it explicitly if needed
|
|
86
102
|
- Always verify the target record count before running in production
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dekiru-data_migration
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SonicGarden
|
|
@@ -48,6 +48,7 @@ extra_rdoc_files: []
|
|
|
48
48
|
files:
|
|
49
49
|
- ".rspec"
|
|
50
50
|
- ".rubocop.yml"
|
|
51
|
+
- ".vscode/settings.json"
|
|
51
52
|
- CODE_OF_CONDUCT.md
|
|
52
53
|
- LICENSE.txt
|
|
53
54
|
- README.md
|