trucker 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +221 -7
- data/VERSION.yml +2 -2
- metadata +3 -5
- data/BACKGROUND.markdown +0 -236
- data/INSTALL.markdown +0 -30
data/README.rdoc
CHANGED
@@ -1,16 +1,230 @@
|
|
1
1
|
= Trucker
|
2
2
|
|
3
|
-
Trucker is a gem
|
3
|
+
Trucker is a gem that helps migrate legacy data into a Rails app.
|
4
4
|
|
5
|
-
==
|
5
|
+
== Installation
|
6
|
+
|
7
|
+
|
8
|
+
1. Install the trucker gem
|
9
|
+
|
10
|
+
sudo gem install trucker
|
11
|
+
|
12
|
+
2. Add trucker to your <em>config.gem</em> block in environment.rb
|
13
|
+
|
14
|
+
config.gem "trucker"
|
15
|
+
|
16
|
+
2. Generate the basic trucker files
|
17
|
+
|
18
|
+
script/generate truck
|
19
|
+
|
20
|
+
This will do the following things:
|
21
|
+
* Add legacy adapter to database.yml
|
22
|
+
* Add legacy base class
|
23
|
+
* Add legacy sub classes for all existing models
|
24
|
+
* Add app/models/legacy to load path in Rails Initializer config block
|
25
|
+
* Generate sample migration task (using pluralized model names)
|
26
|
+
|
27
|
+
3. Update the legacy database adapter in <em>database.yml</em> with your legacy database info
|
28
|
+
|
29
|
+
legacy:
|
30
|
+
adapter: mysql
|
31
|
+
encoding: utf8
|
32
|
+
database: app_legacy
|
33
|
+
username: root
|
34
|
+
password:
|
35
|
+
|
36
|
+
(By convention, we recommend naming your legacy database APP_NAME_legacy.)
|
37
|
+
|
38
|
+
4. If the legacy database doesn't already exist, add it.
|
39
|
+
|
40
|
+
rake db:create:all
|
41
|
+
|
42
|
+
5. Import your legacy data into the legacy database
|
43
|
+
|
44
|
+
mysql -u root app_legacy < old_database.sql
|
45
|
+
|
46
|
+
6. Update <em>set_table_name</em> in each of your legacy models as needed
|
47
|
+
|
48
|
+
class LegacyPost < LegacyBase
|
49
|
+
set_table_name "LEGACY_TABLE_NAME_GOES_HERE"
|
50
|
+
end
|
51
|
+
|
52
|
+
7. Update legacy model field mappings as needed
|
53
|
+
|
54
|
+
class LegacyPost < LegacyBase
|
55
|
+
set_table_name "YOUR LEGACY TABLE NAME GOES HERE"
|
56
|
+
|
57
|
+
def map
|
58
|
+
{
|
59
|
+
:headline => self.title.squish,
|
60
|
+
:body => self.long_text.squish
|
61
|
+
}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
Non-legacy model attributes on the left side.
|
66
|
+
Legacy model attributes on the right side.
|
67
|
+
(aka :new_field => legacy_field)
|
68
|
+
|
69
|
+
8. Need to tweak some data? Just add some core ruby methods or add a helper method.
|
70
|
+
|
71
|
+
class LegacyPost < LegacyBase
|
72
|
+
set_table_name "YOUR LEGACY TABLE NAME GOES HERE"
|
73
|
+
|
74
|
+
def map
|
75
|
+
{
|
76
|
+
:headline => self.title.squish.capitalize,
|
77
|
+
:body => self.long_text.squish
|
78
|
+
}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class LegacyPost < LegacyBase
|
83
|
+
set_table_name "YOUR LEGACY TABLE NAME GOES HERE"
|
84
|
+
|
85
|
+
def map
|
86
|
+
{
|
87
|
+
:headline => tweak_title(self.title.squish),
|
88
|
+
:body => self.long_text.squish
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def tweak_title(title)
|
93
|
+
title = title.capitalize
|
94
|
+
title = title.gsub(/teh/, "the")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
9. Start migrating!
|
99
|
+
|
100
|
+
rake db:migrate:posts
|
101
|
+
|
102
|
+
|
103
|
+
== Migration command line options
|
104
|
+
|
105
|
+
Trucker supports a few command line options when migrating records:
|
106
|
+
|
107
|
+
rake db:migrate:posts limit=100 (migrates 100 records)
|
108
|
+
rake db:migrate:posts limit=100 offset=100 (migrates 100 records, but skip the first 100 records)
|
109
|
+
|
110
|
+
|
111
|
+
== Custom migration labels
|
112
|
+
|
113
|
+
You can tweak the default migration output generated by Trucker by using the :label option
|
114
|
+
|
115
|
+
rake db:migrate:posts
|
116
|
+
=> Migrating posts
|
117
|
+
|
118
|
+
rake db:migrate:posts, :label => "blog posts"
|
119
|
+
=> Migrating blog posts
|
120
|
+
|
121
|
+
|
122
|
+
== Custom helpers
|
123
|
+
|
124
|
+
Trucker is intended for migrating data from fairly simple web apps that started life on PHP, Perl, etc. So, if you're migrating data from an enterprise system, this may not be your best choice.
|
125
|
+
|
126
|
+
That said, if you need to pull off a complex migration for a model, you can use a custom helper method to override Trucker's default migrate method in your rake task.
|
127
|
+
|
128
|
+
namespace :db do
|
129
|
+
namespace :migrate do
|
130
|
+
|
131
|
+
...
|
132
|
+
|
133
|
+
desc 'Migrate pain_in_the_ass model'
|
134
|
+
task :pain_in_the_ass => :environment do
|
135
|
+
Trucker.migrate :pain_in_the_ass, :helper => pain_in_the_ass_migration
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def pain_in_the_ass_migration
|
142
|
+
# Custom code goes here
|
143
|
+
end
|
144
|
+
|
145
|
+
Then just copy the migrate method from lib/trucker.rb and tweak accordingly.
|
146
|
+
|
147
|
+
As an example, here's a custom helper used to migrate join tables on a bunch of models.
|
148
|
+
|
149
|
+
namespace :db do
|
150
|
+
namespace :migrate do
|
151
|
+
|
152
|
+
desc 'Migrates join tables'
|
153
|
+
task :joins => :environment do
|
154
|
+
migrate :joins, :helper => :migrate_joins
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def migrate_joins
|
161
|
+
puts "Migrating #{number_of_records || "all"} joins #{"after #{offset_for_records}" if offset_for_records}"
|
162
|
+
|
163
|
+
["chain", "firm", "function", "style", "website"].each do |model|
|
164
|
+
|
165
|
+
# Start migration
|
166
|
+
puts "Migrating theaters_#{model.pluralize}"
|
167
|
+
|
168
|
+
# Delete existing joins
|
169
|
+
ActiveRecord::Base.connection.execute("TRUNCATE table theaters_#{model.pluralize}")
|
170
|
+
|
171
|
+
# Tweak model ids and foreign keys to match model syntax
|
172
|
+
if model == 'website'
|
173
|
+
model_id = "url_id"
|
174
|
+
send_foreign_key = "url_id".to_sym
|
175
|
+
else
|
176
|
+
model_id = "#{model}_id"
|
177
|
+
send_foreign_key = "#{model}_id".to_sym
|
178
|
+
end
|
179
|
+
|
180
|
+
# Create join object class
|
181
|
+
join = Object.const_set("Theaters#{model.classify}", Class.new(ActiveRecord::Base))
|
182
|
+
|
183
|
+
# Set model foreign key
|
184
|
+
model_foreign_key = "#{model}_id".to_sym
|
185
|
+
|
186
|
+
# Migrate join (unless duplicate)
|
187
|
+
"LegacyTheater#{model.classify}".constantize.find(:all, with(:order => model_id)).each do |record|
|
188
|
+
|
189
|
+
unless join.find(:first, :conditions => {:theater_id => record.theater_id, model_foreign_key => record.send(send_foreign_key)})
|
190
|
+
attributes = {
|
191
|
+
model_foreign_key => record.send(send_foreign_key),
|
192
|
+
:theater_id => record.theater_id
|
193
|
+
}
|
194
|
+
|
195
|
+
# Check if theater chain is current
|
196
|
+
attributes[:is_current] = {'Yes' => 1, 'No' => 0, '' => 0}[record.current] if model == 'chain'
|
197
|
+
|
198
|
+
# Migrate join
|
199
|
+
join.create(attributes)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
== Background
|
207
|
+
|
208
|
+
Trucker is based on a migration technique using legacy models first pioneered by Dave Thomas:
|
209
|
+
http://pragdave.blogs.pragprog.com/pragdave/2006/01/sharing_externa.html
|
210
|
+
|
211
|
+
|
212
|
+
== Note on patches/pull requests
|
6
213
|
|
7
214
|
* Fork the project.
|
8
215
|
* Make your feature addition or bug fix.
|
9
|
-
* Add tests for it. This is important so
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
216
|
+
* Add tests for it. This is important so we don't break a future version unintentionally.
|
217
|
+
* Commit your changes, but do not mess with the rakefile, version, or history.
|
218
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself so we can ignore when we pull)
|
219
|
+
* Send a pull request. Bonus points for topic branches.
|
220
|
+
|
221
|
+
|
222
|
+
== Contributors
|
223
|
+
|
224
|
+
* Patrick Crowley
|
225
|
+
* Rob Kaufman
|
226
|
+
* Jordan Fowler
|
227
|
+
|
14
228
|
|
15
229
|
== Copyright
|
16
230
|
|
data/VERSION.yml
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: trucker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 2
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Patrick Crowley and Rob Kaufman
|
@@ -58,8 +58,6 @@ extra_rdoc_files:
|
|
58
58
|
- LICENSE
|
59
59
|
- README.rdoc
|
60
60
|
files:
|
61
|
-
- BACKGROUND.markdown
|
62
|
-
- INSTALL.markdown
|
63
61
|
- LICENSE
|
64
62
|
- README.rdoc
|
65
63
|
- Rakefile
|
data/BACKGROUND.markdown
DELETED
@@ -1,236 +0,0 @@
|
|
1
|
-
Background
|
2
|
-
==========
|
3
|
-
|
4
|
-
Trucker is based on a migration technique using LegacyModels first pioneered by Dave Thomas.
|
5
|
-
|
6
|
-
Sharing External ActiveRecord Connections
|
7
|
-
http://pragdave.blogs.pragprog.com/pragdave/2006/01/sharing_externa.html
|
8
|
-
|
9
|
-
Using this, I've developed a set of helpers for migrating code.
|
10
|
-
|
11
|
-
- /app/models/legacy/
|
12
|
-
- /app/models/legacy/legacy_base.rb
|
13
|
-
- /app/models/legacy/legacy_model.rb
|
14
|
-
- /config/database.yml
|
15
|
-
- /config/environment.rb
|
16
|
-
- /lib/migration_helper.rb
|
17
|
-
- /lib/tasks/migrate.rake
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
/app/models/legacy/
|
22
|
-
===================
|
23
|
-
|
24
|
-
This folder will contain the base Legacy model, and all subclasses.
|
25
|
-
|
26
|
-
|
27
|
-
/app/models/legacy/legacy_base.rb
|
28
|
-
=================================
|
29
|
-
|
30
|
-
This is the base Legacy model which connects to the legacy database and handles the migration.
|
31
|
-
|
32
|
-
class LegacyBase < ActiveRecord::Base
|
33
|
-
self.abstract_class = true
|
34
|
-
establish_connection "legacy"
|
35
|
-
|
36
|
-
def migrate
|
37
|
-
new_record = self.class.to_s.gsub(/Legacy/,'::').constantize.new(map)
|
38
|
-
new_record[:id] = self.id
|
39
|
-
new_record.save
|
40
|
-
end
|
41
|
-
|
42
|
-
end
|
43
|
-
|
44
|
-
|
45
|
-
/app/models/legacy/legacy_model.rb
|
46
|
-
=================================
|
47
|
-
|
48
|
-
This is a sample Legacy subclass, which specifies the legacy model name and defines a map of old field names to new field names. All Legacy models are stored in /app/models/legacy to keep your main app model namespace unaffected.
|
49
|
-
|
50
|
-
class LegacyModel < LegacyBase
|
51
|
-
set_table_name "model"
|
52
|
-
|
53
|
-
def map
|
54
|
-
{
|
55
|
-
:make => self.car_company.squish,
|
56
|
-
:model => self.car_name.squish
|
57
|
-
}
|
58
|
-
end
|
59
|
-
|
60
|
-
end
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
/config/environment.rb
|
65
|
-
======================
|
66
|
-
|
67
|
-
We need to update the app environment so we can load the legacy models correctly.
|
68
|
-
|
69
|
-
Rails::Initializer.run do |config|
|
70
|
-
config.load_paths += %W( #{RAILS_ROOT}/app/models/legacy )
|
71
|
-
end
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
/config/database.yml
|
76
|
-
====================
|
77
|
-
|
78
|
-
We need to add a custom database adapter so that we can connect to our legacy database.
|
79
|
-
|
80
|
-
By convention, I've used APPNAME_legacy for my legacy databases, but you can easily customize this.
|
81
|
-
|
82
|
-
legacy:
|
83
|
-
adapter: mysql
|
84
|
-
database: APPNAME_legacy
|
85
|
-
username: root
|
86
|
-
password:
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
/app/models/legacy/legacy_base.rb
|
91
|
-
=================================
|
92
|
-
|
93
|
-
This model connects to our legacy database, and provides a migration method.
|
94
|
-
|
95
|
-
class LegacyBase < ActiveRecord::Base
|
96
|
-
self.abstract_class = true
|
97
|
-
establish_connection "legacy"
|
98
|
-
|
99
|
-
def migrate
|
100
|
-
new_record = self.class.to_s.gsub(/Legacy/,'::').constantize.new(map)
|
101
|
-
new_record[:id] = self.id
|
102
|
-
new_record.save
|
103
|
-
end
|
104
|
-
|
105
|
-
end
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
/lib/migration_helper.rb
|
110
|
-
========================
|
111
|
-
|
112
|
-
This helper is used by the rake task to manage the actual migration process.
|
113
|
-
|
114
|
-
def migrate(name, options={})
|
115
|
-
# Grab custom entity label if present
|
116
|
-
label = options.delete(:label) if options[:label]
|
117
|
-
|
118
|
-
unless options[:helper]
|
119
|
-
|
120
|
-
# Grab model to migrate
|
121
|
-
model = name.to_s.singularize.capitalize
|
122
|
-
|
123
|
-
# Wipe out existing records
|
124
|
-
model.constantize.delete_all
|
125
|
-
|
126
|
-
# Status message
|
127
|
-
status = "Migrating "
|
128
|
-
status += "#{number_of_records || "all"} #{label || name}"
|
129
|
-
status += " after #{offset_for_records}" if offset_for_records
|
130
|
-
|
131
|
-
# Set import counter
|
132
|
-
counter = 0
|
133
|
-
counter += offset_for_records if offset_for_records
|
134
|
-
total_records = "Legacy#{model}".constantize.find(:all).size
|
135
|
-
|
136
|
-
# Start import
|
137
|
-
"Legacy#{model}".constantize.find(:all, with(options)).each do |record|
|
138
|
-
counter += 1
|
139
|
-
puts status + " (#{counter}/#{total_records})"
|
140
|
-
record.migrate
|
141
|
-
end
|
142
|
-
else
|
143
|
-
eval options[:helper].to_s
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
def with(options={})
|
148
|
-
{:limit => number_of_records, :offset => offset_for_records}.merge(options)
|
149
|
-
end
|
150
|
-
|
151
|
-
def number_of_records
|
152
|
-
nil || ENV['limit'].to_i if ENV['limit'].to_i > 0
|
153
|
-
end
|
154
|
-
|
155
|
-
def offset_for_records
|
156
|
-
nil || ENV['offset'].to_i if ENV['offset'].to_i > 0
|
157
|
-
end
|
158
|
-
|
159
|
-
Available options include offset and limit:
|
160
|
-
|
161
|
-
rake db:migrate:architects limit=1000
|
162
|
-
rake db:migrate:architects limit=1000 offset=2000
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
/lib/tasks/migrate.rake
|
167
|
-
========================
|
168
|
-
|
169
|
-
This is the basic rake task for migrating legacy data. With a sample model, just add a new migration task with the pluralized name of your existing model. For more complicated migrations, the migrate method supports a helper method which will override the default migration behavior and allow you to do a highly customized migration.
|
170
|
-
|
171
|
-
require 'migration_helper'
|
172
|
-
|
173
|
-
namespace :db do
|
174
|
-
namespace :migrate do
|
175
|
-
|
176
|
-
desc 'Migrates architects'
|
177
|
-
task :architects => :environment do
|
178
|
-
migrate :architects
|
179
|
-
end
|
180
|
-
|
181
|
-
desc 'Migrates theaters'
|
182
|
-
task :architects => :environment do
|
183
|
-
migrate :theaters, :helper => :migrate_theaters
|
184
|
-
end
|
185
|
-
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
def migrate_theaters
|
190
|
-
|
191
|
-
# Delete all theaters if delete_all=true
|
192
|
-
Theater.delete_all if ENV['delete_all']
|
193
|
-
|
194
|
-
# Set default conditions
|
195
|
-
conditions = ["status_publish = 'Yes' AND location_address1 != '' AND location_address1 IS NOT NULL"]
|
196
|
-
|
197
|
-
# Set counters to monitor migration
|
198
|
-
success, failure, skipped = 0, 0, 0
|
199
|
-
|
200
|
-
# Count number of theaters
|
201
|
-
start = Theater.count
|
202
|
-
|
203
|
-
# Migrate theaters
|
204
|
-
puts "\nMigrating #{number_of_records || "all"} theaters #{"after #{offset_for_records}\n\n" if offset_for_records}"
|
205
|
-
LegacyTheater.find(:all, with(:conditions => conditions)).each_with_index do |record, i|
|
206
|
-
|
207
|
-
# Migrate theater
|
208
|
-
new_record = record.migrate
|
209
|
-
message = "#{new_record.name} (#{record.id})"
|
210
|
-
|
211
|
-
if Theater.exists?(record.id)
|
212
|
-
puts "#{i+1} SKIP #{message}\n\n"
|
213
|
-
skipped += 1
|
214
|
-
elsif new_record.save
|
215
|
-
puts "#{i+1} PASS #{message}\n\n"
|
216
|
-
success += 1
|
217
|
-
else
|
218
|
-
puts "#{i+1} FAIL #{message}\n#{new_record.inspect}\n\n"
|
219
|
-
failure += 1
|
220
|
-
end
|
221
|
-
|
222
|
-
# Archive old theater data
|
223
|
-
archive_old_theater_data(record)
|
224
|
-
|
225
|
-
end
|
226
|
-
|
227
|
-
# Count number of theaters
|
228
|
-
finish = Theater.count
|
229
|
-
|
230
|
-
# Batch stats
|
231
|
-
percentage = (failure.to_f / (skipped + success + failure).to_f).to_f * 100
|
232
|
-
puts "BATCH: #{number_of_records || "all"} theaters => #{success} passed, #{failure} failed (#{percentage.truncate}%), #{skipped} skipped (already imported)"
|
233
|
-
|
234
|
-
end
|
235
|
-
|
236
|
-
|
data/INSTALL.markdown
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
|
2
|
-
Here are some imaginary install instructions.
|
3
|
-
|
4
|
-
1. Install the trucker gem and add to your gem config block.
|
5
|
-
|
6
|
-
2. Generate the basic trucker files
|
7
|
-
|
8
|
-
script/generate truck
|
9
|
-
|
10
|
-
- Add legacy adapter to database.yml
|
11
|
-
- Add legacy base class
|
12
|
-
- (Optionally) Add legacy sub classes for all existing models
|
13
|
-
- Add app/models/legacy to load path in Rails Initializer config block
|
14
|
-
- Generate sample migration task (using pluralized model names)
|
15
|
-
|
16
|
-
3. Update database.yml with legacy database info
|
17
|
-
|
18
|
-
4. Run rake db:create:all to create the legacy database
|
19
|
-
|
20
|
-
5. Import your legacy database.
|
21
|
-
(Make sure the legacy adapter is using the correct name.)
|
22
|
-
|
23
|
-
6. Update legacy model table names as needed
|
24
|
-
|
25
|
-
7. Update legacy model field mappings as needed
|
26
|
-
|
27
|
-
8. Start migrating!
|
28
|
-
rake db:migrate:models
|
29
|
-
|
30
|
-
|