File Coverage

blib/lib/Class/DBI/DataMigration/Migrator.pm
Criterion Covered Total %
statement 32 69 46.3
branch 2 26 7.6
condition 0 2 0.0
subroutine 9 13 69.2
pod 3 3 100.0
total 46 113 40.7


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2              
3             =head1 Name
4              
5             Class::DBI::DataMigration::Migrator - Class that does the actual data migration
6             from a source database to a target database.
7              
8             =head1 Synopsis
9              
10             use Class::DBI::DataMigration::Migrator;
11              
12             # Assume we've slurped config.yaml into $yaml (see below for config.yaml contents):
13             my $migrator = new Class::DBI::DataMigration::Migrator($yaml);
14              
15             # Assume that @source_objs_to_migrate is a list of CDBI objects from
16             # the source db that we want to migrate into the target db:
17             my $migrated = $migrator->migrate_objects(\@source_objs_to_migrate);
18              
19             # Target db now contains newly-migrated objects.
20             # Also, $migrated is a hashref to a list of the migrated objects.
21              
22             # ... Meanwhile, in config.yaml:
23             #
24             # This is an example that migrates from the car table in a source database,
25             # called src_db, to the automobile table in a target database, called trg_db.
26             #
27             # The source car table has make, model, model_year and body_colour columns
28             # (body_colour being a has_a relationship to a body_colour table).
29             #
30             # The target automobile table has brand, type, year, and colour columns
31             # corresponding to the respective source columns.
32             #
33             # For mapping between the has_a relationships (body_colour and colour), a
34             # subclass of Mapping, HasAToHasA, is used (see
35             # Class::DBI::DataMigration::Mapping::HasAToHasA.pm for details).
36              
37             ---
38             source_connection:
39             base_class: SourceDB::DBI
40             db_name: dbi:mysql:src_db
41             username: src_uname
42             password: src_pass
43             target_connection:
44             base_class: TargetDB::DBI
45             db_name: dbi:mysql:trg_db
46             username: trg_uname
47             password: trg_pass
48             entities:
49             SourceDB::DBI::Car:
50             mappings:
51             make:
52             target_key: brand
53             model:
54             target_key: type
55             model_year:
56             target_key: year
57             body_colour:
58             target_key: colour
59             mapping:
60             class: Class::DBI::DataMigration::Mapping::HasAToHasA
61             config:
62             target_class: TargetDB::DBI::Colour
63             target_class_search_key: name
64             matching_source_key: body_colour->name
65             target_cdbi_class: TargetDB::DBI::Automobile
66              
67             =cut
68              
69 2     2   540051 use strict;
  2         5  
  2         116  
70              
71             package Class::DBI::DataMigration::Migrator;
72              
73 2     2   11 use base 'Class::Accessor';
  2         5  
  2         174  
74              
75 2     2   1573 use YAML;
  2         18889  
  2         135  
76 2     2   18 use Carp;
  2         4  
  2         258  
77 2     2   2529 use Carp::Assert;
  2         3827  
  2         43  
