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