How does DBIx::Class::Storage::DBI's connect_info work? - perl

The docs for connect_info:
connect_info
This method is normally called by "connection" in DBIx::Class::Schema,
which encapsulates its argument list in an arrayref before passing
them here.
The argument list may contain:
The same 4-element argument set one would normally pass to "connect"
in DBI, optionally followed by extra attributes recognized by
DBIx::Class:
$connect_info_args = [ $dsn, $user, $password, \%dbi_attributes?, \%extra_attributes? ];
A single code reference which returns a
connected DBI database handle optionally followed by extra attributes
recognized by DBIx::Class:
$connect_info_args = [ sub { DBI->connect (...) }, \%extra_attributes? ];
A single hashref with all the attributes and the dsn/user/password
mixed together:
$connect_info_args = [{
dsn => $dsn,
user => $user,
password => $pass,
%dbi_attributes,
%extra_attributes,
}];
$connect_info_args = [{
dbh_maker => sub { DBI->connect (...) },
%dbi_attributes,
%extra_attributes,
}];
This is particularly useful
for Catalyst based applications, allowing the following config
(Config::General style):
<Model::DB>
schema_class App::DB
<connect_info>
dsn dbi:mysql:database=test
user testuser
password TestPass
AutoCommit 1
</connect_info>
</Model::DB>
The dsn/user/password combination can be substituted by the dbh_maker key
whose value is a coderef that returns a connected DBI database handle
Please note that the DBI docs recommend that you always explicitly set
AutoCommit to either 0 or 1. DBIx::Class further recommends that it be
set to 1, and that you perform transactions via our "txn_do" in
DBIx::Class::Schema method. DBIx::Class will set it to 1 if you do not
do explicitly set it to zero. This is the default for most DBDs. See
"DBIx::Class and AutoCommit" for details.
What is this? Is it a method called internally, or a global? And, if it's a method called internally why is it being sent either a dbh maker, or four arguments? What determines what it is sent? It's listed as being a method. What is $connect_info_args?

