1package DBIx::DBHResolver;
2
3use strict;
4use warnings;
5use parent qw(Class::Accessor::Fast);
6use Carp;
7use Config::Any;
8use Data::Util qw(is_value is_array_ref is_hash_ref is_instance is_invocant);
9use DBI;
10use Hash::Merge::Simple qw(merge);
11use Try::Tiny;
12use UNIVERSAL::require;
13
14use DBIx::DBHResolver::Strategy::Key;
15use DBIx::DBHResolver::Strategy::List;
16use DBIx::DBHResolver::Strategy::Range;
17
18our $VERSION                   = '0.17';
19our $CONFIG                    = +{};
20our $DBI                       = 'DBI';
21our $DBI_CONNECT_METHOD        = 'connect';
22our $DBI_CONNECT_CACHED_METHOD = 'connect_cached';
23
24__PACKAGE__->mk_accessors(qw/_config/);
25
26sub new {
27    shift->SUPER::new(
28        +{ _config => +{} } );
29}
30
31sub config {
32    my ( $proto, $config ) = @_;
33
34    if ( is_instance( $proto, 'DBIx::DBHResolver' ) ) {
35        return $proto->_config unless defined $config;
36        $proto->_config($config);
37    }
38    else {
39        return $CONFIG unless defined $config;
40        $CONFIG = $config;
41    }
42}
43
44sub load {
45    my ( $proto, @files ) = @_;
46    for (@files) {
47        croak $! unless ( -f $_ && -r $_ );
48    }
49    my $config;
50    try {
51        $config = Config::Any->load_files(
52            +{ files => \@files, use_ext => 1, flatten_to_hash => 1, } );
53        $config = merge( @$config{@files} );
54    }
55    catch {
56        croak $_;
57    };
58    $proto->config($config);
59}
60
61sub connect {
62    my ( $proto, $cluster_or_node, $args ) = @_;
63    my $dbh = $DBI->$DBI_CONNECT_METHOD(
64        @{ $proto->connect_info( $cluster_or_node, $args ) }
65          {qw/dsn user password attrs/} )
66      or croak($DBI::errstr);
67    return $dbh;
68}
69
70sub connect_cached {
71    my ( $proto, $cluster_or_node, $args ) = @_;
72    my $dbh = $DBI->$DBI_CONNECT_CACHED_METHOD(
73        @{ $proto->connect_info( $cluster_or_node, $args ) }
74          {qw/dsn user password attrs/} )
75      or croak($DBI::errstr);
76    return $dbh;
77}
78
79sub disconnect_all {
80    my ($proto) = @_;
81
82    my %drivers = DBI->installed_drivers;
83    for my $drh ( values %drivers ) {
84        for my $dbh ( @{ $drh->{ChildHandles} } ) {
85            eval { $dbh->disconnect; };
86        }
87    }
88}
89
90sub connect_info {
91    my ( $proto, $cluster_or_node, $args ) = @_;
92
93    my ($resolved_node) = $proto->resolve( $cluster_or_node, $args );
94    return $proto->node_info($resolved_node);
95}
96
97sub resolve {
98    my ( $proto, $cluster_or_node, $args ) = @_;
99
100    if ( $proto->is_cluster($cluster_or_node) ) {
101        if ( is_hash_ref($args) ) {
102            croak q|args has not 'strategy' field| unless $args->{strategy};
103            my $strategy_class =
104              $proto->_resolve_namespace( $args->{strategy} );
105            my ( $resolved_node, @keys ) =
106              $proto->_ensure_class_loaded($strategy_class)
107              ->resolve( $proto, $cluster_or_node, $args );
108            return $proto->resolve( $resolved_node, \@keys );
109        }
110        else {
111            my $cluster_info = $proto->cluster_info($cluster_or_node);
112            if ( is_array_ref $cluster_info ) {
113                my ( $resolved_node, @keys ) =
114                  DBIx::DBHResolver::Strategy::Key->resolve(
115                    $proto,
116                    $cluster_or_node,
117                    +{
118                        strategy => 'Key',
119                        nodes    => $cluster_info,
120                        key      => $args,
121                    }
122                  );
123                return $proto->resolve( $resolved_node, \@keys );
124            }
125            elsif ( is_hash_ref $cluster_info ) {
126                my $strategy_class =
127                  $proto->_resolve_namespace( $cluster_info->{strategy} );
128                my ( $resolved_node, @keys ) =
129                  $proto->_ensure_class_loaded($strategy_class)
130                  ->resolve( $proto, $cluster_or_node,
131                    +{ %$cluster_info, key => $args, } );
132                return $proto->resolve( $resolved_node, \@keys );
133            }
134        }
135    }
136    elsif ( $proto->is_node($cluster_or_node) ) {
137        my $connect_info = $proto->node_info($cluster_or_node);
138        if ( is_hash_ref($connect_info) ) {
139            return $cluster_or_node;
140        }
141        else {
142            return $connect_info;
143        }
144    }
145    else {
146        croak sprintf( '%s is not defined', $cluster_or_node );
147    }
148}
149
150sub resolve_node_keys {
151    my ( $proto, $cluster_or_node, $keys, $args ) = @_;
152    my %node_keys;
153    for my $key (@$keys) {
154        if ( is_hash_ref $args ) {
155            $args->{strategy} ||= 'Key';
156            $args->{key} = $key;
157        }
158        else {
159            $args = $key;
160        }
161
162        my $resolved_node = $proto->resolve( $cluster_or_node, $args );
163        $node_keys{$resolved_node} ||= [];
164        push( @{ $node_keys{$resolved_node} }, $key );
165    }
166
167    return wantarray ? %node_keys : \%node_keys;
168}
169
170sub cluster_info {
171    my ( $proto, $cluster, $cluster_info ) = @_;
172    if ( defined $cluster_info ) {
173        $proto->config->{clusters}{$cluster} = $cluster_info;
174    }
175    else {
176        return $proto->config->{clusters}{$cluster};
177    }
178}
179
180sub clusters {
181    my ( $proto, $cluster ) = @_;
182    my $cluster_info = $proto->cluster_info($cluster);
183    my @nodes =
184      is_array_ref($cluster_info)
185      ? @$cluster_info
186      : @{ $cluster_info->{nodes} };
187    wantarray ? @nodes : \@nodes;
188}
189
190{
191    no warnings;
192    *cluster = \&clusters;
193}
194
195sub is_cluster {
196    my ( $proto, $cluster ) = @_;
197    exists $proto->config->{clusters}{$cluster} ? 1 : 0;
198}
199
200sub node_info {
201    my ( $proto, $node, $node_info ) = @_;
202    if ( defined $node_info ) {
203        $proto->config->{connect_info}{$node} = $node_info;
204    }
205    else {
206        return $proto->config->{connect_info}{$node};
207    }
208}
209
210sub is_node {
211    my ( $proto, $node ) = @_;
212    exists $proto->config->{connect_info}{$node} ? 1 : 0;
213}
214
215sub _ensure_class_loaded {
216    my ( $proto, $class_name ) = @_;
217    unless ( is_invocant $class_name ) {
218        try {
219            $class_name->require;
220        }
221        catch {
222            croak $_;
223        };
224    }
225    $class_name;
226}
227
228sub _resolve_namespace {
229    my ( $proto, $class_name ) = @_;
230    $class_name = 'Key'
231      if ( defined $class_name && $class_name eq 'Remainder' );
232    $class_name =
233        $class_name =~ /^\+(.+)$/
234      ? $1
235      : join( '::', ( __PACKAGE__, 'Strategy', $class_name ) );
236    $class_name;
237}
238
2391;
240
241=head1 NAME
242
243DBIx::DBHResolver - Resolve database connection on the environment has many database servers.
244
245=head1 SYNOPSIS
246
247  use DBIx::DBHResolver;
248
249  my $r = DBIx::DBHResolver->new;
250  $r->config(+{
251    connect_info => +{
252      main_master => +{
253        dsn => 'dbi:mysql:dbname=main;host=localhost',
254        user => 'master_user', password => '',
255        attrs => +{ RaiseError => 1, AutoCommit => 0, },
256      },
257      main_slave => +{
258        dsn => 'dbi:mysql:dbname=main;host=localhost',
259        user => 'slave_user', password => '',
260        attrs => +{ RaiseError => 1, AutoCommit => 1, },
261      }
262    },
263  });
264
265  my $dbh_master = $r->connect('main_master');
266  $dbh_master->do( 'UPDATE people SET ...', undef, ... );
267
268  my $dbh_slave = $r->connect('main_slave');
269  my $people = $dbh_slave->selectrow_hashref( 'SELECT * FROM people WHERE id = ?', undef, 20 );
270
271=head1 DESCRIPTION
272
273DBIx::DBHResolver resolves database connection on the environment has many database servers.
274The resolution algorithm is extensible and pluggable, because of this you can make custom strategy module easily.
275
276This module can retrieve L<DBI>'s database handle object or connection information (data source, user, credential...) by labeled name
277and treat same cluster consists many nodes as one labeled name, choose fetching strategy.
278
279DBIx::DBHResolver is able to use as instance or static class.
280
281=head2 USING STRATEGY, MAKING CUSTOM STRATEGY
282
283See L<DBIx::DBHResolver::Strategy::Key>.
284
285=head2 connect_info format
286
287B<connect_info> is node information to connect it. Following fields are recognized.
288
289  my $connect_info = +{
290    dsn => 'dbi:mysql:db=test',
291    user => 'root',
292    password => '',
293    attrs => +{ RaiseError => 1, AutoCommit => 0 },
294    opts => +{},
295  };
296
297=over
298
299=item dsn
300
301string value. dsn is connection information used by L<DBI>'s connect() method.
302
303=item user
304
305string value. user is database access user used by L<DBI>'s connect() method.
306
307=item password
308
309string value. user is database access password used by L<DBI>'s connect() method.
310
311=item attrs
312
313hash reference value. attrs is optional parameter used by L<DBI>'s connect() method.
314
315=item opts
316
317hash reference value. opts is optional parameter used by this module.
318
319=back
320
321=head1 METHODS
322
323=head2 new()
324
325Create DBIx::DBHResolver instance.
326
327=head2 load( $yaml_file_path )
328
329Load config file formatted yaml.
330
331=head2 config( \%config )
332
333Load config. Example config (perl hash reference format):
334
335  +{
336    clusters => +{
337      diary_master => [qw/diary001_master diary002_master/],
338      people_master => [qw/people001_master people002_master people003_master people004_master/]
339    },
340    connect_info => +{
341      diary001_master => +{
342        dsn => 'dbi:driverName:...',
343        user => 'root', password => '', attrs => +{},
344      },
345      diary002_master => +{ ... },
346      ...
347    },
348  }
349
350=head2 connect( $cluster_or_node, $args )
351
352Retrieve database handle. If $args is scalar or array reference, then $args is treated sharding key.
353If $args is hash reference, then see below.
354
355=over
356
357=item strategy
358
359Optional parameter. Specify suffix of strategy module name. Default strategy module is prefixed 'DBIx::DBHResolver::Strategy::'.
360If you want to make custom strategy that is not started of 'DBIx::DBHResolver::Strategy::', then add prefix '+' at the beginning of the module name, such as '+MyApp::Strategy::Custom'.
361
362=item key
363
364Optional parameter. Strategy module uses hint choosing node.
365
366=back
367
368=head2 connect_cached($cluster_or_node, $args)
369
370Retrieve database handle from own cache, if not exists cache then using DBI::connect(). $args is same as connect().
371
372=head2 connect_info($cluster_or_node, $args)
373
374Retrieve connection info as HASHREF. $args is same as connect().
375
376=head2 resolve($cluster_or_node, $args)
377
378Return resolved node name. $args is same as connect.
379
380=head2 resolve_node_keys($cluster_or_node, $keys, $args)
381
382Return hash resolved node and keys. $args is same as connect
383
384  use DBIx::DBHResolver;
385
386  my $resolver = DBIx::DBHResolver->new;
387  $resolver->config(+{
388    clusters => +{
389      MASTER => +{
390        nodes => [qw/MASTER001 MASTER002 MASTER003/],
391        strategy => 'Key',
392      }
393    },
394    connect_info => +{
395      MASTER001 => +{ ... },
396      MASTER002 => +{ ... },
397      MASTER003 => +{ ... },
398    },
399  });
400
401  my @keys = ( 3 .. 8 );
402  my %node_keys = $resolver->resolve_node_keys( 'MASTER', \@keys );
403  ### %node_keys = ( MASTER001 => [ 3, 6 ], MASTER002 => [ 4, 7 ], MASTER003 => [ 5, 7 ] )
404  while ( my ( $node, $keys ) = each %node_keys ) {
405      process_node( $node, $keys );
406  }
407
408=head2 disconnect_all()
409
410Disconnect all cached database handles.
411
412=head2 cluster_info($cluster)
413
414Return cluster info hash ref.
415
416=head2 clusters($cluster)
417
418Retrieve cluster member node names as Array.
419
420  my $r = DBIx::DBHResolver->new;
421  $r->config(+{ ... });
422  my $cluster_or_node = 'activities_master';
423  if ( $r->is_cluster($cluster_or_node) ) {
424    for ($r->cluster( $cluster_or_node )) {
425      process_activities_node($_);
426    }
427  }
428  else {
429    process_activities_node($cluster_or_node);
430  }
431
432=head2 is_cluster($cluster)
433
434Return boolean value which cluster or not given name.
435
436=head2 is_node($node)
437
438Return boolean value which node or not given name.
439
440=head1 GLOBAL VARIABLES
441
442=head2 $CONFIG
443
444Stored config on using class module.
445
446=head2 $DBI
447
448DBI module name, default 'DBI'. If you want to use custom L<DBI> sub class, then you must override this variable.
449
450=head2 $DBI_CONNECT_METHOD
451
452DBI connect method name, default 'connect';
453
454If you want to use L<DBIx::Connector> instead of L<DBI>, then:
455
456  use DBIx::Connector;
457  use DBIx::DBHResolver;
458
459  $DBIx::DBHResolver::DBI = 'DBIx::Connector';
460  $DBIx::DBHResolver::DBI_CONNECT_METHOD = 'new';
461  $DBIx::DBHResolver::DBI_CONNECT_CACHED_METHOD = 'new';
462
463  my $r = DBIx::DBHResolver->new;
464  $r->config(+{...});
465
466  $r->connect('main_master')->txn(
467    fixup => sub {
468      my $dbh = shift;
469      ...
470    }
471  );
472
473=head2 $DBI_CONNECT_CACHED_METHOD
474
475DBI connect method name, default 'connect_cached';
476
477=head1 AUTHOR
478
479=over
480
481=item Kosuke Arisawa E<lt>arisawa@gmail.comE<gt>
482
483=item Toru Yamaguchi E<lt>zigorou@cpan.orgE<gt>
484
485=back
486
487=head1 SEE ALSO
488
489=over
490
491=item L<DBI>
492
493=back
494
495=head1 LICENSE
496
497This library is free software; you can redistribute it and/or modify
498it under the same terms as Perl itself.
499
500=cut
501
502# Local Variables:
503# mode: perl
504# perl-indent-level: 4
505# indent-tabs-mode: nil
506# coding: utf-8-unix
507# End:
508#
509# vim: expandtab shiftwidth=4:
510