78              
79             __PACKAGE__->mk_accessors(
80             qw/entities mappers/
81             );
82              
83             =head1 Methods
84              
85             =head2 new
86              
87             my $migrator = Class::DBI::DataMigration::Migrator->new($yaml);
88              
89             Create and initialize a new instance of this class. Expects a YAML
90             configuration string (see example above) that will be used to initialize the
91             new object's source and target database connections, its entities hash, and its
92             mappers.
93              
94             =head2 entities
95              
96             Accessor/mutator for a hashref of hashrefs, describing the ways in which data
97             in entities (tables) in the source database will get migrated by this migrator
98             to data in the entities in the target database.
99              
100             =head2 mappers
101              
102             Accessor/mutator for a hashref of mapper objects (see Class::DBI::Mapper),
103             keyed on the various source database entity classes from which this migrator
104             will migrate data.
105              
106             =cut
107              
108             sub new {
109 1     1 1 78104 my ($class, $yaml) = @_;
110 1         4 my $self = {};
111 1         5 bless $self, $class;
112 1         9 $self->mappers({});
113 1         37 $self->_initialize(Load($yaml));
114 0         0 return $self;
115             }
116              
117             =head2 migrate_objects
118              
119             Expects a reference to a list of source database objects to be migrated.
120             Iterates through the list and calls $self->map() with each source object,
121             collecting and returning a reference to the list of resultant target database
122             objects.
123              
124             =cut
125              
126             sub migrate_objects {
127 0     0 1 0 my ($self, $to_migrate) = @_;
128              
129             # iterate through the list referred to by $to_migrate, mapping each object as we go:
130 0         0 my @migrated = ();
131 0         0 foreach my $obj (@$to_migrate) {
132 0         0 my $newobj = $self->map($obj); # will create a target db object and return it, or return an error string
133 0 0       0 confess $newobj unless (ref($newobj));
134 0         0 push @migrated, $newobj;
135             }
136              
137 0         0 return \@migrated;
138             }
139              
140             =head2 map
141              
142             Given a source database object, looks for a mapper object for that object's
143             class in the mappers hash, and calls map() on it with the source object. Returns
144             the result of that map() call (presumably a target database object), or an error
145             message if no suitable mapper could be found.
146              
147             =cut
148              
149             sub map {
150 0     0 1 0 my ($self, $to_map) = @_;
151              
152             # find appropriate mapper and use it
153 0 0       0 my $mapper = $self->mappers->{ref($to_map)}
154             or return __PACKAGE__ . " couldn't find mapper for object of class: " . ref($to_map);
155 0         0 return $mapper->map($to_map);
156             }
157              
158             sub _initialize {
159 1     1   38291 my ($self, $config) = @_;
160              
161 1 50       10 $self->_initialize_connection($config->{source_connection}) if $config->{source_connection};
162 1 50       5 $self->_initialize_connection($config->{target_connection}) if $config->{target_connection};
163 1         9 $self->entities($config->{entities});
164 1         22 $self->_build_mappers();
165             }
166              
167             sub _initialize_connection {
168 0     0   0 my ($self, $config) = @_;
169              
170 0         0 eval "require $config->{base_class}";
171 0 0       0 carp("Error requiring $config->{base_class}: " . $@) if $@;
172              
173 0 0       0 $config->{base_class}->set_db(
174             'Main',
175             $config->{db_name},
176             $config->{username},
177             $config->{password})
178             unless $config->{base_class}->db_Main; # don't overwrite db connection
179             # if it's already in place
180             }
181              
182             my $DEFAULT_MAPPER = { class => 'Class::DBI::DataMigration::Mapper' };
183              
184             sub _build_mappers {
185 1     1   3 my $self = shift;
186              
187 1         2 foreach my $key (keys %{$self->entities}) {
  1         6  
188             # Each key should be a cdbi class name in the source db:
189 1         47 __require_once($key);
190              
191 0         0 my $entity = $self->entities->{$key};
192 0 0       0 my $mapper = $self->_build_mapper(
193              
194             ($entity->{mapper} ?
195             $entity->{mapper} :
196             $DEFAULT_MAPPER),
197              
198             $entity->{target_cdbi_class},
199             $entity->{mappings}
200              
201             );
202              
203 0         0 $self->mappers->{$key} = $mapper;
204             }
205             }
206              
207             sub _build_mapper {
208 0     0   0 my ($self, $mapper, $target_cdbi_class, $mappings_hash) = @_;
209 0         0 my $default_mapping_class = 'Class::DBI::DataMigration::Mapping';
210 0         0 my $target_keys = {};
211 0         0 my $mappings = {};
212              
213 0 0       0 Carp::Assert::should(ref($mappings_hash), 'HASH')
214             if $Carp::Assert::DEBUG;
215              
216 0         0 while (my ($key, $hash) = each %$mappings_hash) {
217 0         0 $target_keys->{$key} = $hash->{target_key};
218              
219 0 0       0 if (my $mapping_hash = $hash->{mapping}) {
220              
221 0 0       0 my $mapping_class = (exists $mapping_hash->{class}) ?
222             $mapping_hash->{class} :
223             $default_mapping_class;
224 0         0 __require_once($mapping_class);
225              
226             # In the default case, there will be no config, but the base
227             # Mapping class doesn't need any, so that's ok:
228 0         0 $mappings->{$key} = $mapping_class->new($mapping_hash->{config});
229             } else {
230 0         0 __require_once($default_mapping_class);
231 0         0 $mappings->{$key} = $default_mapping_class->new;
232             }
233             }
234              
235 0 0       0 $mapper->{class} = $DEFAULT_MAPPER->{class} unless $mapper->{class};
236 0         0 __require_once($mapper->{class});
237 0         0 return $mapper->{class}->new({
238             target_cdbi_class => $target_cdbi_class,
239             target_keys => $target_keys,
240             target_search_keys => $mapper->{target_search_keys},
241             mappings => $mappings
242             });
243             }
244              
245             sub __require_once {
246             # only require a package if it's not already loaded
247             # (useful for not require'ing modules that have been dynamically
248             # generated, and whose .pm files don't actually exist):
249 1     1   3 my $pkg = shift;
250 1 0       4 unless (defined %{"$pkg\::"}) {
  1         27  
251 0           eval "require $pkg";
252 0 0 0       carp $@ and return 0 if $@;
253             }
254              
255 0           return 1;
256             }
257              
258             =begin testing
259              
260             use lib 't/testlib';
261              
262             use YAML;
263             use Class::DBI::Loader;
264              
265             use DMTestSetup; # set up empty test source/target db's and associated CDBI classes
266              
267             use_ok('Class::DBI::DataMigration::Migrator');
268             can_ok('Class::DBI::DataMigration::Migrator', 'entities');
269             can_ok('Class::DBI::DataMigration::Migrator', 'mappers');
270              
271             # Start filling source data:
272              
273             my $source_grey = SourceDB::BodyColour->create({
274             name => 'grey'
275             });
276              
277             my $target_grey = TargetDB::Colour->create({
278             name => 'grey'
279             });
280              
281             # Build a reference list to check the migrated data against later:
282              
283             my @reference = (
284              
285             {
286             make => 'Chevrolet',
287             model => 'Caprice Classic',
288             model_year => '1989',
289             body_colour => $source_grey
290             },
291              
292             {
293             make => 'Jaguar',
294             model => 'XJS',
295             model_year => '1959',
296             body_colour => $source_grey
297             },
298              
299             {
300             make => 'Plymouth',
301             model => 'Reliant',
302             model_year => '1983',
303             body_colour => $source_grey
304             }
305              
306             );
307              
308              
309             foreach (@reference) {
310             SourceDB::Car->create($_);
311             }
312              
313             # simulate a config file that maps between our source and target db's (we don't
314             # need to supply connection info -- see DMTestBase.pm):
315              
316             my $yaml = <<'...';
317             ---
318             entities:
319             SourceDB::Car:
320             mappings:
321             make:
322             target_key: brand
323             model:
324             target_key: type
325             model_year:
326             target_key: year
327             body_colour:
328             target_key: colour
329             mapping:
330             class: Class::DBI::DataMigration::Mapping::HasAToHasA
331             config:
332             target_class: TargetDB::Colour
333             target_class_search_key: name
334             matching_source_key: body_colour->name
335             target_cdbi_class: TargetDB::Automobile
336             ...
337              
338             # now do the actual migration:
339              
340             ok(my $migrator = new Class::DBI::DataMigration::Migrator($yaml), 'migrator construction');
341             my @objs = SourceDB::Car->retrieve_all;
342             my $migrated = $migrator->migrate_objects(\@objs);
343              
344             # finally, check the migrated data against our reference list; to do this, we
345             # set up two parallel formatted structures -- arrays of hashes which *should*
346             # have the same keys and values if the data migrated correctly -- and then we
347             # can use eq_set() to check them:
348              
349             @formatted_mig = ();
350             foreach (@$migrated) {
351             my $hashref = {
352             make => $_->brand,
353             model => $_->type,
354             model_year => $_->year,
355             body_colour => $_->colour->name
356             };
357             push @formatted_mig, $hashref;
358             }
359              
360             my @formatted_ref =();
361             foreach (@reference) {
362             my $hashref = {
363             make => $_->{make},
364             model => $_->{model},
365             model_year => $_->{model_year},
366             body_colour => $_->{body_colour}->name
367             };
368             push @formatted_ref, $hashref;
369             }
370              
371             ok(eq_set(\@formatted_ref, \@formatted_mig), 'data migrated correctly') or
372             diag "Data didn't migrate correctly; reference = \n" . Dump(\@formatted_ref) .
373             "\nmigrated = \n" . Dump(\@formatted_mig);
374              
375             =end testing
376              
377             =head1 Author
378              
379             Dan Friedman, C<< >>
380              
381             =head1 Copyright & License
382              
383             Copyright 2004 Dan Friedman, All Rights Reserved.
384              
385             This program is free software; you can redistribute it and/or modify it
386             under the same terms as Perl itself.
387              
388             Please note that these modules are not products of or supported by the
389             employers of the various contributors to the code.
390              
391             =cut
392              
393             1;
394              
395