Here is how I got it to work
Schema Class
You've got to use the storage that's done like this (and you can find it in the docs)
package MyDBIC::Schema;
__PACKAGE__->storage_type("DBIx::Class::Storage::DBI::mysql::MySubClass")
Storage Class
package DBIx::Class::Storage::DBI::mysql::MySubClass;
use mro 'c3';
use base 'DBIx::Class::Storage::DBI::mysql';
sub connect_info {
my $self = shift;
my $retval = $self->next::method([{
username => _username(),
password => $password,
dsn => "my:dsn:"
})
$retval;
};
I found a rough example of how to do it burred here. Talk about shitty docs..
connect_info appears to be broke. Things tried.
Subclassing a Storage with a custom connect_info. It gets called, but nothing it returns does anything useful. Calling ->connect->storage->ensure_connected; on a schema results in error.
Wrapping connect_info with Moose::around(). Fails to wrap. The method 'connect_info' was not found in the inheritance hierarchy.
Calling $self->connect_info() from BUILD... DBIx::Class::Storage::DBI::connect_info gets called but no matter what I hand to it, the sub only gets $self, the first argument.
Calling __PACKAGE__->connect_info({}) or __PACKAGE__->connect_info([{}]) results in both arguments being sent to the DBIx::Class::Storage::DBI::connect_info but the lack of a $self results in Class::XSAccessor: invalid instance method invocant: no hash ref supplied at when the sub tries to write to the accessor.

Related

DBIx::something that can restore session variables on reconnection?

Ordinary DBI::db handler will lost all database session settings that was made using $dbh->do('SET variable_name=value').
Is there any DBIx::* class/package or so that provides method like "set_session" to set session variables and can restore this variables after detection of connection lost (connection timeout in 90% of real cases) ?
It may looks like this:
# inside the user code:
$dbh->set(variable => 'string', yet_another_variable => 42)
# inside the DBIx::* package:
sub reconnect {
# ...
while (my ($var, $val) = each %{$self->saved_vars}) {
$self->dbh->do("SET $var=?", {}, $val)
}
# ...
}
DBI supports something called Callbacks. I can't link to this bit of the doc as the section is quite long, so here it is verbatim.
A more common application for callbacks is setting connection state
only when a new connection is made (by connect() or connect_cached()).
Adding a callback to the connected method (when using connect) or via
connect_cached.connected (when useing connect_cached()>) makes this
easy. The connected() method is a no-op by default (unless you
subclass the DBI and change it). The DBI calls it to indicate that a
new connection has been made and the connection attributes have all
been set. You can give it a bit of added functionality by applying a
callback to it. For example, to make sure that MySQL understands your
application's ANSI-compliant SQL, set it up like so:
my $dbh = DBI->connect($dsn, $username, $auth, {
Callbacks => {
connected => sub {
shift->do(q{
SET SESSION sql_mode='ansi,strict_trans_tables,no_auto_value_on_zero';
});
return;
},
}
});
This is your exact use-case I believe. Do this instead of running your own code after you've connected.

How to fetch values that are hard coded in a Perl subroutine?

I have a perl code like this:
use constant OPERATING_MODE_MAIN_ADMIN => 'super_admin';
use constant OPERATING_MODE_ADMIN => 'admin';
use constant OPERATING_MODE_USER => 'user';
sub system_details
{
return {
operating_modes => {
values => [OPERATING_MODE_MAIN_ADMIN, OPERATING_MODE_ADMIN, OPERATING_MODE_USER],
help => {
'super_admin' => 'The system displays the settings for super admin',
'admin' => 'The system displays settings for normal admin',
'user' => 'No settings are displayed. Only user level pages.'
}
},
log_level => {
values => [qw(FATAL ERROR WARN INFO DEBUG TRACE)],
help => "http://search.cpan.org/~mschilli/Log-Log4perl-1.49/lib/Log/Log4perl.pm#Log_Levels"
},
};
}
How will I access the "value" fields and "help" fields of each key from another subroutine? Suppose I want the values of operating_mode alone or log_level alone?
The system_details() returns a hashref, which has two keys with values being hashrefs. So you can dereference the sub's return and assign into a hash, and then extract what you need
my %sys = %{ system_details() };
my #loglevel_vals = #{ $sys{log_level}->{values} };
my $help_msg = $sys{log_level}->{help};
The #loglevel_vals array contains FATAL, ERROR etc, while $help_msg has the message string.
This makes an extra copy of a hash while one can work with a reference, as in doimen's answer
my $sys = system_details();
my #loglevel_vals = #{ $sys->{log_level}->{values} };
But as the purpose is to interrogate the data in another sub it also makes sense to work with a local copy, what is generally safer (against accidentally changing data in the caller).
There are modules that help with deciphering complex data structures, by displaying them. This helps devising ways to work with data. Often quoted is Data::Dumper, which also does more than show data. Some of the others are meant to simply display the data. A couple of nice ones are Data::Dump and Data::Printer.
my $sys = system_details;
my $log_level = $sys->{'log_level'};
my #values = #{ $log_level->{'values'} };
my $help = $log_level->{'help'};
If you need to introspect the type of structure stored in help (for example help in operating_mode is a hash, but in log_level it is a string), use the ref builtin func.

mojolicious helper storing an elasticsearch connection

i'm experimenting with elasticsearch within mojolicious.
I'm reasonably new at both.
I wanted to create a helper to store the ES connection and I was hoping to pass the helper configuration relating to ES (for example the node info, trace_on file etc).
If I write the following very simple helper, it works;
has elasticsearch => sub {
return Search::Elasticsearch->new( nodes => '192.168.56.21:9200', trace_to => ['File','/tmp/elasticsearch.log'] );
};
and then in startup
$self->helper(es => sub { $self->app->elasticsearch() });
however if I try to extend that to take config - like the following -
it fails. I get an error "cannot find index on package" when the application calls $self->es->index
has elasticsearch => sub {
my $config = shift;
my $params->{nodes} = '192.168.56.21:' . $config->{port};
$params->{trace_to} = $config->{trace_to} if $config->{trace_to};
my $es = Search::Elasticsearch->new( $params );
return $es;
};
and in startup
$self->helper(es => sub { $self->app->elasticsearch($self->config->{es}) });
I assume I'm simply misunderstanding helpers or config or both - can someone enlighten me?
Just fyi, in a separate controller file I use the helper as follows;
$self->es->index(
index => $self->_create_index_name($index),
type => 'crawl_data',
id => $esid,
body => {
content => encode_json $data,
}
);
that works fine if I create the helper using the simple (1st) form above.
I hope this is sufficient info? please let me know if anything else is required?
First of all, has and helper are not the same. has is a lazily built instance attribute. The only argument to an attribute constructor is the instance. For an app, it would look like:
package MyApp;
has elasticsearch => sub {
my $app = shift;
Search::ElasticSearch->new($app->config->{es});
};
sub startup {
my $app = shift;
...
}
This instance is then persistent for the life of the application after first use. I'm not sure if S::ES has any reconnect-on-drop logic, so you might need to think about it a permanent object is really what you want.
In contrast a helper is just a method, available to the app, all controllers and all templates (in the latter case, as a function). The first argument to a helper is a controller instance, whether the current one or a new one, depending on context. Therefore you need to build your helper like:
has (elasticsearch => sub {
my ($c, $config) = #_;
$config ||= $c->app->config->{es};
Search::ElasticSearch->new($config);
});
This mechanism will build the instance on demand and can accept pass-in arguments, perhaps for optional configuration override as I have shown in that example.
I hope this answers your questions.

Best way to check for incorrect hash key input

In my Perl script, I have subroutine that is called hundreds of times with as many different sets of parameters, as the only values that are sent in are ones that differ from the defaults. (It goes without saying that the number of permutations and combinations is very large) To make it more robust, I would like to do some checking on the parameters. Here is a shrunken version of my subroutine (the actual version has dozens of parameters with very specific, sometimes lengthy names):
# Obtain any parameters that differ from the defaults and send for processing
sub importantSub
{
my %params =
(
commandType => 5,
commandId => 38,
channel1Enable => 0,
channel2Enable => 0,
channel3Enable => 0,
channel4Enable => 0,
channel5Enable => 0,
channel6Enable => 0,
channel7Enable => 0,
channel8Enable => 0,
channel9Enable => 0,
channel10Enable => 0,
# This goes on for a VERY long time
#_
);
# Make sure we have exactly as many keys as we expect - verify that
# no additional parameters were added (Real version has 92)
if( keys(%params) != 92 )
{
croak("Unexpected parameter in hash!");
}
return &$privateProcessingFunction('Data Path Configuration', \%params);
}
As you can see, I currently do a check to see if the number of values is the same, as if something is sent in as "chan1Enable" instead of "channel1Enable", it will throw that number off.
But with so many calls to the subroutine from multiple other scripts written by multiple other engineers, I would like to find a way to find WHICH value was incorrect (e.g. Don't just say that there was an unexpected parameter, say that "chan1Enable" was invalid). Furthermore, if multiple values were incorrect, I'd like to list all of them.
What is the most efficient way to do this?
(I ask about efficiency since the function is currently called in over 400 different ways and that will likely continue to grow as the application expands.)
There are two kinds of errors: supplying an unrecognized parameter, or failing to supply a recognized parameter. You'll have to worry about the second issue as you edit the list of parameters and make sure that the new parameters are used consistently throughout the application.
The best and easiest solution is to use another hash.
my #params = qw(commandType commandId channel1Enabled ...);
my %copy = %params;
my #validation_errors = ();
# are all the required parameters present?
foreach my $param (#params) {
if (not exists $copy{$param}) {
push #validation_errors, "Required param '$param' is missing.";
}
delete $copy{$param};
}
# since we have delete'd all the recognized parameters,
# anything left is unrecognized
foreach my $param (keys %copy) {
push #validation_errors, "Unrecognized param '$param' = '$copy{$param}' in input.";
}
if (#validation_errors) {
die "errors in input:\n", join("\n", #validation_errors);
}
I recommend using a formal tool to help validate your parameters your passing in. Params::Validate is tried and true, while Type::Params is a recent take on the problem space, allowing you to use same set of constraints that you would also use with Moo or Moose.
Here's the kind of diagnostic that Params::Validate would give you for
an unrecognized parameter:
use Params::Validate ':all';
sub foo {
my %p = validate(#_, {
first_required => 1,
second_required => 1,
first_optional => 0.
});
}
foo( boom => 'zoom' );
Results in:
The following parameter was passed in the call to main::foo but was not listed in the validation options: boom
at /tmp/t.pl line 7
main::foo('boom', 'zoom') called at /tmp/t.pl line 14

Mojolicious custom sessions

I am trying to use database sessions with Mojolicious instead of the builtin ones that are working with signed cookies.
In the startup subroutine I have something like:
my $dbh = DBI->connect(
$config->{database}->{dsn},
$config->{database}->{user},
$config->{database}->{password},
);
my $session = MojoX::Session->new(
store => [dbi => {dbh => $dbh}], # use MojoX::Session::Store::Dbi
transport => 'cookie', # this is by default
ip_match => 1
);
(ref($self))->attr( 'session' => sub {
return $session;
} );
And I want to use the session object like $self->session or $self->app->session in controllers.
Unfortunately it doesn't work - it uses previous sessions (from different browsers).
This drives me crazy - all I tried today was to make this work - I've read all the documentation available, also the source of MojoX::Session and all its related classes, tried in about 923847293847239847 ways to make it work, but nothing seems to do it.
PS: I created the session table in the db.
Do you know what I should do in order to be able to use DB sessions with Mojolicious?
You can connect MojoX::Session to the application as a plugin in a startup function.
use Mojolicious::Plugin::Session;
[...]
sub startup {
my $self = shift;
[...]
$self->plugin( session => {
stash_key => 'mojox-session',
store => [dbi => {dbh => $dbh}], # use MojoX::Session::Store::Dbi
transport => 'cookie',
ip_match => 1
});
[...]
Afterwards, you'll have access to session through stash key 'mojox-session' in controllers.
For example:
$self->stash('mojox-session')->data('something');
You can use whatever sort of session backend you like. Just create your own controller base class derived from Mojolicious::Controller and override session(), like so:
package NiceController;
use Mojo::Base 'Mojolicious::Controller';
sub session { # custom code here }
1;
then in startup(), set your controller class as the default:
$self->controller_class('NiceController');
Finally, make sure your application controllers inherit from NiceController instead of Mojolicious::Controller
It's probably a good idea to make your overridden session() function work just like the built-in one, to avoid future confusion.
-xyz
The $app->session method is reserved for using the built-in sessions.
You should take a look at the Mojolicious helpers and you probably want to use a different method name to avoid conflict.