Codebase list libdbix-sequence-perl / debian/latest Sequence.pm
debian/latest

Tree @debian/latest (Download .tar.gz)

Sequence.pm @debian/latestraw · history · blame

  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
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
package DBIx::Sequence;

use strict;
use vars qw($VERSION);
$VERSION = '1.5';

use DBI;
use Carp;

use constant COLUMN_PREFIX => '';
use constant DEFAULT_INIT_VALUE => 1;
use constant DEFAULT_ALLOW_ID_REUSE => 1;
use constant DEBUG_LEVEL => 0;
use constant DEFAULT_STATE_TABLE => 'dbix_sequence_state';
use constant DEFAULT_RELEASE_TABLE => 'dbix_sequence_release';

sub new
{
	my $class_name = shift;
	my $args = shift;

	my $self = {};
	$self = bless $self, $class_name;

	$self->{_dbh} = $args->{dbh} || $self->getDbh($args) || die 'Cannot get Database handle';

	$self->{state_table} = $args->{state_table};
	$self->{release_table} = $args->{release_table};
	$self->{_arg_reuse} = $args->{allow_id_reuse} if(exists $args->{allow_id_reuse});

	delete $self->{db_user};
	delete $self->{db_pw};
	delete $self->{db_dsn};

	$self->_InitQueries();


	return $self; 
}

sub getDbh
{
	my $self = shift;
	my $args = shift;

	return DBI->connect($args->{db_dsn}, $args->{db_user}, $args->{db_pw}, {
								RaiseError => 0,
								PrintError => 0,
								AutoCommit => 1,
								Warn => 0, }) || croak __PACKAGE__.": $DBI::errstr";
}


sub Next
{
	my $self = shift;
	my $dataset = shift;

	croak "No dataset specified" if not defined $dataset;

	print STDERR "Request of Next() id\n" if $self->DEBUG_LEVEL();

	my $current_sth = $self->{_current_sth};
	my $init_sth = $self->{_init_sth};

	if($self->_Create_Dataset($dataset))
	{
		return $self->DEFAULT_INIT_VALUE();
	}

	if($self->_AllowedReuse())
	{
		my $released_ids_sth = $self->{_released_ids_sth};

		$released_ids_sth->execute($dataset);

		while(my $released_id = $released_ids_sth->fetchrow())
		{
			if($self->_release_race_for($dataset, ( $released_id =~ m/^(\d+)$/ )[0] ))
			{
				print STDERR "Returning released id $released_id\n" if $self->DEBUG_LEVEL();
				$released_ids_sth->finish;
				return $released_id;
			}
		}
	}
				
	my $unique_id = $self->_race_for($dataset);
	if(!$unique_id)
	{
		croak __PACKAGE__." was unable to generate a unique id for ".$dataset."\n";
	}

	print STDERR "Returning new unique id $unique_id\n" if $self->DEBUG_LEVEL();
	return $unique_id;
}

sub Currval
{
	my $self = shift;
	my $dataset = shift;

	croak "No dataset specified" if !$dataset;

	my $current_sth = $self->{_current_sth};
	$current_sth->execute($dataset) || croak __PACKAGE__.": $DBI::errstr";
	my ($c_dataset, $current_id) = $current_sth->fetchrow(); $current_sth->finish;

	print STDERR "Returning CURRVAL $current_id for $c_dataset\n";
	return $current_id;
}	


sub Release
{
	my $self = shift;
	my $dataset = shift;
	my $release_id = shift;

	croak "No dataset specified" if !$dataset;
	croak __PACKAGE__." NO ID specified for Release()" if not defined $release_id;

	print STDERR "Asked to release id $release_id in dataset $dataset\n" if $self->DEBUG_LEVEL();


	if($self->_AllowedReuse())
	{	
		my $release_id_sth = $self->{_release_id_sth};
	
		if($release_id_sth->execute($dataset, $release_id) ne 'OEO')
		{
			print STDERR "Release successful.\n" if $self->DEBUG_LEVEL();
			return 1;
		}	
		return 0;
	}
	else
	{
		warn "Release() of ID not permitted by class ".__PACKAGE__; 
	}
}

	
sub Delete_Dataset
{
	my $self = shift;
	my $dataset = shift;

	croak "No dataset specified" if !$dataset;	


	my $delete_state_sth = $self->{_delete_state_sth};
	my $delete_release_sth = $self->{_delete_release_sth};

	print STDERR "Deleting dataset ".$dataset."\n" if $self->DEBUG_LEVEL();

	$delete_state_sth->execute($dataset) || croak __PACKAGE__.": $DBI::errstr";
	$delete_release_sth->execute($dataset) || croak __PACKAGE__.": $DBI::errstr";

	print STDERR "Deletion successul\n" if $self->DEBUG_LEVEL();

	return 1;
}

