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 CHANGED
@@ -1,16 +1,230 @@
1
1
  = Trucker
2
2
 
3
- Trucker is a gem for migrating legacy data into a Rails app.
3
+ Trucker is a gem that helps migrate legacy data into a Rails app.
4
4
 
5
- == Note on Patches/Pull Requests
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 I don't break it in a
10
- future version unintentionally.
11
- * Commit, do not mess with rakefile, version, or history.
12
- (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
- * Send me a pull request. Bonus points for topic branches.
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
@@ -1,5 +1,5 @@
1
1
  ---
2
- :major: 0
2
+ :minor: 2
3
3
  :build:
4
- :minor: 1
5
4
  :patch: 0
5
+ :major: 0
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: 27
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 1
8
+ - 2
9
9
  - 0
10
- version: 0.1.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
-