| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
|
|---|
| 8 |
|
|---|
| 9 |
|
|---|
| 10 |
|
|---|
| 11 |
|
|---|
| 12 |
|
|---|
| 13 |
|
|---|
| 14 |
|
|---|
| 15 |
|
|---|
| 16 |
|
|---|
| 17 |
|
|---|
| 18 |
|
|---|
| 19 |
|
|---|
| 20 |
|
|---|
| 21 |
|
|---|
| 22 |
|
|---|
| 23 |
|
|---|
| 24 |
|
|---|
| 25 |
|
|---|
| 26 |
|
|---|
| 27 |
|
|---|
| 28 |
|
|---|
| 29 |
|
|---|
| 30 |
|
|---|
| 31 |
=pod |
|---|
| 32 |
|
|---|
| 33 |
=head1 NAME |
|---|
| 34 |
|
|---|
| 35 |
HoneyClient::DB - Perl extension to provide an abstract interface for |
|---|
| 36 |
storing HoneyClient data into a database. |
|---|
| 37 |
|
|---|
| 38 |
=head1 VERSION |
|---|
| 39 |
|
|---|
| 40 |
This documentation refers to HoneyClient::DB version 0.99. |
|---|
| 41 |
|
|---|
| 42 |
=head1 SYNOPSIS |
|---|
| 43 |
|
|---|
| 44 |
As a generic example, let's store data about superheroes. |
|---|
| 45 |
|
|---|
| 46 |
=head2 DEFINE SCHEMAS |
|---|
| 47 |
|
|---|
| 48 |
# First, we define a schema for each superhero ability (child object). |
|---|
| 49 |
use HoneyClient::DB; |
|---|
| 50 |
package HoneyClient::DB::SuperHero::Ability; |
|---|
| 51 |
use base("HoneyClient::DB"); |
|---|
| 52 |
|
|---|
| 53 |
# Define Ability Schema |
|---|
| 54 |
our %fields = ( |
|---|
| 55 |
string => { |
|---|
| 56 |
# Each ability should have a name. |
|---|
| 57 |
name => { |
|---|
| 58 |
# This name should be required. |
|---|
| 59 |
required => 1, # Must exist and is not null |
|---|
| 60 |
}, |
|---|
| 61 |
}, |
|---|
| 62 |
|
|---|
| 63 |
# Each ability may have an optional description. |
|---|
| 64 |
text => [ 'description' ], |
|---|
| 65 |
|
|---|
| 66 |
# Each ability may have an optional recharge time. |
|---|
| 67 |
int => [ 'recharge_time' ], |
|---|
| 68 |
); |
|---|
| 69 |
|
|---|
| 70 |
# Next, we define a schema for each superhero (parent object). |
|---|
| 71 |
package HoneyClient::DB::SuperHero; |
|---|
| 72 |
use base("HoneyClient::DB"); |
|---|
| 73 |
|
|---|
| 74 |
# Define SuperHero Schema |
|---|
| 75 |
our %fields = ( |
|---|
| 76 |
string => { |
|---|
| 77 |
# Each superhero should have a name. |
|---|
| 78 |
name => { |
|---|
| 79 |
# This name should be required. |
|---|
| 80 |
required => 1, |
|---|
| 81 |
key => $HoneyClient::DB::KEY_UNIQUE_MULT, |
|---|
| 82 |
}, |
|---|
| 83 |
# Each superhero may have an optional real name. |
|---|
| 84 |
real_name => { |
|---|
| 85 |
key => $HoneyClient::DB::KEY_UNIQUE_MULT, |
|---|
| 86 |
}, |
|---|
| 87 |
# If 2 SuperHero Objects have the same 'name' and 'real_name' |
|---|
| 88 |
# fields, then only the first object will be inserted succesfully |
|---|
| 89 |
}, |
|---|
| 90 |
|
|---|
| 91 |
# Each superhero may have optional height and weight stats. |
|---|
| 92 |
int => [ 'height', 'weight' ], |
|---|
| 93 |
|
|---|
| 94 |
# Each superhero must have a primary ability. |
|---|
| 95 |
ref => { |
|---|
| 96 |
primary_ability => { |
|---|
| 97 |
# Reference child object type. |
|---|
| 98 |
objclass=> "HoneyClient::DB::SuperHero::Ability", |
|---|
| 99 |
|
|---|
| 100 |
# This should be required. |
|---|
| 101 |
required => 1, |
|---|
| 102 |
}, |
|---|
| 103 |
}, |
|---|
| 104 |
|
|---|
| 105 |
# Each superhero may have optional abilities. |
|---|
| 106 |
array => { |
|---|
| 107 |
abilities => { |
|---|
| 108 |
# Reference child object type. |
|---|
| 109 |
objclass=> "HoneyClient::DB::SuperHero::Ability", |
|---|
| 110 |
}, |
|---|
| 111 |
}, |
|---|
| 112 |
|
|---|
| 113 |
# Each superhero should have a birth date. |
|---|
| 114 |
timestamp => { |
|---|
| 115 |
birth_date => { |
|---|
| 116 |
required => 1, |
|---|
| 117 |
}, |
|---|
| 118 |
}, |
|---|
| 119 |
); |
|---|
| 120 |
|
|---|
| 121 |
1; |
|---|
| 122 |
|
|---|
| 123 |
=head2 USE SCHEMAS |
|---|
| 124 |
|
|---|
| 125 |
# Now, we start generating data to insert into our database. |
|---|
| 126 |
|
|---|
| 127 |
use HoneyClient::DB::SuperHero; |
|---|
| 128 |
use Data::Dumper; |
|---|
| 129 |
|
|---|
| 130 |
# Create a new superhero. |
|---|
| 131 |
my $hero = { |
|---|
| 132 |
name => 'Superman', |
|---|
| 133 |
real_name => 'Clark Kent', |
|---|
| 134 |
weight => 225, |
|---|
| 135 |
height => 75, |
|---|
| 136 |
birth_date => '1998-06-01 12:34:56', # YYYY-MM-DD HH:MM:SS |
|---|
| 137 |
primary_ability => { |
|---|
| 138 |
name => 'Super Strength', |
|---|
| 139 |
description => 'More powerful than a locomotive.', |
|---|
| 140 |
}, |
|---|
| 141 |
abilities => [ |
|---|
| 142 |
{ |
|---|
| 143 |
name => 'Flight', |
|---|
| 144 |
description => "It's a bird, it's a plane.", |
|---|
| 145 |
}, |
|---|
| 146 |
{ |
|---|
| 147 |
name => 'Heat Vision', |
|---|
| 148 |
recharge_time => 5, # in seconds |
|---|
| 149 |
}, |
|---|
| 150 |
], |
|---|
| 151 |
}; |
|---|
| 152 |
|
|---|
| 153 |
# Instantiate a new SuperHero object. |
|---|
| 154 |
# Upon creation, the data will be checked against the schema. |
|---|
| 155 |
# This call will croak if any errors occur. |
|---|
| 156 |
$hero = HoneyClient::DB::SuperHero->new($hero); |
|---|
| 157 |
|
|---|
| 158 |
# Insert the data into the database. |
|---|
| 159 |
$hero->insert(); |
|---|
| 160 |
|
|---|
| 161 |
# Retrieve the superhero. |
|---|
| 162 |
my $filter = { |
|---|
| 163 |
name => 'Superman', |
|---|
| 164 |
}; |
|---|
| 165 |
|
|---|
| 166 |
# Retrieves rows in the SuperHero table where name is 'Superman'. |
|---|
| 167 |
# NOTE: At this time, the returned data is NOT identical to |
|---|
| 168 |
# the object inserted. |
|---|
| 169 |
my $inserted_hero = HoneyClient::DB::SuperHero->select($filter); |
|---|
| 170 |
|
|---|
| 171 |
# Printing the contents of the returned content should clarify |
|---|
| 172 |
# how the data looks. |
|---|
| 173 |
$Data::Dumper::Indent = 1; |
|---|
| 174 |
$Data::Dumper::Terse = 0; |
|---|
| 175 |
print Dumper($inserted_hero) . "\n"; |
|---|
| 176 |
|
|---|
| 177 |
=head1 DESCRIPTION |
|---|
| 178 |
|
|---|
| 179 |
This library is an abstract class used to access and store HoneyClient |
|---|
| 180 |
within a database. The class is not to be used directly, but can be inherited |
|---|
| 181 |
by sub-classes, in order to indirectly store specific types of data into a |
|---|
| 182 |
database. |
|---|
| 183 |
|
|---|
| 184 |
B<Note>: Any calls made to this library will fail, if a database is not properly |
|---|
| 185 |
described in the <HoneyClient/><DB/> section of the etc/honeyclient.xml |
|---|
| 186 |
configuration file or if the library cannot establish a connection to the |
|---|
| 187 |
database. |
|---|
| 188 |
|
|---|
| 189 |
=head2 SCHEMA DEFINITION |
|---|
| 190 |
|
|---|
| 191 |
The schema for a HoneyClient::DB subclass is created from the B<%fields> |
|---|
| 192 |
variable, a multi-level hash table. |
|---|
| 193 |
|
|---|
| 194 |
=head3 FIRST LEVEL: DATA TYPE |
|---|
| 195 |
|
|---|
| 196 |
The keys at the first level of B<%fields> define the data types to be used |
|---|
| 197 |
for each column, which are named as keys in the second level. The following |
|---|
| 198 |
is a list of acceptable data types: |
|---|
| 199 |
|
|---|
| 200 |
=over 4 |
|---|
| 201 |
|
|---|
| 202 |
=item * B<'int'> |
|---|
| 203 |
|
|---|
| 204 |
An integer. |
|---|
| 205 |
|
|---|
| 206 |
=item * B<'string'> |
|---|
| 207 |
|
|---|
| 208 |
A string no longer than 255 characters. |
|---|
| 209 |
|
|---|
| 210 |
=item * B<'text'> |
|---|
| 211 |
|
|---|
| 212 |
A string no longer than 65,535 characters. |
|---|
| 213 |
|
|---|
| 214 |
=item * B<'timestamp'> |
|---|
| 215 |
|
|---|
| 216 |
An ISO8601 compliant timestamp (i.e., 'MMDDYYYY HH:MM:SS'). |
|---|
| 217 |
|
|---|
| 218 |
=item * B<'array'> |
|---|
| 219 |
|
|---|
| 220 |
An array. Used to represent one-to-many relationships. |
|---|
| 221 |
|
|---|
| 222 |
B<Note>: If this type is specified, then the L<'objclass'> option |
|---|
| 223 |
must be set within each column name. |
|---|
| 224 |
|
|---|
| 225 |
=item * B<'ref'> |
|---|
| 226 |
|
|---|
| 227 |
A reference. Used to represent one-to-one relationships. |
|---|
| 228 |
|
|---|
| 229 |
B<Note>: If this type is specified, then the L<'objclass'> option |
|---|
| 230 |
must be set within each column name. |
|---|
| 231 |
|
|---|
| 232 |
=back |
|---|
| 233 |
|
|---|
| 234 |
=head3 SECOND LEVEL: COLUMN NAMES |
|---|
| 235 |
|
|---|
| 236 |
Column names are defined as keys in the second level of B<%fields>. If |
|---|
| 237 |
each column does not need any special options (e.g., making the column |
|---|
| 238 |
required), then an array reference can hold all the column names. |
|---|
| 239 |
For example, the following schema defines 3 default integer fields: |
|---|
| 240 |
|
|---|
| 241 |
%our fields = { |
|---|
| 242 |
int => [ |
|---|
| 243 |
'col_a', |
|---|
| 244 |
'col_b', |
|---|
| 245 |
'col_c', |
|---|
| 246 |
], |
|---|
| 247 |
}; |
|---|
| 248 |
|
|---|
| 249 |
However, if some of the columns need special options set (e.g., making 'col_b' |
|---|
| 250 |
required), then a sub-hash table should be defined instead, as follows: |
|---|
| 251 |
|
|---|
| 252 |
%our fields = { |
|---|
| 253 |
int => { |
|---|
| 254 |
'col_a' => {}, |
|---|
| 255 |
'col_b' => { |
|---|
| 256 |
'required' => 1, |
|---|
| 257 |
}, |
|---|
| 258 |
'col_c' => {}, |
|---|
| 259 |
}, |
|---|
| 260 |
}; |
|---|
| 261 |
|
|---|
| 262 |
=head3 THIRD LEVEL: OPTIONS |
|---|
| 263 |
|
|---|
| 264 |
If needed, options are defined in the third level of B<%fields>. These |
|---|
| 265 |
options are described as follows: |
|---|
| 266 |
|
|---|
| 267 |
=over 4 |
|---|
| 268 |
|
|---|
| 269 |
=item * B<'check_func'> |
|---|
| 270 |
|
|---|
| 271 |
If defined, then this contains a reference to a subroutine that will |
|---|
| 272 |
verify the actual column data is in a proper format. This overrides the |
|---|
| 273 |
default internal check function for the data type of that column. |
|---|
| 274 |
|
|---|
| 275 |
=item * B<'init_val'> |
|---|
| 276 |
|
|---|
| 277 |
If defined, then this value will be the default value that the database |
|---|
| 278 |
will assign to the column, if empty data is inserted into this column. |
|---|
| 279 |
|
|---|
| 280 |
=item * B<'key'> |
|---|
| 281 |
|
|---|
| 282 |
If defined, then this value creates an index for the column. |
|---|
| 283 |
Possible values are: |
|---|
| 284 |
|
|---|
| 285 |
=over 4 |
|---|
| 286 |
|
|---|
| 287 |
=item * B<$HoneyClient::DB::KEY_INDEX> |
|---|
| 288 |
|
|---|
| 289 |
If set, an index will be created in the database to improve the search |
|---|
| 290 |
time of this column. This option is only recommended for very frequently |
|---|
| 291 |
searched columns. |
|---|
| 292 |
|
|---|
| 293 |
=item * B<$HoneyClient:DB::KEY_UNIQUE> |
|---|
| 294 |
|
|---|
| 295 |
If set, a UNIQUE index is created for the column. If a record is inserted that |
|---|
| 296 |
has a match with this column in a previously existing record, the insert will |
|---|
| 297 |
fail on the database side, but the 'id' of that existing record will be |
|---|
| 298 |
returned. |
|---|
| 299 |
|
|---|
| 300 |
=item * B<$HoneyClient:DB::KEY_UNIQUE_MULT> |
|---|
| 301 |
|
|---|
| 302 |
If set, the column is added to a UNIQUE index comprised of all other columns |
|---|
| 303 |
with the KEY_UNIQUE_MULT key option. This index is used to ensure ALL VALUES for |
|---|
| 304 |
the columns in the index are distinct. An insert of a record matching an |
|---|
| 305 |
existing record will return the ID of that record. |
|---|
| 306 |
|
|---|
| 307 |
=back |
|---|
| 308 |
|
|---|
| 309 |
=item * B<'objclass'> |
|---|
| 310 |
|
|---|
| 311 |
This option is required and only used by the L<'array'> and L<'ref'> |
|---|
| 312 |
data types. The value should be a string which contains the package name of |
|---|
| 313 |
the schema to include as a child. |
|---|
| 314 |
|
|---|
| 315 |
=item * B<'required'> |
|---|
| 316 |
|
|---|
| 317 |
If defined and set to 1, then this option will cause all subsequent |
|---|
| 318 |
B<HoneyClient::DB::*-E<gt>L<new>($data)> operations to fail, if the B<$data> |
|---|
| 319 |
does not contain the required field. |
|---|
| 320 |
|
|---|
| 321 |
=back |
|---|
| 322 |
|
|---|
| 323 |
=head1 DATABASE CONFIGURATION |
|---|
| 324 |
|
|---|
| 325 |
This library expects to connect to a MySQL v5.0 or greater database. |
|---|
| 326 |
To specify which database this library should use, see the |
|---|
| 327 |
<HoneyClient/><DB/> section of the etc/honeyclient.xml configuration file |
|---|
| 328 |
for further details. |
|---|
| 329 |
|
|---|
| 330 |
=cut |
|---|
| 331 |
|
|---|
| 332 |
package HoneyClient::DB; |
|---|
| 333 |
|
|---|
| 334 |
use Data::Dumper qw(Dumper); |
|---|
| 335 |
use strict 'vars', 'subs'; |
|---|
| 336 |
use warnings; |
|---|
| 337 |
|
|---|
| 338 |
BEGIN { |
|---|
| 339 |
|
|---|
| 340 |
|
|---|
| 341 |
use DBI; |
|---|
| 342 |
use Carp (); |
|---|
| 343 |
use HoneyClient::Util::Config; |
|---|
| 344 |
use DateTime::Format::ISO8601; |
|---|
| 345 |
use Math::BigInt; |
|---|
| 346 |
use Log::Log4perl qw(:easy); |
|---|
| 347 |
|
|---|
| 348 |
require Exporter; |
|---|
| 349 |
|
|---|
| 350 |
|
|---|
| 351 |
use sigtrap qw(die untrapped normal-signals error-signals); |
|---|
| 352 |
$SIG{PIPE} = 'IGNORE'; |
|---|
| 353 |
|
|---|
| 354 |
|
|---|
| 355 |
our @ISA = qw(Exporter); |
|---|
| 356 |
our @EXPORT = qw(); |
|---|
| 357 |
our @EXPORT_OK; |
|---|
| 358 |
our $VERSION = 0.99; |
|---|
| 359 |
|
|---|
| 360 |
my $database_version; |
|---|
| 361 |
} |
|---|
| 362 |
|
|---|
| 363 |
|
|---|
| 364 |
our $LOG = get_logger(); |
|---|
| 365 |
|
|---|
| 366 |
our $dbhandle; |
|---|
| 367 |
|
|---|
| 368 |
|
|---|
| 369 |
our ( %_types, %_check, %_required, %_init_val, %_keys, %defaults ); |
|---|
| 370 |
|
|---|
| 371 |
|
|---|
| 372 |
our %fields; |
|---|
| 373 |
|
|---|
| 374 |
|
|---|
| 375 |
our ( $STATUS_DELETED, $STATUS_ADDED, $STATUS_MODIFIED ) = |
|---|
| 376 |
( 0, 1, 2 ); |
|---|
| 377 |
our ( $KEY_INDEX, $KEY_UNIQUE, $KEY_UNIQUE_MULT ) = |
|---|
| 378 |
( 0, 1, 2 ); |
|---|
| 379 |
our $debug = 0; |
|---|
| 380 |
|
|---|
| 381 |
|
|---|
| 382 |
our %config; |
|---|
| 383 |
$config{driver} = "mysql"; |
|---|
| 384 |
$config{host} = getVar( name => "host", namespace => "HoneyClient::DB" ); |
|---|
| 385 |
$config{port} = getVar( name => "port", namespace => "HoneyClient::DB" ); |
|---|
| 386 |
$config{user} = getVar( name => "user", namespace => "HoneyClient::DB" ); |
|---|
| 387 |
$config{pass} = getVar( name => "pass", namespace => "HoneyClient::DB" ); |
|---|
| 388 |
$config{dbname} = getVar( name => "dbname", namespace => "HoneyClient::DB" ); |
|---|
| 389 |
|
|---|
| 390 |
if ( !db_exists(%config) ) { |
|---|
| 391 |
die; |
|---|
| 392 |
} |
|---|
| 393 |
|
|---|
| 394 |
END { |
|---|
| 395 |
$dbhandle->disconnect() if $dbhandle; |
|---|
| 396 |
} |
|---|
| 397 |
|
|---|
| 398 |
=pod |
|---|
| 399 |
|
|---|
| 400 |
=head1 METHODS |
|---|
| 401 |
|
|---|
| 402 |
=head2 Object Creation |
|---|
| 403 |
|
|---|
| 404 |
=over 4 |
|---|
| 405 |
|
|---|
| 406 |
=item new |
|---|
| 407 |
|
|---|
| 408 |
Receives an unblessed hash, imports the schema (if necessary), checks that |
|---|
| 409 |
required fields contain the proper data, and returns the blessed object. |
|---|
| 410 |
|
|---|
| 411 |
It must be called using an object class derived from HoneyClient::DB. |
|---|
| 412 |
For Example: |
|---|
| 413 |
|
|---|
| 414 |
$my_obj = new HoneyClient::DB::SomeObj->new({ |
|---|
| 415 |
field_a => "foo", |
|---|
| 416 |
field_b => "bar" |
|---|
| 417 |
}); |
|---|
| 418 |
|
|---|
| 419 |
=cut |
|---|
| 420 |
|
|---|
| 421 |
sub new { |
|---|
| 422 |
my ( $class, $self ) = @_; |
|---|
| 423 |
|
|---|
| 424 |
bless( $self, $class ); |
|---|
| 425 |
|
|---|
| 426 |
|
|---|
| 427 |
_import_schema($class) if ( !exists( $_types{$class} ) ); |
|---|
| 428 |
|
|---|
| 429 |
|
|---|
| 430 |
my @missing = $self->_check_required(); |
|---|
| 431 |
if ( scalar @missing ) { |
|---|
| 432 |
$LOG->fatal( "Object missing required attribute(s): " |
|---|
| 433 |
. join( ', ', @missing ) |
|---|
| 434 |
. '.' ); |
|---|
| 435 |
Carp::croak( "Object missing required attribute(s): " |
|---|
| 436 |
. join( ', ', @missing ) |
|---|
| 437 |
. '.\n' ); |
|---|
| 438 |
} |
|---|
| 439 |
|
|---|
| 440 |
|
|---|
| 441 |
foreach my $key ( keys %$self ) { |
|---|
| 442 |
eval { |
|---|
| 443 |
if ( $self->{$key} ) |
|---|
| 444 |
{ |
|---|
| 445 |
$self->{$key} = $_check{$class}{$key}->( $self->{$key} ); |
|---|
| 446 |
} |
|---|
| 447 |
}; |
|---|
| 448 |
if ($@) { |
|---|
| 449 |
$LOG->fatal("Invalid Object $key\t$@"); |
|---|
| 450 |
Carp::croak "Invalid Object $key\n\t$@"; |
|---|
| 451 |
} |
|---|
| 452 |
if ( $_types{$class}{$key} =~ m/(array|ref):(.*)/ ) { |
|---|
| 453 |
my $ref = ref( $self->{$key} ); |
|---|
| 454 |
my $childType = $1; |
|---|
| 455 |
my $childClass = $2; |
|---|
| 456 |
if ( $childClass->can('new') ) { |
|---|
| 457 |
if ( $ref eq 'HASH' and $childType eq 'ref' ) { |
|---|
| 458 |
$self->{$key} = $childClass->new( $self->{$key} ); |
|---|
| 459 |
} |
|---|
| 460 |
if ( $ref eq 'ARRAY' and $childType eq 'array' ) { |
|---|
| 461 |
foreach my $obj ( @{ $self->{$key} } ) { |
|---|
| 462 |
$obj = $childClass->new($obj); |
|---|
| 463 |
} |
|---|
| 464 |
} |
|---|
| 465 |
} |
|---|
| 466 |
else { |
|---|
| 467 |
$LOG->fatal("Invalid Object! $childType does not exist"); |
|---|
| 468 |
Carp::croak "Invalid Object! $childType does not exist"; |
|---|
| 469 |
} |
|---|
| 470 |
} |
|---|
| 471 |
} |
|---|
| 472 |
return $self; |
|---|
| 473 |
} |
|---|
| 474 |
|
|---|
| 475 |
|
|---|
| 476 |
|
|---|
| 477 |
sub _check_required { |
|---|
| 478 |
my $self = shift; |
|---|
| 479 |
my $class = ref $self; |
|---|
| 480 |
|
|---|
| 481 |
|
|---|
| 482 |
if ( exists $_required{$class} ) { |
|---|
| 483 |
my @missing; |
|---|
| 484 |
foreach ( keys %{ $_required{$class} } ) { |
|---|
| 485 |
push( @missing, $_ ) |
|---|
| 486 |
if ( !defined( $self->{$_} ) or ( $self->{$_} eq "" ) ); |
|---|
| 487 |
} |
|---|
| 488 |
return @missing; |
|---|
| 489 |
} |
|---|
| 490 |
return; |
|---|
| 491 |
} |
|---|
| 492 |
|
|---|
| 493 |
sub _import_schema { |
|---|
| 494 |
my $class = shift; |
|---|
| 495 |
my $schema = \%{ $class . "::fields" }; |
|---|
| 496 |
|
|---|
| 497 |
|
|---|
| 498 |
while ( my ( $type, $attrib ) = each(%$schema) ) { |
|---|
| 499 |
my $ref = ref $attrib; |
|---|
| 500 |
|
|---|
| 501 |
|
|---|
| 502 |
if ( $ref eq 'ARRAY' ) { |
|---|
| 503 |
foreach ( @{$attrib} ) { |
|---|
| 504 |
$_types{$class}{$_} = $type; |
|---|
| 505 |
if ( $type =~ m/(ref|array)/ ) { |
|---|
| 506 |
delete $_types{$class}; |
|---|
| 507 |
$LOG->fatal("Invalid Object Type. ref AND array types must " |
|---|
| 508 |
. "be defined as a hash containing 'objclass'"); |
|---|
| 509 |
Carp::croak "Invalid Object Type. ref AND array types must " |
|---|
| 510 |
. "be defined as a hash containing 'objclass'"; |
|---|
| 511 |
} |
|---|
| 512 |
$_check{$class}{$_} = $defaults{$type}{check_func} |
|---|
| 513 |
or $_check{$class}{$_} = \&check_nothing; |
|---|
| 514 |
} |
|---|
| 515 |
} |
|---|
| 516 |
|
|---|
| 517 |
|
|---|
| 518 |
elsif ( $ref eq 'HASH' ) { |
|---|
| 519 |
while ( my ( $a, $opts ) = each %$attrib ) { |
|---|
| 520 |
$_types{$class}{$a} = $type; |
|---|
| 521 |
if ( $opts->{required} ) { |
|---|
| 522 |
$_required{$class}{$a} = 1; |
|---|
| 523 |
} |
|---|
| 524 |
|
|---|
| 525 |
|
|---|
| 526 |
if ( $type =~ m/^(array|ref)$/ ) { |
|---|
| 527 |
if ( !exists $opts->{objclass} ) { |
|---|
| 528 |
$LOG->fatal("$1 of unknown class: $a"); |
|---|
| 529 |
Carp::croak "$1 of unknown class: $a"; |
|---|
| 530 |
} |
|---|
| 531 |
if ( !exists $_types{ $opts->{objclass} } ) { |
|---|
| 532 |
_import_schema( $opts->{objclass} ); |
|---|
| 533 |
} |
|---|
| 534 |
$_types{$class}{$a} .= ':' . $opts->{objclass}; |
|---|
| 535 |
} |
|---|
| 536 |
|
|---|
| 537 |
|
|---|
| 538 |
if ( $opts->{check_func} ) { |
|---|
| 539 |
$_check{$class}{$a} = $opts->{check_func}; |
|---|
| 540 |
} |
|---|
| 541 |
else { |
|---|
| 542 |
$_check{$class}{$a} = $defaults{$type}{check_func} |
|---|
| 543 |
or $_check{$class}{$a} = \&check_nothing; |
|---|
| 544 |
} |
|---|
| 545 |
|
|---|
| 546 |
|
|---|
| 547 |
if ( $opts->{key} ) { |
|---|
| 548 |
$_keys{$class}{$a} = $opts->{key}; |
|---|
| 549 |
} |
|---|
| 550 |
if ( $opts->{init_val} ) { |
|---|
| 551 |
$_init_val{$class}{$a} = $opts->{key}; |
|---|
| 552 |
} |
|---|
| 553 |
} |
|---|
| 554 |
} |
|---|
| 555 |
else { |
|---|
| 556 |
$LOG->warn("$class\{$type\} is defined improperly"); |
|---|
| 557 |
} |
|---|
| 558 |
} |
|---|
| 559 |
|
|---|
| 560 |
|
|---|
| 561 |
|
|---|
| 562 |
if (!$class->deploy_table()) { |
|---|
| 563 |
$LOG->fatal("${class}->_import_schema: " . "Failed to deploy table"); |
|---|
| 564 |
Carp::croak("${class}->_import_schema: " . "Failed to deploy table"); |
|---|
| 565 |
} |
|---|
| 566 |
} |
|---|
| 567 |
|
|---|
| 568 |
=back |
|---|
| 569 |
|
|---|
| 570 |
=head2 Database Operations |
|---|
| 571 |
|
|---|
| 572 |
=over 4 |
|---|
| 573 |
|
|---|
| 574 |
=item insert |
|---|
| 575 |
|
|---|
| 576 |
Creates and executes a SQL INSERT statement for the referenced object. The |
|---|
| 577 |
object must be initialized at the time this method is called. |
|---|
| 578 |
|
|---|
| 579 |
$my_obj->insert(); |
|---|
| 580 |
|
|---|
| 581 |
B<Input> |
|---|
| 582 |
|
|---|
| 583 |
There are no parameters, however the calling object is used as input for the |
|---|
| 584 |
insert operation. |
|---|
| 585 |
|
|---|
| 586 |
B<Return Value> |
|---|
| 587 |
|
|---|
| 588 |
Returns the 'id' of the (parent) object inserted. |
|---|
| 589 |
|
|---|
| 590 |
=cut |
|---|
| 591 |
|
|---|
| 592 |
sub insert { |
|---|
| 593 |
my $obj = shift; |
|---|
| 594 |
my $id = undef; |
|---|
| 595 |
|
|---|
| 596 |
$dbhandle = HoneyClient::DB::_connect(%config); |
|---|
| 597 |
|
|---|
| 598 |
|
|---|
| 599 |
$LOG->debug("Attempting insert operation."); |
|---|
| 600 |
eval { $id = _insert( $obj, undef ); }; |
|---|
| 601 |
if ($@) { |
|---|
| 602 |
$LOG->warn("insert failed, Rolling Back: $@"); |
|---|
| 603 |
$dbhandle->rollback(); |
|---|
| 604 |
} |
|---|
| 605 |
else { |
|---|
| 606 |
$dbhandle->commit(); |
|---|
| 607 |
} |
|---|
| 608 |
$dbhandle->disconnect() if $dbhandle; |
|---|
| 609 |
return $id; |
|---|
| 610 |
} |
|---|
| 611 |
|
|---|
| 612 |
|
|---|
| 613 |
|
|---|
| 614 |
sub _insert { |
|---|
| 615 |
my ( $obj, $fk_col, $fk_id ) = @_; |
|---|
| 616 |
my $ref = ref $obj; |
|---|
| 617 |
|
|---|
| 618 |
if ( $ref eq 'ARRAY' ) { |
|---|
| 619 |
return _insert_array( $obj, $fk_col, $fk_id ); |
|---|
| 620 |
} |
|---|
| 621 |
elsif ( exists $_types{$ref} ) { |
|---|
| 622 |
return _insert_obj( $obj, $fk_col, $fk_id ); |
|---|
| 623 |
} |
|---|
| 624 |
elsif ($ref) { |
|---|
| 625 |
$LOG->warn("Can't insert object of type $ref"); |
|---|
| 626 |
} |
|---|
| 627 |
else { |
|---|
| 628 |
$LOG->warn("Attempted to insert scalar value into the database"); |
|---|
| 629 |
} |
|---|
| 630 |
return undef; |
|---|
| 631 |
} |
|---|
| 632 |
|
|---|
| 633 |
sub _insert_array { |
|---|
| 634 |
my ( $obj, $fk_col, $fk_id ) = @_; |
|---|
| 635 |
my @entries; |
|---|
| 636 |
foreach (@$obj) { |
|---|
| 637 |
my $id = _insert( $_, $fk_col, $fk_id ); |
|---|
| 638 |
ref($id) eq 'ARRAY' ? push( @entries, @$id ) : push( @entries, $id ); |
|---|
| 639 |
} |
|---|
| 640 |
return \@entries; |
|---|
| 641 |
} |
|---|
| 642 |
|
|---|
| 643 |
sub _insert_obj { |
|---|
| 644 |
my ( $obj, $fk_col, $fk_id ) = @_; |
|---|
| 645 |
my ( $class, $table ) = ( ref($obj), _get_table($obj) ); |
|---|
| 646 |
my ( $id, %insert, %index, %children ); |
|---|
| 647 |
|
|---|
| 648 |
|
|---|
| 649 |
while ( my ( $col, $data ) = each %$obj ) { |
|---|
| 650 |
if ( !$_types{$class}{$col} ) { |
|---|
| 651 |
$LOG->warn("$col=>$data is not a valid field in $class"); |
|---|
| 652 |
delete $obj->{$col}; |
|---|
| 653 |
} |
|---|
| 654 |
|
|---|
| 655 |
elsif ( $_types{$class}{$col} =~ m/(array)/ ) { |
|---|
| 656 |
$children{$col} = $data; |
|---|
| 657 |
} |
|---|
| 658 |
|
|---|
| 659 |
elsif ( $_types{$class}{$col} =~ m/ref:(.*)/ ) { |
|---|
| 660 |
if ( my $ft = $1->_get_table() ) { |
|---|
| 661 |
$insert{ $ft . '_fk' } = _insert($data); |
|---|
| 662 |
} |
|---|
| 663 |
} |
|---|
| 664 |
|
|---|
| 665 |
else { |
|---|
| 666 |
$insert{$col} = $dbhandle->quote($data); |
|---|
| 667 |
} |
|---|
| 668 |
} |
|---|
| 669 |
|
|---|
| 670 |
|
|---|
| 671 |
$insert{$fk_col} = $fk_id if ( $fk_col && $fk_id ); |
|---|
| 672 |
|
|---|
| 673 |
|
|---|
| 674 |
my $sql = |
|---|
| 675 |
"INSERT INTO $table (" |
|---|
| 676 |
. join( ',', keys %insert ) |
|---|
| 677 |
. ") VALUES (" |
|---|
| 678 |
. join( ',', values(%insert) ) . ')'; |
|---|
| 679 |
eval { |
|---|
| 680 |
$LOG->debug($sql); |
|---|
| 681 |
$dbhandle->do($sql); |
|---|
| 682 |
}; |
|---|
| 683 |
|
|---|
| 684 |
|
|---|
| 685 |
if ($@) { |
|---|
| 686 |
if ( $dbhandle->err == 1062 ) { |
|---|
| 687 |
my $filter; |
|---|
| 688 |
while ( my ( $col, $key_type ) = each %{ $_keys{$class} } ) { |
|---|
| 689 |
if ( $key_type == $KEY_UNIQUE || $key_type == $KEY_UNIQUE_MULT ) |
|---|
| 690 |
{ |
|---|
| 691 |
$filter->{$col} = $obj->{$col}; |
|---|
| 692 |
} |
|---|
| 693 |
} |
|---|
| 694 |
my @rows = $class->_select( $filter, 'id' ); |
|---|
| 695 |
if (scalar @rows) { |
|---|
| 696 |
$id = $rows[0]->{id}; |
|---|
| 697 |
} |
|---|
| 698 |
else { |
|---|
| 699 |
$LOG->fatal("Error: Can't resolve duplicate records\t" . $dbhandle->err . ": $@"); |
|---|
| 700 |
Carp::croak("Error: Can't resolve duplicate records\n\t" . $dbhandle->err . ": $@"); |
|---|
| 701 |
} |
|---|
| 702 |
} |
|---|
| 703 |
else { |
|---|
| 704 |
$LOG->fatal("Error: " . $dbhandle->err . ": $@"); |
|---|
| 705 |
Carp::croak("Error: " . $dbhandle->err . ": $@"); |
|---|
| 706 |
} |
|---|
| 707 |
} |
|---|
| 708 |
else { |
|---|
| 709 |
$id = $dbhandle->{'mysql_insertid'}; |
|---|
| 710 |
} |
|---|
| 711 |
|
|---|
| 712 |
|
|---|
| 713 |
foreach ( keys %children ) { |
|---|
| 714 |
my $rv = _insert( $children{$_}, $table . '_fk', $id ); |
|---|
| 715 |
|
|---|
| 716 |
|
|---|
| 717 |
} |
|---|
| 718 |
return $id; |
|---|
| 719 |
} |
|---|
| 720 |
|
|---|
| 721 |
=item select |
|---|
| 722 |
|
|---|
| 723 |
Creates and executes a SQL SELECT statement and returns an array of hash refs |
|---|
| 724 |
containing result rows. If no fields are specified, all fields are returned. |
|---|
| 725 |
The first parameter is a hash reference to a query filter. The filter may be |
|---|
| 726 |
followed by a list of field names to retrieve. |
|---|
| 727 |
|
|---|
| 728 |
@my_objects = HoneyClient::DB::SomeObj->select($my_filter,@columns); |
|---|
| 729 |
|
|---|
| 730 |
or |
|---|
| 731 |
|
|---|
| 732 |
$my_objects_ref = HoneyClient::DB::SomeObj->select($my_filter,@columns); |
|---|
| 733 |
|
|---|
| 734 |
B<Input> |
|---|
| 735 |
|
|---|
| 736 |
The first parameter is a hash_ref containing a filter. The filter is used to |
|---|
| 737 |
generate a SQL query. |
|---|
| 738 |
|
|---|
| 739 |
The filter is followed by a list of column to select. |
|---|
| 740 |
|
|---|
| 741 |
Both parameters are optional. If the first parameter is a scalar, it is assumed |
|---|
| 742 |
that there is no filter. |
|---|
| 743 |
|
|---|
| 744 |
B<**NOTE**> Currently it is not possible to include a child object (ref or |
|---|
| 745 |
array type) in the filter. Only 'id's of child objects are accepted. |
|---|
| 746 |
|
|---|
| 747 |
B<Return Value> |
|---|
| 748 |
|
|---|
| 749 |
Returns the 'id' of the (parent) object inserted. |
|---|
| 750 |
|
|---|
| 751 |
=cut |
|---|
| 752 |
|
|---|
| 753 |
sub select { |
|---|
| 754 |
my @results; |
|---|
| 755 |
eval { |
|---|
| 756 |
$dbhandle = HoneyClient::DB::_connect(%config); |
|---|
| 757 |
@results = _select(@_); |
|---|
| 758 |
$dbhandle->disconnect() if $dbhandle; |
|---|
| 759 |
}; |
|---|
| 760 |
if ($@) { |
|---|
| 761 |
$LOG->fatal("select error: $@"); |
|---|
| 762 |
Carp::croak("select error: $@"); |
|---|
| 763 |
@results = (); |
|---|
| 764 |
} |
|---|
| 765 |
wantarray ? return @results : return \@results; |
|---|
| 766 |
} |
|---|
| 767 |
|
|---|
| 768 |
sub _select { |
|---|
| 769 |
my ( $class, $filter, @fields ) = @_; |
|---|
| 770 |
|
|---|
| 771 |
|
|---|
| 772 |
if ( $filter && ref($filter) ne 'HASH' ) { |
|---|
| 773 |
unshift( @fields, $filter ); |
|---|
| 774 |
$filter = {}; |
|---|
| 775 |
} |
|---|
| 776 |
|
|---|
| 777 |
|
|---|
| 778 |
my $sql = "SELECT "; |
|---|
| 779 |
$sql .= |
|---|
| 780 |
( |
|---|
| 781 |
scalar(@fields) |
|---|
| 782 |
? join( ',', @fields ) |
|---|
| 783 |
: join( ',', $class->get_fields() ) ); |
|---|
| 784 |
$sql .= " FROM " . $class->_get_table() . " WHERE "; |
|---|
| 785 |
my @conditions; |
|---|
| 786 |
|
|---|
| 787 |
|
|---|
| 788 |
while ( my ( $col, $data ) = each %$filter ) { |
|---|
| 789 |
if ( !exists $_types{$class}{$col} ) { |
|---|
| 790 |
|
|---|
| 791 |
|
|---|
| 792 |
} |
|---|
| 793 |
elsif ( $_types{$class}{$col} =~ /array:.*/ ) { |
|---|
| 794 |
@$data = map $dbhandle->quote($_), @$data; |
|---|
| 795 |
push( @conditions, 'id IN (' . join( ',', @$data ) . ')' ) |
|---|
| 796 |
if ( scalar(@$data) ); |
|---|
| 797 |
} |
|---|
| 798 |
elsif ( $_types{$class}{$col} =~ /ref:(.*)/ ) { |
|---|
| 799 |
push @conditions, |
|---|
| 800 |
( $1->_get_table() . '_fk=' . $dbhandle->quote($data) ); |
|---|
| 801 |
} |
|---|
| 802 |
else { |
|---|
| 803 |
push @conditions, ( $col . '=' . $dbhandle->quote($data) ); |
|---|
| 804 |
} |
|---|
| 805 |
} |
|---|
| 806 |
$sql .= join( ' AND ', @conditions ); |
|---|
| 807 |
|
|---|
| 808 |
$LOG->debug($sql); |
|---|
| 809 |
my @results = (); |
|---|
| 810 |
my $sth = $dbhandle->prepare($sql); |
|---|
| 811 |
$sth->execute(); |
|---|
| 812 |
while ( my $row = $sth->fetchrow_hashref() ) { |
|---|
| 813 |
push @results, $row; |
|---|
| 814 |
} |
|---|
| 815 |
|
|---|
| 816 |
return @results; |
|---|
| 817 |
} |
|---|
| 818 |
|
|---|
| 819 |
sub includes { |
|---|
| 820 |
my @ids; |
|---|
| 821 |
foreach (@_) { |
|---|
| 822 |
push( @ids, $_ ) if ( !( ref $_ ) && ( $_ =~ /^\d+$/ ) ); |
|---|
| 823 |
if ( exists $_->{id} ) { |
|---|
| 824 |
push @ids, $_->{id}; |
|---|
| 825 |
} |
|---|
| 826 |
else { |
|---|
| 827 |
next; |
|---|
| 828 |
} |
|---|
| 829 |
} |
|---|
| 830 |
return \@ids; |
|---|
| 831 |
} |
|---|
| 832 |
|
|---|
| 833 |
sub _get_table { |
|---|
| 834 |
my $class = shift; |
|---|
| 835 |
my ( $table, $ref ); |
|---|
| 836 |
( $ref = ref($class) ) ? ( $table = $ref ) : ( $table = $class ); |
|---|
| 837 |
$table =~ s/HoneyClient::DB:://g; |
|---|
| 838 |
$table =~ s/::/_/g; |
|---|
| 839 |
$table; |
|---|
| 840 |
} |
|---|
| 841 |
|
|---|
| 842 |
=back |
|---|
| 843 |
|
|---|
| 844 |
=head2 Utility Functions |
|---|
| 845 |
|
|---|
| 846 |
=over 4 |
|---|
| 847 |
|
|---|
| 848 |
=item get_fields |
|---|
| 849 |
|
|---|
| 850 |
Retrieves a list of fields as defined by the schema, excluding array fields. Can |
|---|
| 851 |
be used in conjunction with calls to HoneyClient::DB::select to execute a SELECT |
|---|
| 852 |
query that retrieves all fields. |
|---|
| 853 |
|
|---|
| 854 |
=back |
|---|
| 855 |
|
|---|
| 856 |
=cut |
|---|
| 857 |
|
|---|
| 858 |
sub get_fields { |
|---|
| 859 |
my $self = shift; |
|---|
| 860 |
my $class = ( ref($self) or $self ); |
|---|
| 861 |
|
|---|
| 862 |
my @fields; |
|---|
| 863 |
|
|---|
| 864 |
push @fields,'id'; |
|---|
| 865 |
|
|---|
| 866 |
foreach ( keys %{ $_types{$class} } ) { |
|---|
| 867 |
if ( $_types{$class}{$_} !~ m/array:.*/ ) { |
|---|
| 868 |
if ( $_types{$class}{$_} =~ m/ref:(.*)/ ) { |
|---|
| 869 |
push( @fields, $1->_get_table . '_fk' ); |
|---|
| 870 |
} |
|---|
| 871 |
else { push @fields, $_; } |
|---|
| 872 |
} |
|---|
| 873 |
} |
|---|
| 874 |
return @fields; |
|---|
| 875 |
} |
|---|
| 876 |
|
|---|
| 877 |
sub _connect { |
|---|
| 878 |
my %conf = @_; |
|---|
| 879 |
my $dsn = "DBI:" |
|---|
| 880 |
. $conf{driver} |
|---|
| 881 |
. ":database=" |
|---|
| 882 |
. $conf{dbname} |
|---|
| 883 |
. ";host=" |
|---|
| 884 |
. $conf{host} |
|---|
| 885 |
. ";port=" |
|---|
| 886 |
. $conf{port}; |
|---|
| 887 |
my $dbh = |
|---|
| 888 |
DBI->connect_cached( $dsn, $conf{user}, $conf{pass}, |
|---|
| 889 |
{ 'RaiseError' => 1, 'PrintError' => 0 } ); |
|---|
| 890 |
|
|---|
| 891 |
if ( $dbh ne '' ) { |
|---|
| 892 |
$dbh->{'AutoCommit'} = 0; |
|---|
| 893 |
|
|---|
| 894 |
|
|---|
| 895 |
|
|---|
| 896 |
return $dbh; |
|---|
| 897 |
} |
|---|
| 898 |
else { |
|---|
| 899 |
$LOG->fatal("__PACKAGE__->_Connect Failed: $DBI::errstr"); |
|---|
| 900 |
Carp::croak "__PACKAGE__->_Connect Failed: $DBI::errstr"; |
|---|
| 901 |
} |
|---|
| 902 |
} |
|---|
| 903 |
|
|---|
| 904 |
|
|---|
| 905 |
|
|---|
| 906 |
sub deploy_table { |
|---|
| 907 |
my $class = shift; |
|---|
| 908 |
my $table = $class->_get_table(); |
|---|
| 909 |
|
|---|
| 910 |
|
|---|
| 911 |
if ( table_exists($table) ) { |
|---|
| 912 |
if ($debug) { |
|---|
| 913 |
$LOG->warn("${class}->deploy_table: Table $table exists!!"); |
|---|
| 914 |
} |
|---|
| 915 |
return 1; |
|---|
| 916 |
} |
|---|
| 917 |
$dbhandle = HoneyClient::DB::_connect(%config); |
|---|
| 918 |
my ( @mult_unique_key, @foreign_keys, %arrays ); |
|---|
| 919 |
|
|---|
| 920 |
|
|---|
| 921 |
my $sql = "CREATE TABLE $table (\n" |
|---|
| 922 |
. "\tid INT UNSIGNED AUTO_INCREMENT PRIMARY KEY"; |
|---|
| 923 |
|
|---|
| 924 |
|
|---|
| 925 |
while ( my ( $col, $type ) = each %{ $_types{$class} } ) |
|---|
| 926 |
{ |
|---|
| 927 |
|
|---|
| 928 |
if ( $type =~ m/ref:(.*)/ ) { |
|---|
| 929 |
$sql .= ",\n\t" . $1->_get_table() . "_fk INT UNSIGNED"; |
|---|
| 930 |
push @foreign_keys, $1; |
|---|
| 931 |
} |
|---|
| 932 |
|
|---|
| 933 |
|
|---|
| 934 |
elsif ( $type =~ m/array:(.*)/ ) { |
|---|
| 935 |
$arrays{$col} = $1; |
|---|
| 936 |
next; |
|---|
| 937 |
} |
|---|
| 938 |
|
|---|
| 939 |
|
|---|
| 940 |
else { |
|---|
| 941 |
$sql .= ",\n\t$col " . _get_db_type($type); |
|---|
| 942 |
} |
|---|
| 943 |
|
|---|
| 944 |
|
|---|
| 945 |
if ( exists $_required{$class} && $_required{$class}{$col} ) { |
|---|
| 946 |
$sql .= " NOT NULL"; |
|---|
| 947 |
} |
|---|
| 948 |
|
|---|
| 949 |
|
|---|
| 950 |
|
|---|