sub Bootstrap
{
	my $self = shift;
	my $dataset = shift;
	my $data_table = shift;
	my $data_field = shift;

	croak "No dataset specified" if !$dataset;
	croak "No data_table to Bootstrap()" if(!$data_table);
	croak "No data_field to Bootstrap()" if(!$data_field);


	print STDERR "Bootstrapping dataset ". $dataset." with $data_table and $data_field\n" if $self->DEBUG_LEVEL();

	my $bootstrap_query = "SELECT
					MAX($data_field)
				FROM
					".$data_table;
	
	print STDERR "\n\n", $bootstrap_query, "\n\n" if $self->DEBUG_LEVEL();

	my $bootstrap_sth = $self->{_dbh}->prepare($bootstrap_query) || croak __PACKAGE__.": $DBI::errstr";
	$bootstrap_sth->execute() || croak __PACKAGE__.": $DBI::errstr";

	my $bootstrap_id = $bootstrap_sth->fetchrow(); $bootstrap_sth->finish;

	croak "Bootstrap() failed" if(!$bootstrap_id);

	$self->_Create_Dataset($dataset);

	print STDERR "Bootstrap successfull.\n" if $self->DEBUG_LEVEL();

	my $next_id = $self->_race_for($dataset, $bootstrap_id + 1);

	print STDERR "Bootstrap next id is : $next_id\n" if $self->DEBUG_LEVEL();

	return $next_id;
}

sub _Create_Dataset
{
	my $self = shift;
	my $dataset = shift;

	croak "No dataset specified" if !$dataset;

	my $current_sth = $self->{_current_sth};
	my $init_sth = $self->{_init_sth};

        $current_sth->execute($dataset) || croak __PACKAGE__.": $DBI::errstr";
        my ($c_dataset, $current_id) = $current_sth->fetchrow(); $current_sth->finish;

        if(!$c_dataset)
        {
                $init_sth->execute($dataset,$self->DEFAULT_INIT_VALUE()) || croak __PACKAGE__.": $DBI::errstr";
                return $self->DEFAULT_INIT_VALUE();
        }
	else { return 0; }
}	

sub STATE_TABLE 
{
	my $self = shift;

	croak "Self not defined!" if not defined $self;

	return $self->{state_table} || $self->DEFAULT_STATE_TABLE();
}

sub RELEASE_TABLE
{
	my $self = shift;
	
	croak "Self not defined!" if not defined $self;
	
	return $self->{release_table} || $self->DEFAULT_RELEASE_TABLE();
}
	
sub _AllowedReuse
{
	my $self = shift;

	if(exists $self->{_arg_reuse})
	{
		return undef if($self->{_arg_reuse} =~ /no/i);
		return 1 if($self->{_arg_reuse});
		return undef;
	}
	else
	{
		return $self->DEFAULT_ALLOW_ID_REUSE();
	}
}
		
sub _race_for
{
	my $self = shift;
	my $dataset = shift;
	my $race_for_id = shift;

	croak "No dataset specified" if !$dataset;

	my $current_sth = $self->{_current_sth};
	my $race_sth = $self->{_race_sth};

	my $unique_id;
	my $got_id = 0;
	my $current_id;
	while($got_id == 0)
	{
		$current_sth->execute($dataset) || croak __PACKAGE__.": $DBI::errstr"; 
		$current_id = ($current_sth->fetchrow() =~ m/^(\d+)$/ )[0]; $current_sth->finish;

		if(!$race_for_id || $race_for_id <= $current_id)
		{ 
			$race_for_id = $current_id + 1; 
		}
			
		if ($race_sth->execute(($race_for_id), $dataset, $current_id) ne '0E0')
                {
			$unique_id = $race_for_id;
                        $got_id = 1;
                }
	}	

	return $unique_id;
}

