Mojar-Mysql

 view release on metacpan or  search on metacpan

lib/Mojar/Mysql/Connector.pm  view on Meta::CPAN

use Carp 'croak';
use File::Spec::Functions 'catfile';
use Mojar::ClassShare 'have';

sub import {
  my ($pkg, %param) = @_;
  my $caller = caller;
  # Helpers
  $param{-connector} //= 1 if exists $param{-dbh} and $param{-dbh};
  if (exists $param{-connector} and my $cname = delete $param{-connector}) {
    $cname = 'connector' if "$cname" eq '1';
    no strict 'refs';
    *{"${caller}::$cname"} = sub {
      my $self = shift;
      if (@_) {
        $self->{$cname} = (@_ > 1) ? Mojar::Mysql::Connector->new(@_) : shift;
        return $self;
      }
      return $self->{$cname} //= Mojar::Mysql::Connector->new;
    };
    if (exists $param{-dbh} and my $hname = delete $param{-dbh}) {
      $hname = 'dbh' if "$hname" eq '1';
      *{"${caller}::$hname"} = sub {
        my $self = shift;
        if (@_) {
          $self->{$hname} = (@_ > 1) ? $self->$cname->connect(@_) : shift;
          return $self;
        }
        return $self->{$hname}
          if defined $self->{$hname} and $self->{$hname}->ping;
        return $self->{$hname} = $self->$cname->connect;
      };
    }
  }
  # Global defaults
  if (%param and %{$pkg->Defaults}) {
    # Already have defaults => check unchanged
    # Not interested in defaults of Defaults => use hash not methods
    my $ps = join ':', map +($_ .':'. ($param{$_} // 'undef')),
        sort keys %param;
    my $ds = join ':', map +($_ .':'. ($pkg->Defaults->{$_} // 'undef')),
        sort keys %{$pkg->Defaults};
    die "Redefining class defaults for $pkg" unless $ps eq $ds;
  }
  @{$pkg->Defaults}{keys %param} = values %param if %param;
  # Debugging
  $pkg->trace($param{TraceLevel})
    if exists $param{TraceLevel} and defined $param{TraceLevel};
}

# Class attribute

# Use a singleton object for holding use-time class defaults
have Defaults => sub { bless {} => ref $_[0] || $_[0] };

# Attributes

has quiesce_timeout => 500;

my @DbdFields = qw(RaiseError PrintError PrintWarn AutoCommit TraceLevel
    mysql_auto_reconnect mysql_enable_utf8);

has RaiseError => 1;
has PrintError => 0;
has PrintWarn => 0;
has AutoCommit => 1;
has TraceLevel => 0;
has mysql_auto_reconnect => 0;
has mysql_enable_utf8 => 1;

my @ConFields = qw(label cnfdir cnf cnfgroup);

has 'label';
has cnfdir => '.';
has 'cnf';
has 'cnfgroup';

my @DbiFields = qw(driver host port schema user password);

has driver => 'mysql';
has 'host';  # eg 'localhost'
has 'port';  # eg 3306
has 'schema';  # eg 'test';
has 'user';
has 'password';

# Public methods

sub new {
  my ($proto, %param) = @_;
  # $proto may contain defaults to be cloned
  # %param may contain defaults for overriding
  my %defaults = ref $proto ? ( %{ ref($proto)->Defaults }, %$proto )
                            : %{$proto->Defaults};
  delete $defaults{$_} for grep { ref $proto and /^dbh\./ } keys %defaults;
  return Mojo::Base::new($proto, %defaults, %param);
}

sub connect {
  my ($proto, @args) = @_;
  my $class = ref $proto || $proto;
  @args = $proto->dsn(@args) unless @args and $args[0] =~ /^DBI:/i;
  my $dbh;
  eval { $dbh = $class->SUPER::connect(@args) }
  or do {
    my $e = $@;
    croak sprintf "Connection error\n%s\n%s", $proto->dsn_to_dump(@args), $e;
  };
  return $dbh;
}

sub connection {
  my ($self, $tag) = @_; $tag //= 'connection';
  return $self->{"dbh.$tag"} if ($self->{"dbh.$tag"} //= $self->connect)->ping;
  return $self->{"dbh.$tag"} = $self->connect;
}

sub dsn {
  my ($proto, %param) = @_;
  my $param = $proto->new(%param);

  my $cnf_txt = '';
  if (my $cnf = $param->cnf) {
    # MySQL .cnf file
    $cnf .= '.cnf' unless $cnf =~ /\.cnf$/;
    $cnf = catfile $param->cnfdir, $cnf if ! -r $cnf and defined $param->cnfdir;
    croak "Failed to find/read .cnf file ($cnf)" unless -f $cnf and -r $cnf;

    $cnf_txt = ';mysql_read_default_file='. $cnf;

lib/Mojar/Mysql/Connector.pm  view on Meta::CPAN

security, and error handling.  Supports easy use of credential (cnf) files, akin
to

  mysql --defaults-file=credentials.cnf

It aims to reduce boilerplate, verbosity, mistakes, and parameter overload, but
above all it tries to make it quick and easy to Do The Right Thing.

As the name implies, the class provides connector objects -- containers for
storing and updating your connection parameters.  When you call C<connect>, the
connector returns a handle created using its retained parameters plus any
call-time parameters passed.  You don't however have to use connectors; for
simple usage it can be easier to use C<connect> directly from the class.

You can use a DSN tuple if you want to, but it's more readable and less
error-prone to specify your parameters either as a hash or by setting individual
attributes.  Each call to C<connect> will then construct the DSN for you.

You can optionally import a helper method, called C<dbh> (or whatever name you
choose) so that you can focus even less on the connector/connection and more on
your code.  The helper will cache your database handle and create a new one
automatically if the old one is destroyed or goes stale.

The fourth layer of convenience is provided by the added database handle
methods.  Changing session variables (C<session_var>) is easy, listing only
genuine tables (C<real_tables>) is easy, and there's
L<more|/"DATABASE HANDLE METHODS">.

=head1 CLASS METHODS

=head2 C<new>

  Mojar::Mysql::Connector->new(label => 'cache', cnf => 'myuser_localhost');

Constructor for a connector, based on class defaults.  Takes a (possibly empty)
list of parameters.  Returns a connector (Mojar::Mysql::Connector object) the
defaults of which are those of the class overlaid with those passed to the
constructor.

=head2 C<connect>

 $dbh1 = Mojar::Mysql::Connector->connect(
   'DBI:mysql:test;host=localhost', 'admin', 's3cr3t', {}
 );
 $dbh2 = Mojar::Mysql::Connector->connect(
   schema => 'test',
   host => 'localhost',
   user => 'admin',
   password => 's3cr3t'
 );
 $dbh3 = Mojar::Mysql::Connector->connect;

Constructor for a connection (db handle).  If the first element passed has
prefix C<DBI:> then it is a DSN string (the traditional route) and so is passed
straight to C<DBI::connect> (L<DBI/"DBI Class Methods">).  Otherwise a DSN is
first constructed.  (The DSN tuple does not persist and is constructed fresh on
each call to C<connect>.)

In the examples above, $dbh1 and $dbh2 are not equivalent because the second
connector would also incorporate module defaults and use-time parameters, in
addition to the passed parameters.  So, for instance, mysql_enable_utf8 might be
included in the second connector.

=head2 C<dsn>

  @dbi_args = Mojar::Mysql::Connector->dsn(
    cnf => 'myuser_localhost', schema => 'test'
  );

A convenience method used internally by connect.  Takes a (possibly empty)
parameter hash.  Returns a four-element array to pass to C<DBI::connect>,
constructed from the default values of the constructing class overlaid with any
additional parameters passed.  The main reason for using this method is when you
want to use L<DBI> (or another DSN-consumer) directly but want to avoid the
inconvenience of assembling sensible parameters yourself.

  use DBI;
  use Mojar::Mysql::Connector (
    cnfdir => '/srv/myapp/cfg',
    cnf => 'myuser_localhost'
  );
  my $dbh = DBI->connect(
    Mojar::Mysql::Connector->dsn(schema => 'foo', AutoCommit => 0)
  );

=head2 C<dsn_to_dump>

  warn(Mojar::Mysql::Connector->dsn_to_dump(@dsn));

A convenience method used internally to chop up the four-element array
(particularly the fourth element, the hash ref) into something more readable,
for error reporting and debugging.  Tries to occlude any password within.

=head2 C<Defaults>

  say Mojar::Util::dumper(Mojar::Mysql::Connector->Defaults);

Provides access to the class defaults in order to help debugging.

=head1 OBJECT METHODS

=head2 C<new>

  $connector->new(label => 'transaction', AutoCommit => 0);

Constructor for a connector based on an existing connector.  Takes a
(possibly empty) parameter hash.  Returns a connector (Mojar::Mysql::Connector
object) the defaults of which are those of the given connector overlaid with
any arguments passed in.

=head2 C<connect>

  $dbh = $connector->connect(
    'DBI:mysql:test;host=localhost', 'admin', 's3cr3t', {});
  $dbh = $connector->connect(AutoCommit => 0);
  $dbh = $connector->connect;

Constructor for a connection (db handle).  If the first element passed has
prefix C<DBI:> then it is a DSN string (the traditional route) and so is passed
straight to C<DBI::connect> (L<DBI/"DBI Class Methods">) without consideration
of the connector's existing parameters.  Otherwise a DSN is first constructed.
(The DSN tuple does not persist and is constructed fresh on each call to
C<connect>.)

=head2 Attributes

All connector parameters are implemented as attributes with exactly the same
spelling.  So for example you can

  $connector->RaiseError(undef);  # disable RaiseError
  $connector->mysql_enable_utf8(0);  # disable mysql_enable_utf8

The attributes, with their coded defaults, are

  RaiseError => 1
  PrintError => 0
  PrintWarn => 0
  AutoCommit => 1
  TraceLevel => 0
  mysql_auto_reconnect => 0
  mysql_enable_utf8 => 1

  label
  cnfdir => '.'
  cnf
  cnfgroup

  driver => 'mysql'
  host
  port
  schema
  user
  password

In addition, any L<DBD::mysql> attributes (beginning "mysql_") are passed
through to the driver.

  $dbh = $connector->connect(mysql_skip_secure_auth => 1);

=head1 DATABASE HANDLE METHODS

=head2 C<selectall_arrayref_hashrefs>

  $_->{Command} ne 'Sleep' and say $_->{User}
    for $dbh->selectall_arrayref_hashrefs(q{SHOW FULL PROCESSLIST});

  printf '%s can select: %s', $_->{User}, $_->{Select_priv}
    for $dbh->selectall_arrayref_hashrefs(q{SELECT * FROM mysql.user});

Returns an arrayref of hashrefs, each hashref being a record of the resultset.
The keys of the hashref are the column/field names of the record.  This is
simply minimal sugar on the selectall_arrayref method provided by DBI; it saves
the little boilerplate of "Slice => {}".  If you want to pass bound values then
you need undef as the second argument.

  printf '%s can select: %s', $_->{User}, $_->{Select_priv}
    for $dbh->selectall_arrayref_hashrefs(
      q{SELECT * FROM mysql.user WHERE User != ?}, undef, 'root'
    );

=head2 C<mysqld_version>

  if ($dbh->mysqld_version =~ /^5.0/) {...}

Returns the version of the db server connected to; the version part of

  mysqld --version

=head2 C<thread_id>

  $tmp_table_name = q{ConcurrencySafe_}. $dbh->thread_id;

Utility method to get the connection's thread identifier (unique on that db
server at that point in time).

=head2 C<current_schema>

  $schema_name = $dbh->current_schema;

The same string as given by

lib/Mojar/Mysql/Connector.pm  view on Meta::CPAN


=head2 C<enable_quotes>

The inverse of C<disable_quotes>.

=head2 C<disable_fk_checks>

  $dbh->disable_fk_checks->do(q{DROP TABLE ...});

Disable foreign key checks.  Lasts the lifetime of the connection.

=head2 C<enable_fk_checks>

The inverse of C<disable_fk_checks>.

=head2 C<schemata>

  for my $schema (@{$dbh->schemata}) {...}

Returns an arrayref of schema names, similar to

  SHOW DATABASES

but does not get fooled by C<lost+found>.

=head2 C<tables_and_views>

  foreach my $table ($dbh->tables_and_views) {...}

Returns a hashref of table and view names, similar to

  SHOW TABLES

See also L<DBI/tables>.

=head2 C<real_tables>

  for my $table (@{$dbh->real_tables}) {...}

Returns a arrayref of real table names, similar to

  SHOW TABLES

but excluding views.

=head2 C<views>

  for my $view (@{$dbh->views}) {...}

Returns a arrayref of view names, similar to

  SHOW TABLES

but excluding real tables.

=head1 CHARACTER ENCODINGS

To read/store characters encoded as non-ASCII, non-UTF8, you must disable
handling of UTF-8.

  $connector = Mojar::Mysql::Connector->new(mysql_enable_utf8 => 0);

This is essential, for example, when fetching high-latin (eg non-ASCII 8859-1)
characters.

=head1 DEBUGGING

You can enable DBI trace logging at use-time:

  use Mojar::Mysql::Connector (TraceLevel => '3|CON');

and since you have access to all of L<DBI>, you can set tracing using the method

  $dbh->trace('3|CON');
  ...
  $dbh->trace(0);

or by using the attribute

  {
    local $dbh->{TraceLevel} = '3|CON';
    ...
  }

To set tracing for all handles, use the class method instead.  See
L<DBI/TRACING>.

=head1 SUPPORT

=head2 Homepage

L<http://niczero.github.com/mojar-mysql>

=head2 Wiki

L<http://github.com/niczero/mojar/wiki>

=head1 RATIONALE

This class was first used in production in 2002.  Before then, connecting to
databases was ugly and annoying.  Setting C<RaiseError> upon every connect was
clumsy and irritating.  In development teams it was tricky checking that all
code was using sensible parameters and awkward ensuring use of risky parameters
(eg C<disable_fk_checks>) was kept local.  As use of this class spread, it had
to be useful in persistent high performance applications as well as many small
scripts and the occasional commandline.  More recently I discovered the Joy of
L<Mojolicious> and employed L<Mojo::Base> to remove unwanted complexity and
eliminate a long-standing bug.  The ensuing fun motivated an extensive rewrite,
fixing broken documentation, improved the tests (thank you travis), and we have,
finally, its public release.  As noted below there are now quite a few smart
alternatives out there but I'm still surprised how little support there is for
keeping passwords out of your codebase and helping you manage multiple
connections.

=head1 COPYRIGHT AND LICENCE

Copyright (C) 2002--2017, Nic Sandfield.

This program is free software, you can redistribute it and/or modify it under
the terms of the Artistic License version 2.0.



( run in 1.628 second using v1.01-cache-2.11-cpan-6aa56a78535 )