File Coverage

blib/lib/DBIx/DBHResolver.pm
Criterion Covered Total %
statement 112 134 83.5
branch 34 48 70.8
condition 5 10 50.0
subroutine 28 34 82.3
pod 13 14 92.8
total 192 240 80.0


line stmt bran cond sub pod time code
1             package DBIx::DBHResolver;
2              
3 7     7   4232 use strict;
  7         14  
  7         256  
4 7     7   33 use warnings;
  7         12  
  7         198  
5 7     7   6559 use parent qw(Class::Accessor::Fast);
  7         2417  
  7         34  
6 7     7   33840 use Carp;
  7         19  
  7         680  
7 7     7   6887 use Config::Any;
  7         83028  
  7         289  
8 7     7   7376 use Data::Util qw(is_value is_array_ref is_hash_ref is_instance is_invocant);
  7         7481  
  7         773  
9 7     7   19111 use DBI;
  7         151503  
  7         653  
10 7     7   7666 use Hash::Merge::Simple qw(merge);
  7         3543  
  7         469  
11 7     7   7933 use Try::Tiny;
  7         12008  
  7         407  
12 7     7   6457 use UNIVERSAL::require;
  7         17533  
  7         90  
13              
14 7     7   4946 use DBIx::DBHResolver::Strategy::Key;
  7         19  
  7         81  
15 7     7   4256 use DBIx::DBHResolver::Strategy::List;
  7         26  
  7         75  
16 7     7   4495 use DBIx::DBHResolver::Strategy::Range;
  7         17  
  7         73  