sub _release_race_for
{
	my $self = shift;
	my $dataset = shift;
	my $release_id = shift;

	croak "No dataset specified" if !$dataset;
	croak "No ID specified for release race" if not defined $release_id;

	if($self->{_race_release_sth}->execute($dataset, $release_id) ne 'OEO')
	{
		return 1;
	}
	return 0;
}
		
sub _InitQueries
{
	my $self = shift;

	
	my $current_query = "SELECT
				".$self->COLUMN_PREFIX()."dataset,
				".$self->COLUMN_PREFIX()."state_id
				FROM
					".$self->STATE_TABLE()."
				WHERE
					".$self->COLUMN_PREFIX()."dataset = ?";

	print STDERR "\n\n", $current_query, "\n\n" if $self->DEBUG_LEVEL();

	$self->{_current_sth} = $self->{_dbh}->prepare_cached($current_query) || croak __PACKAGE__.": $DBI::errstr";

	my $init_query = "INSERT INTO
				".$self->STATE_TABLE()." (
								".$self->COLUMN_PREFIX()."dataset,
								".$self->COLUMN_PREFIX()."state_id
							) values (?,?)";

	print STDERR "\n\n", $init_query, "\n\n" if $self->DEBUG_LEVEL();

	$self->{_init_sth} = $self->{_dbh}->prepare_cached($init_query) || croak __PACKAGE__.": $DBI::errstr"; 

		
	my $race_query = "UPDATE
				".$self->STATE_TABLE()."
				SET
					".$self->COLUMN_PREFIX()."state_id  = ?
				WHERE
					".$self->COLUMN_PREFIX()."dataset = ?
				AND
					".$self->COLUMN_PREFIX()."state_id = ?";


	print STDERR "\n\n", $race_query, "\n\n" if $self->DEBUG_LEVEL();

	$self->{_race_sth} = $self->{_dbh}->prepare_cached($race_query) || croak __PACKAGE__.": $DBI::errstr"; 

			
	my $release_query = "DELETE FROM
					".$self->RELEASE_TABLE()."
				WHERE
					".$self->COLUMN_PREFIX()."dataset = ?
				AND	
					".$self->COLUMN_PREFIX()."released_id = ?";


	print STDERR "\n\n", $release_query, "\n\n" if $self->DEBUG_LEVEL();

	$self->{_race_release_sth} = $self->{_dbh}->prepare_cached($release_query) || croak __PACKAGE__.": $DBI::errstr";


	my $released_ids_query = "SELECT
					".$self->COLUMN_PREFIX()."released_id
				FROM
					".$self->RELEASE_TABLE()."
				WHERE
					".$self->COLUMN_PREFIX()."dataset = ?";

	print STDERR "\n\n", $released_ids_query, "\n\n" if $self->DEBUG_LEVEL();

	$self->{_released_ids_sth} = $self->{_dbh}->prepare_cached($released_ids_query) || croak __PACKAGE__.": $DBI::errstr";
			


	my $release_id_query = "INSERT INTO
					".$self->RELEASE_TABLE()."
					(
					".$self->COLUMN_PREFIX()."dataset,
					".$self->COLUMN_PREFIX()."released_id
					) values (?,?)";

	print STDERR "\n\n", $release_id_query, "\n\n" if $self->DEBUG_LEVEL();

	$self->{_release_id_sth} = $self->{_dbh}->prepare_cached($release_id_query) || croak __PACKAGE__.": $DBI::errstr";


	my $delete_state_query = "DELETE FROM 
						".$self->STATE_TABLE()."	
					WHERE
						dataset = ?";

	print STDERR "\n\n", $delete_state_query, "\n\n" if $self->DEBUG_LEVEL();

	$self->{_delete_state_sth} = $self->{_dbh}->prepare_cached($delete_state_query) || croak __PACKAGE__.": $DBI::errstr";


	my $delete_release_query = "DELETE FROM 
						".$self->RELEASE_TABLE()."
					WHERE
						dataset = ?";
	
	print STDERR "\n\n", $delete_release_query, "\n\n" if $self->DEBUG_LEVEL();

	$self->{_delete_release_sth} = $self->{_dbh}->prepare_cached($delete_release_query) || croak __PACKAGE__.": $DBI::errstr";

	
	return 1;	
}

42;


__END__

=head1 NAME

DBIx::Sequence - A simple SQL92 ID generator 

=head1 SYNOPSIS

  use DBIx::Sequence;

  my $sequence = new DBIx::Sequence({ dbh => $dbh });
  my $next_id = $sequence->Next('dataset');


=head1 DESCRIPTION

This module is intended to give easier portability to Perl database application by providing
a database independant unique ID generator. This way, an application developer is not
bound to use his database's SEQUENCE or auto_increment thus making his application 
portable on multiple database environnements.

This module implements a simple Spin Locker mechanism and is garanteed to return
a unique value every time it is called, even with concurrent processes. It uses
your database for its state storage with ANSI SQL92 compliant SQL. All SQL queries
inside DBIx::Sequence are pre cached and very efficient especially under mod_perl.

=head1 INSTALLATION

	perl Makefile.PL
	make
	make test
	make install

Note:

If you decide to run extended tests for the module, you will have to provide the
make test with a DSN (connect string) to your database (dbi:Driver:db;host=hostname)
and a valid username/password combination for a privileged user.

DBIx::Sequence uses 2 tables for its operation, namely the dbix_sequence_state and the
dbix_sequence_release tables. Those tables will be created if you run extended tests, if 
not you will need to create them yourself. 

	dbix_sequence_state:
	| dataset  | varchar(50) |      
	| state_id | int(11)     |    

	dbix_sequence_release:
	| dataset     | varchar(50) |     
	| released_id | int(11)     |     

Those table names are overloadable at your convenience, see the OVERLOADING section
for details.

=head1 BASIC USAGE

The basic usage of this module is to generate a unique ID to replace the use of your
database's SEQUENCE of auto_increment field. 

=head2 INIT

First, you need to create the sequence object:

	use DBIx::Sequence;
	my $sequence = new DBIx::Sequence({
						db_user => 'scott',
						db_pw => 'tiger',
						db_dsn => 'dbi:mysql:scottdb',
						allow_id_reuse => 1,
						});

DBIx::Sequence can be used to manage multiple sets of ID's (perhaps you could have one dataset 
per table, or one and only one dataset). This permits you to handle multiple applications with 
the same sequence class.  The dataset is normally simply a token string that represents your ID 
set. If the dataset does not exists, DBIx::Sequence will create automagically for you. No special 
steps are involved in the creation of a dataset.

The arguments contains the database informations, db_user, db_pw and db_dsn and are stored
in a hash reference.

At this point, the object has pre cached all of the SQL that will be used to generate
the spin locker race. It is normally a good idea to have a shared sequence object (especially)
under mod_perl to save the prepare overhead.  The 'allow_id_reuse' argument can be passed to 
the constructor to either allow the use of the Release() or deny it. (True value makes it allowed)

=head2 GETTING THE NEXT ID

To get the next id, you simpy have to use the Next() method of your sequence while specifying the
dataset you are getting the next id for.

	my $next_id = $sequence->Next($dataset);

=head2 RELEASING ID'S.

Generated ID's can be _explicitly_ released in your application.  When an ID is released, 
the sequence will be able to give this id back to you throught the Next() method.

This is how it is done:

	$sequence->Release($dataset, $id);

Note:

You must use release only when you are _CERTAIN_ that your ID is not used anymore and that
you want it to be recycled. The Spin Locking mechanism will also take place on released id's
to ensure that no two processes can get the same ID.

=head2 PERMANENTLY REMOVING A DATASET

To make DBIx::Sequence forget about an existing dataset, you need to use the Delete_Dataset()
method.

	$sequence->Delete_Dataset($dataset);

This will clear all state and existence for this dataset and will also clear it's
released id's. Note that if your application still uses this dataset, it will be
automatically recreated blank.


=head2 BOOTSTRAPPING A DATASET FROM EXISTING DATA

It is possible to sync the state of a DBIx::Sequence dataset by using the Bootstrap()
method. 

	$sequence->Bootstrap('my_dataset','my_bootstrap_table','my_primary_field');

Bootstrap() takes 3 arguments. 

=over 3

=item * The dataset to bootstrap

=item * The table from wich you will bootstrap

=item * The field in the bootstrap table that will be used to bootstrap the dataset.

=back

Bootstrap will then sync up the DBIx::Sequence's state with the maximum id of the 
'my_primary_field' in 'my_bootstrap_table'. The bootstrap field must be a numeric
field as you can suspect. The SQL function MAX() will be called on it during the 
bootstrap process.

Note: The bootstrap method _can_ be used at runtime since it will initiate a race
for updating the value thus following the same algorithm. It is recommended though
that you use Bootstrap() when no other concurrent processes are requesting id's.

=head2 OVERLOADING

It is possible to create an overloaded class of DBIx::Sequence. 
This permits you to create a DBIx::Sequence that has different properties than 
the orignal one. The only thing you really have to overload to modify the behaviour
of DBIx::Sequence are some constants:

=over 3

=item * STATE_TABLE : Defines the table used by DBIx::Sequence to store dataset's states.

=item * RELEASE_TABLE : Defines the table used by DBIx::Sequence to store released id's.

=item * COLUMN_PREFIX : A string to be prepended to every column in the internal SQL statements.

=item * DEFAULT_INIT_VALUE : Value used to initialize a dataset when it is first created.

=item * DEFAULT_ALLOW_ID_REUSE : When set to true, will allow the use of Release() if not specified in the constructor. (allow_id_reuse)

=item * DEBUG_LEVEL : When set to true, will enable debugging to STDERR.

=back

So it is very easy to specify the behaviour of DBIx::Sequence that you wish to use
by creating an overloaded class.

Also, a very important method to overload is the getDbh() method. This is the 
function that returns the database handle to the DBIx::Sequence. Your overloaded
class should redefine the getDbh method.

Overloading getDbh will make your sequence class integrate more cleanly with your application.

i.e.

	package MySequence;

	use DBI;
	use DBIx::Sequence;

	use vars qw(@ISA);
	@ISA = qw(DBIx::Sequence);

	use constant STATE_TABLE => 'my_state_table';
	use constant RELEASE_TABLE => 'my_release_table';
	use constant COLUMN_PREFIX => '';
	use constant DEFAULT_INIT_VALUE => '100';
	use constant DEFAULT_ALLOW_ID_REUSE => 1;
	use constant DEBUG_LEVEL => 0;

	sub getDbh
	{
		my $self = shift;

		return MyApplication::MyDBModule::getDbh();
	}

	1;

Then, your code can use this class for its sequencing. Notice that since we overloaded getDbh(), we don't
need to pass a second parameter to new().


	use MySequence;

	my $sequence = new MySequence();
	my $next_id = $sequence->Next($dataset);



=head1 SPECIAL NOTE ON DATABASE HANDLE OPTIONS

DBIx::Sequence requires that the dbh object you passe to it has the AutoCommit flag
set to 1. The main reason for this is that if AutoCommit is off, DBIx::Sequence will have
to do an implicit commit() call, wich in most cases is a bad idea, especially when the dbh
passed to the sequence object already has transactions prelogged in it. 


=head1 CVS AND BLEEDING VERSIONS

For the latest development information, CVS access and Changelog, please visit:

http://labs.turbulent.ca

If you use this module in a project, please let me know!

Your comments and rants are more than welcomed!

Commercial support for this module is available, please contact me for info!

=head1 TODO

=over 3

=item * Implement multiple locking mechanism (semaphore, spin, db locker)

=item * Implement pluggable locking module support

=back

=head1 AUTHOR

Benoit Beausejour, <bbeausej@pobox.com>

=head1 NOTES

This code was made possible by the help of individuals:

Philippe "Gozer" M. Chiasson <gozer@cpan.org>

Thanks to Uri Guttman for documentation checks ;)

=head1 CONTRIBUTORS

Here are the people who submitted patches and changes to the module, they have 
my thanks for their contributions:

Trevor Shellhorn <trevor.schellhorn-perl@marketingtips.com>

Dan Kubb <dkubb@cpan.org>

=head1 SEE ALSO

perl(1).

=head1 COPYRIGHT

Copyright (c) 2000 Benoit Beausejour <bbeausej@pobox.com>
All rights reserved. This program is free software, you can
redistribute it and/or modify it under the same terms as
Perl itself.

=cut