17              
18             our $VERSION = '0.17';
19             our $CONFIG = +{};
20             our $DBI = 'DBI';
21             our $DBI_CONNECT_METHOD = 'connect';
22             our $DBI_CONNECT_CACHED_METHOD = 'connect_cached';
23              
24             __PACKAGE__->mk_accessors(qw/_config/);
25              
26             sub new {
27             shift->SUPER::new(
28 7     7 1 208 +{ _config => +{} } );
29             }
30              
31             sub config {
32 7088     7088 1 13426 my ( $proto, $config ) = @_;
33              
34 7088 100       21971 if ( is_instance( $proto, 'DBIx::DBHResolver' ) ) {
35 2985 100       12198 return $proto->_config unless defined $config;
36 9         68 $proto->_config($config);
37             }
38             else {
39 4103 100       22847 return $CONFIG unless defined $config;
40 10         34 $CONFIG = $config;
41             }
42             }
43              
44             sub load {
45 4     4 1 12 my ( $proto, @files ) = @_;
46 4         11 for (@files) {
47 6 50 33     335 croak $! unless ( -f $_ && -r $_ );
48             }
49 4         8 my $config;
50             try {
51 4     4   203 $config = Config::Any->load_files(
52             +{ files => \@files, use_ext => 1, flatten_to_hash => 1, } );
53 4         99071 $config = merge( @$config{@files} );
54             }
55             catch {
56 0     0   0 croak $_;
57 4         231 };
58 4         245 $proto->config($config);
59             }
60              
61             sub connect {
62 0     0 1 0 my ( $proto, $cluster_or_node, $args ) = @_;
63 0         0 my $dbh = $DBI->$DBI_CONNECT_METHOD(
64 0 0       0 @{ $proto->connect_info( $cluster_or_node, $args ) }
65             {qw/dsn user password attrs/} )
66             or croak($DBI::errstr);
67 0         0 return $dbh;
68             }
69              
70             sub connect_cached {
71 0     0 1 0 my ( $proto, $cluster_or_node, $args ) = @_;
72 0         0 my $dbh = $DBI->$DBI_CONNECT_CACHED_METHOD(
73 0 0       0 @{ $proto->connect_info( $cluster_or_node, $args ) }
74             {qw/dsn user password attrs/} )
75             or croak($DBI::errstr);
76 0         0 return $dbh;
77             }
78              
79             sub disconnect_all {
80 0     0 1 0 my ($proto) = @_;
81              
82 0         0 my %drivers = DBI->installed_drivers;
83 0         0 for my $drh ( values %drivers ) {
84 0         0 for my $dbh ( @{ $drh->{ChildHandles} } ) {
  0         0  
85 0         0 eval { $dbh->disconnect; };
  0         0  
86             }
87             }
88             }
89              
90             sub connect_info {
91 740     740 1 14624 my ( $proto, $cluster_or_node, $args ) = @_;
92              
93 740         2679 my ($resolved_node) = $proto->resolve( $cluster_or_node, $args );
94 740         2313 return $proto->node_info($resolved_node);
95             }
96              
97             sub resolve {
98 2347     2347 1 3457 my ( $proto, $cluster_or_node, $args ) = @_;
99              
100 2347 100       5526 if ( $proto->is_cluster($cluster_or_node) ) {
    50          
101 1255 100       7263 if ( is_hash_ref($args) ) {
102 75 50       190 croak q|args has not 'strategy' field| unless $args->{strategy};
103 75         229 my $strategy_class =
104             $proto->_resolve_namespace( $args->{strategy} );
105 75         260 my ( $resolved_node, @keys ) =
106             $proto->_ensure_class_loaded($strategy_class)
107             ->resolve( $proto, $cluster_or_node, $args );
108 75         229 return $proto->resolve( $resolved_node, \@keys );
109             }
110             else {
111 1180         3427 my $cluster_info = $proto->cluster_info($cluster_or_node);
112 1180 100       8905 if ( is_array_ref $cluster_info ) {
    50          
113 20         173 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 20         90 return $proto->resolve( $resolved_node, \@keys );
124             }
125             elsif ( is_hash_ref $cluster_info ) {
126 1160         3812 my $strategy_class =
127             $proto->_resolve_namespace( $cluster_info->{strategy} );
128 1160         3962 my ( $resolved_node, @keys ) =
129             $proto->_ensure_class_loaded($strategy_class)
130             ->resolve( $proto, $cluster_or_node,
131             +{ %$cluster_info, key => $args, } );
132 1160         5363 return $proto->resolve( $resolved_node, \@keys );
133             }
134             }
135             }
136             elsif ( $proto->is_node($cluster_or_node) ) {
137 1092         5706 my $connect_info = $proto->node_info($cluster_or_node);
138 1092 100       5645 if ( is_hash_ref($connect_info) ) {
139 857         2942 return $cluster_or_node;
140             }
141             else {
142 235         2947 return $connect_info;
143             }
144             }
145             else {
146 0         0 croak sprintf( '%s is not defined', $cluster_or_node );
147             }
148             }
149              
150             sub resolve_node_keys {
151 36     36 1 642 my ( $proto, $cluster_or_node, $keys, $args ) = @_;
152 36         43 my %node_keys;
153 36         73 for my $key (@$keys) {
154 336 100       839 if ( is_hash_ref $args ) {
155 50   50     110 $args->{strategy} ||= 'Key';
156 50         79 $args->{key} = $key;
157             }
158             else {
159 286         344 $args = $key;
160             }
161              
162 336         710 my $resolved_node = $proto->resolve( $cluster_or_node, $args );
163 336   100     1053 $node_keys{$resolved_node} ||= [];
164 336         330 push( @{ $node_keys{$resolved_node} }, $key );
  336         798  
165             }
166              
167 36 100       320 return wantarray ? %node_keys : \%node_keys;
168             }
169              
170             sub cluster_info {
171 1780     1780 1 2753 my ( $proto, $cluster, $cluster_info ) = @_;
172 1780 50       3095 if ( defined $cluster_info ) {
173 0         0 $proto->config->{clusters}{$cluster} = $cluster_info;
174             }
175             else {
176 1780         3820 return $proto->config->{clusters}{$cluster};
177             }
178             }
179              
180             sub clusters {
181 600     600 1 879 my ( $proto, $cluster ) = @_;
182 600         1208 my $cluster_info = $proto->cluster_info($cluster);
183 503         1393 my @nodes =
184             is_array_ref($cluster_info)
185             ? @$cluster_info
186 600 100       3091 : @{ $cluster_info->{nodes} };
187 600 50       2915 wantarray ? @nodes : \@nodes;
188             }
189              
190             {
191 7     7   9340 no warnings;
  7         17  
  7         3279  
192             *cluster = \&clusters;
193             }
194              
195             sub is_cluster {
196 2353     2353 1 3227 my ( $proto, $cluster ) = @_;
197 2353 100       5264 exists $proto->config->{clusters}{$cluster} ? 1 : 0;
198             }
199              
200             sub node_info {
201 1832     1832 0 2611 my ( $proto, $node, $node_info ) = @_;
202 1832 50       3363 if ( defined $node_info ) {
203 0         0 $proto->config->{connect_info}{$node} = $node_info;
204             }
205             else {
206 1832         3429 return $proto->config->{connect_info}{$node};
207             }
208             }
209              
210             sub is_node {
211 1098     1098 1 4649 my ( $proto, $node ) = @_;
212 1098 100       2235 exists $proto->config->{connect_info}{$node} ? 1 : 0;
213             }
214              
215             sub _ensure_class_loaded {
216 1235     1235   1677 my ( $proto, $class_name ) = @_;
217 1235 50       6420 unless ( is_invocant $class_name ) {
218             try {
219 0     0   0 $class_name->require;
220             }
221             catch {
222 0     0   0 croak $_;
223 0         0 };
224             }
225 1235         10143 $class_name;
226             }
227              
228             sub _resolve_namespace {
229 1235     1235   1807 my ( $proto, $class_name ) = @_;
230 1235 50 33     5816 $class_name = 'Key'
231             if ( defined $class_name && $class_name eq 'Remainder' );
232 1235 50       4388 $class_name =
233             $class_name =~ /^\+(.+)$/
234             ? $1
235             : join( '::', ( __PACKAGE__, 'Strategy', $class_name ) );
236 1235         2587 $class_name;
237             }
238              
239             1;
240              
241             =head1 NAME
242              
243             DBIx::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              
273             DBIx::DBHResolver resolves database connection on the environment has many database servers.
274             The resolution algorithm is extensible and pluggable, because of this you can make custom strategy module easily.
275              
276             This module can retrieve L's database handle object or connection information (data source, user, credential...) by labeled name
277             and treat same cluster consists many nodes as one labeled name, choose fetching strategy.
278              
279             DBIx::DBHResolver is able to use as instance or static class.
280              
281             =head2 USING STRATEGY, MAKING CUSTOM STRATEGY
282              
283             See L.
284              
285             =head2 connect_info format
286              
287             B 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              
301             string value. dsn is connection information used by L's connect() method.
302              
303             =item user
304              
305             string value. user is database access user used by L's connect() method.
306              
307             =item password
308              
309             string value. user is database access password used by L's connect() method.
310              
311             =item attrs
312              
313             hash reference value. attrs is optional parameter used by L's connect() method.
314              
315             =item opts
316              
317             hash reference value. opts is optional parameter used by this module.
318              
319             =back
320              
321             =head1 METHODS
322              
323             =head2 new()
324              
325             Create DBIx::DBHResolver instance.
326              
327             =head2 load( $yaml_file_path )
328              
329             Load config file formatted yaml.
330              
331             =head2 config( \%config )
332              
333             Load 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              
352             Retrieve database handle. If $args is scalar or array reference, then $args is treated sharding key.
353             If $args is hash reference, then see below.
354              
355             =over
356              
357             =item strategy
358              
359             Optional parameter. Specify suffix of strategy module name. Default strategy module is prefixed 'DBIx::DBHResolver::Strategy::'.
360             If 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              
364             Optional parameter. Strategy module uses hint choosing node.
365              
366             =back
367              
368             =head2 connect_cached($cluster_or_node, $args)
369              
370             Retrieve 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              
374             Retrieve connection info as HASHREF. $args is same as connect().
375              
376             =head2 resolve($cluster_or_node, $args)
377              
378             Return resolved node name. $args is same as connect.
379              
380             =head2 resolve_node_keys($cluster_or_node, $keys, $args)
381              
382             Return 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              
410             Disconnect all cached database handles.
411              
412             =head2 cluster_info($cluster)
413              
414             Return cluster info hash ref.
415              
416             =head2 clusters($cluster)
417              
418             Retrieve 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              
434             Return boolean value which cluster or not given name.
435              
436             =head2 is_node($node)
437              
438             Return boolean value which node or not given name.
439              
440             =head1 GLOBAL VARIABLES
441              
442             =head2 $CONFIG
443              
444             Stored config on using class module.
445              
446             =head2 $DBI
447              
448             DBI module name, default 'DBI'. If you want to use custom L sub class, then you must override this variable.
449              
450             =head2 $DBI_CONNECT_METHOD
451              
452             DBI connect method name, default 'connect';
453              
454             If you want to use L instead of L, 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              
475             DBI connect method name, default 'connect_cached';
476              
477             =head1 AUTHOR
478              
479             =over
480              
481             =item Kosuke Arisawa Earisawa@gmail.comE
482              
483             =item Toru Yamaguchi Ezigorou@cpan.orgE
484              
485             =back
486              
487             =head1 SEE ALSO
488              
489             =over
490              
491             =item L
492              
493             =back
494              
495             =head1 LICENSE
496              
497             This library is free software; you can redistribute it and/or modify
498             it 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: