NAME OOPS - Object Oriented Persistent Store SYNOPSIS use OOPS; transaction(sub { $oops = new OOPS dbi_dsn => $DBI_DSN, username => $username, password => $password, table_prefix => "MY"; $oops->{h} = { a => b}; $oops->{foobar} = $myobject; $oops->{abcd} = getref(%{$oops->{h}}, 'a'); $oops->commit; }); DESCRIPTION The goal of OOPS is to make perl objects transparently persistent. OOPS handles deeply nested and cross-linked objects -- even object hierarchies that are too large to fit in memory and (with a hint) individual hash tables that are too large for memory. Objects will be demand-loaded into memory as they are accessed. All changes to your object hierarchy will be saved with a single commit(). Full transactional consistenty is the only operational mode. Either all of your changes are saved or none of them are. During your program, you will see a consistent view of the data: no other running transactions will change the data you see. If another transaction changes data that you are using then at least one of the transactions must abort. This happens automatically. OOPS maps all perl objects to the same RDBMS schema. No advance schema definition is required on the part of the user of OOPS. The name comes from the realization that perl's data model is much more complicated than I initially understood. Internally, the RDBMS schema uses four tables: a table of objects, a table of attributes (keys and values), a table of large attributes that big the normal column widths, and a table of counters. At this time, OOPS is expecting a web-like workflow: create OOPS instance access some ojbects modify some objects commit exit If you need more than one transaction in a program, create more than one OOPS instance. To make your data persistent, make a reference to your data from the OOPS object. To later retrieve your data, simply access it through the OOPS object. EXAMPLE PROGRAM use OOPS; transaction(sub { my $oops = new OOPS dbi_dsn => 'DBI:mysql:database=MY-DATABASE-NAME;host=localhost', username => 'MY-USERNAME', password => 'MY-PASSWORD', table_prefix => "MY-TABLE-PREFIX"; my $p = $oops->{pages}{"/some/path"}; $p->{next} = $oops->{pages}{"/some/other/path"}; $p->{jpgs} = [ read_file("x.jpg"), read_file("y.jpg") ]; $oops->commit; }); exit; SUPPORTED DATA TYPES Perl HASHes, REFs, SCALARs, and ARRAYs are supported. Currently, HASH keys may not be longer than 255 characters. Class names may not be more than 128 characters long. References to hash keys and array elements are supported. At the current time, large ARRAYs are not efficient. Use HASHes instead if this matters to you. References to array elements and hash values are not efficient. Large HASHes are supported by only loading keys that are accessed. HASHes, array elements, and REFs are implmented with tie(). ARRAYs are not currently tie()d because of bugs in perl. Multiple references to the same scalar are supported. References to array elements and hash values are supported. Persistent data is reference counted and cycles must be manually broken to assure de-allocation. SUPPORTED PLATFORMS The following RDBMSs are supported in the code: PostgreSQL OOPS has been tested with PostgreSQL version 7.4.2 and 7.3.5. 7.4.2 on Linux 2.4.23 performs okay. 7.3.5 on pre-release DragonflyBSD is slow. mysql OOPS has been tested with mysql 4.0.16 and 4.0.18 using InnoDB tables. SQLite OOPS has been tested with DBD::SQLite 0.31. With SQLite, an additional parameter to "OOPS->new()" is recognized: "default_synchronous". Possible values are: FULL Sync() all transactions to disk before returning. NORMAL The default: sync() at critical moments only - protects against program failure, but not all power or OS failures. OFF Don't sync() at all and go really fast. SQLite is not 8-bit clean with respect to "\0". OOPS uses a DBD::SQLite feature to translate binary nulls. A side-effect is that backslash will be doubled "\" -> "\\". OOPS does not use the "counters" table with SQLite. Perl versions 5.8.2 through 5.8.4 are supported. Prior to 5.8.2, it wasn't possible to untie scalars from within the a tied scalar access method. An ugly workaround is possible if there is enough interest. OOPS has been tested on Linux 2.4.23 (Debian unstable as of April '04); on FreeBSD 4.9; and on DragonflyBSD prerelease. As far as performance goes, mysql and SQLite are both about twice as fast as PostgreSQL for applications that only have one transaction at at time. SQLite is particularly slow when there are multiple transactions as its lock grangularity is the entire database. Mysql also slows down when there are multiple simultaneous transactions. I don't know yet which backend is fastest for applications with many simultanous transactions. The RDBMS schema for SQLite is different than for mysql or PostgreSQL (which are the same as each other). FUNCTIONS "transaction($funcref, @args)" "transaction()" is a wrapper for a complete transaction. Transactions that fail due to deadlock with other processes will be re-run automatically. The first parameter is a reference to a function. Any additional parameters will be passed as parameters to that function. The return value of "transaction()" is the return value of "&$funcref()". It is not necessary to use the "transaction()" method. Beware that nearly any operation on persistent date (even read operations) can cause deadlock. Any use of persistent data can trigger a deadlock. The "transaction()" function catches this and retries automatically upto $OOPS::transaction_maxtries times (15 times unless you change it). If you don't use "transaction()" you might want to catch the exceptions that "transaction()" catches. To do this, you can regex match $@ against $OOPS::transfailrx. "getref(%hash, $key)" References to tied hash keys are buggy in all perls through 5.8.3. Use getref(%hash, $key) to create your reference to a tied hash key. See: and . $ref = getref(%hash, $key); Alternatively, use "$oops->workaround27555($ref)". Getref() and workaround27555() work around all the perl bugs with tied hash key references. Failure to use them may result in unexpected and inconsistent results. PUBLIC CLASS METHODS "OOPS->new(dbi_dsn => $dbi_dsn, user => $user, password => $password, table_prefix => $prefix)" Creates a OOPS object instance. In addition to the normal DBI default environment variables of $DBI_DSN, $DBI_DRIVER, $DBI_USER, $DBI_PASS, OOPS has its own set that overrides the DBI_ set: $OOPS_DSN, $OOPS_DRIVER, $OOPS_USER, $OOPS_PASS. OOPS allows a prefix to be supplied for it's internal table names. If you set a prefix of "FOO_" then it will use a "FOO_object" table instead of an "object" table. This can be set as an argument to "new()" or it can be set with the environment variable $OOPS_PREFIX. This allows multiple separate object spaces to exist within the same SQL database. It's intended use is to support testing. "OOPS->inital_setup(dbi_dsn => $dbi_dsn, user => $user, password => $password, table_prefix => $prefix)" Drops and recreates the database tables. Don't use it too often :-) The regression suite drops and re-creates the tables many times. Be careful. PUBLIC OBJECT METHODS "->commit()" Writes any changed objects back to the database and commits the transaction. Currently only one commit() call is allowed. Do not access your persistent data after commit() -- it may work but this is not covered well in the regresssion suite. "->virtual_object(\%hash [,$new_value])" Queries [and sets] the load-virtual flag on a persistent hash. Hashes that load virtual will do separate queries for each key rather than load the entire hash. This is a good thing if your has has many keys. This flag takes effect the next time the hash is loaded. The value is a perl boolean. This may be handled automatically in the future. "->workaround27555($reference)" References to tied hash keys are buggy in all perls through 5.8.3. Use workaround27555($reference) to register your new tied hash key references so that they can be transformed into references that actually work correctly. $ref = \%hash{$key}; $oops->workaround27555($ref); "workaround27555()" is harmless if called on other sorts of references so it is safe to use indescriminately. See . Alternatively, use "getref(%hash, $key)". "->dbh()" This returns the main DBI database handle used by OOPS. This function is provided for those who want to hand-write queries. Please note: no changes are written to the DBMS until commit(). "->load_object($id)" This will load a persistent object by number. It returns the object or undef if the object doesn't exists. This function is provided for those who want to hand-write queries. ERRATA, DEVELOPMENT STATUS, BUGS, AND BUG BOUNTY OOPS has been thoroughly tested. As this is written, there are no known users of OOPS. Don't let this stop you from using it! Someone has to go first. The regression suite is very well developed: there is twice as much code in the test suite as there is in the module itself. The suite does over 1.5million tests. On my fastest computer it takes over six hours to run. I have so much confidence in my test suite, I'm offering a bounty on bugs! Bug Bounty I'll pay (via paypal) US$5.00 to anyone who submits a new bug (with regression test) that is caused by a problem in my OOPS module. To qualify, bugs must be serious: they must cause data corruption, false results, or program failure. Bugs must not be listed here. I'll report the number of bounties paid in the CHANGELOG. Known fixable bugs in OOPS memory leaks OOPS currently has memory leaks. This may or may not matter to your application. The rate of leakage varies depending on which RDBMS is used. SQLite seems to have the most significant problems. Schema error with mysql There is an error in the schema that OOPS uses with mysql. This error does not cause any problems with the versions of mysql that OOPS has been tested with (4.0.16 and 4.0.18). This error will be fixed in the next version of OOPS. delayed DESTROY Additional references to the in-memory copies of persistent data are kept by OOPS. These extra references will prevent DESTORY methods from being called as soon as they otherwise would be. other magic Other perl magic attributes are not currently stored persistently. Many probably could be supported, but many could not. For example, taint does not work on tied hashes: . unreferenced blessed scalars When you bless a reference to a scalar value, the blessing is stored with the scalar, not the reference. The blessing remains even if there is no reference to the scalar. The following code prints "true". my $x = 'foobar'; my $y = \$x; bless $y, 'baz'; $y = 7; $y = \$x; my $z = ref($y); print "true\n" if $z eq 'baz'; At the current time, OOPS does not store such blessings. OOPS does remember blessings when there is a reference. re-blessing the OOPS object If you re-bless the OOPS object, your data is likely to become inaccessable. I list this here so nobody claims a bounty for breaking OOPS in this way. DBD::Pg and ASCII NULL DBD::Pg does not easily support ASCII NULL. OOPS has only partial support for ASCII NULL with PostgreSQL: don't have ASCII NULL in your class names. Bugs in perl that effect OOPS references to hash keys Persistent hashes are implemented with tie. There are bugs with perl's implementation of references to tied hash keys. These bugs will be triggered in several situations: creating a reference to tied hash key that doesn't exist yet; deleting a key that has a reference tied to it; assigning through a reference to a key that has multiple references. All of the above can either be avoided or you can workaround them by either calling "workaround27555"($YOUR_REFERENCE) whenver you create a tied hash key reference or by using "getref(%hash, $key)" to create your reference. The perl bugs are documented in: and . "local" and tie "local(%some_tied_hash)" doesn't work right. Thus "local(%some_persistent_hash)" won't work right either: . "scalar(%hash)" "scalar(%hash)" support was added in perl 5.8.3 and does not exist in 5.8.2. tied arrays don't work right There are a couple of bugs with tied arrays that prevent OOPS from using them: and . This isn't a big deal unless you've got big arrays. SQLite and perl's malloc(). If SQLite is used with a perl that has been compiled to use perl's "malloc()", it will report LOTS of "Bad free() ignored (PERL_CORE)" errors. It is not currently known if these errors are harmfull beyond generating lots of output to STDERR. The default perl configuration on FreeBSD uses perl's "malloc()". FUTURE DIRECTIONS OOPS isn't done. There are a bunch of things that I am considering adding to it. If any of these things is important to you, speak up so that I know there is interest... fix the bugs There are bugs listed in the DEVELOPMENT STATUS section that could be fixed. First up is fixing the memory leak. code cleanup and general performance enhancements The initial release of OOPS concentrated on correct behavior and other aspects of the module were somewhat ignored. The code could be cleaned up a bunch. better concurency With the transaction model of SERIALIABLE on mysql, there are serious performance issues surrounding access to the main hash. Change to a different transaction model or DBMS. perl-syntax sql query translator SELECT Employee WHERE $Employee->{salary} > 5000 It's possible to translate perl-syntax queries into real SQL that can be used to query the object store. better grouping Objects are loaded in groups rather than individually. There is much room for improvement in choosing how groups are formed. This is largely undeveloped as yet. support for databases other than mysql PostgreSQL is the next likely supported backend. caching Many possibilities. A cache-invalidation daemon to note when objects have changed. Re-verification of touched data from the database. Ability to call commit() more than once. weak references Support for persistent weak references is possible. external references to objects Currently objects are reference counted internally. You must have a reference to something from an already existing object for it to continue to exist. contracts OOPS has to do a lot of scanning of objects to see if they've changed. Explicit notification of changes would improve performance. OOPS could call functions before saving and after loading to transform objects for a better or cleaner on-disk representation. support for 'base' & accessor methods This isn't something that I care about but maybe someone else does? schema enforcement Allow explicit schemas to be defined. Do not save objects that don't conform. RDBMS -> object mapping Map existing RDBMS schemas into objects. data viewer Viewing large datasets of deep and cross-linked data is difficult. Perhaps a CGI-based or Tk-based data navigator would help. support for tied data structures It is possible to support storing tied data. The tied object is what would need to be persistent. This would only work on some kinds of ties. garbage collect circular references Like perl, OOPS uses reference counting to know when to delete an object. Unlike perl, restarting your program does not clear out the circular references. support for other base types. Right now, just HASH, SCALAR, REF, and ARRAY are supported. Regular expressions, file handles, I don't know it's possible to support code references. WRITING SQL QUERIES BY HAND If you want to query your data, then until a translator is written, your only choice for making queries is to write them by hand. Using your data does not require a query: anything you've got a reference to will be loaded as you access it. Queries are for performing searches that don't have a perl-object index. Each perl HASH, REF/SCALAR, or ARRAY has a row in the "object" table and multiple rows in the "attribute" and "big" tables. Here are the columns you'll care about: object There is one row per perl object. id The object id. class The blessed class name (limited to 255 characters). otype The type of object: 'H' A HASH. 'A' An ARRAY. 'S' A SCALAR or REF. attribute This is a table of key/value pairs. The keys corrospond to perl hash keys and perl array indexes. The values corrospond to perl hash values and array element values. id The object id. pkey The hash key or array index. pval he hash value or array value. Limited to 255 characters. ptype Flags the type of the value. Possible values are: '0' A normal value. Numeric or string. 'B' An big value. "pval" will be a copy of the start of the value for the first N characters. The end of "pval" will be a MD5 checksum of the full big value. 'R' A reference to another object. big This is a table of values that were too large for the normal columns. Even with databases that support wide columns, a separate big table is used so that you don't load large scalars unless you actually need the value. id The object id. pkey The hash key or array index. pval The hash value or array value. Limited to whatever the underlying database will support as it's largest blob. fragno Blob fragment number. This column only exists with SQLite. SQLite has a smallish maximum row size and so big values must be split into multiple rows. REFs are are special. There are several types of REFs: references to scalar values; references to objects; secondary references to scalar values; references to scalar values that are part of another object (references to hash keys and references to array elements). The representation of references is designed so that you don't need to care what sort of REF it is when you're doing a query. The basic REF is a ref to a value inside another object. An example: OBJECT TABLE id class otype 1 OOPS::NamedObj H 383 SCALAR R 384 SCALAR R 385 SCALAR R 386 REF R 400 HASH H 500 ARRAY A ATTRIBUTE TABLE id pkey pval ptype 1 A500 500 R 1 H400 400 R 1 R383 383 R 1 R384 384 R 1 R385 385 R 1 R386 386 R 383 400 'a-key' 0 384 384 'nopkey' 0 384 'nokey' 'a-value' 0 385 384 'nopkey' 0 386 386 'nopkey' 0 386 'nopkey' 500 R 400 'a-key' 'a-value 0 400 'another-key' 'another-value' 0 400 'A500' 500 R 500 0 'a-value' 0 500 1 'another-value' 0 HASH 1 is %$oops. REF 383 is a reference to the key 'a-key' in object #400 (a HASH). REF 384 is a ref to scalar. It uses two rows to make writing queries easier. REF 385 is a duplicate reference to a scalar value. It duplicates REF 384. In behavior, these two REFs should be identical even though they are represented differently in the database. REF 386 is a ref to an object: #500 (an ARRAY). HASH 400 is a normal hash. ARRAY 500 is a normal hash. This example data is what you would end up with after running code like: my $oops = new OOPS dbi_dsn => 'DBI:mysql:database=MY-DATABASE-NAME;host=localhost', username => 'MY-USERNAME', password => 'MY-PASSWORD'; $oops->{A500} = [ 'a-value', 'another-value' ]; $oops->{H400} = { 'a-key' => 'a-value', 'another-key' => 'another-value', 'A500' => $oops->{A500}, }; $oops->{R383} = \$oops->{H400}{'a-key'}; $oops->workaround27555($oops->{R383}); $oops->{R384} = \'a-value'; $oops->{R385} = $oops->{R384}; $oops->{R386} = \$oops->{A500}; $oops->commit; SQL queries require a bunch of joins to link data structures together. Here are some examples. "SELECT Foobar WHERE $Foobar->{xyz} = 'abc'" SELECT object FROM object, attribute WHERE object.class = 'Foobar' AND object.id = attribute.id AND attribute.pkey = 'xyz' AND attribute.pval = 'abc' AND attribute.ptype = '0' "SELECT Foobar WHERE ${$Foobar->{xyz}} = 'abc'" This example should show why an automatic translator would be a good idea... SELECT ohash.object FROM object AS ohash, attribute AS ahash, object AS oref, attribute AS aref, attribute AS target WHERE ohash.class = 'Foobar' AND ohash.otype = 'H' AND ahash.id = ohash.id AND ahash.pkey = 'xyz' AND ahash.ptype = 'R' AND oref.id = ahash.pval AND oref.otype = 'S' # this is the outer ref AND oref.id = aref.id AND aref.pval = target.pkey # here's the reference indirection AND target.pval = 'abc' AND target.ptype = '0' If you construct a query like these examples that return object id's, then use "$object = $oops->load_object($id)" to load them into memory. I reccomend that hand-written queries be read-only as there are additional columns that must be kept consistent. For example, the object table includes a reference count column to handle garbage collection of the persistent data. RUNNING THE REGRESSION TEST SUITE The regression test suite empties and re-creates the persistent store over and over again. To prevent the accidental erasure of production data, all of the tests require a special environment variable to be set $OOPSTEST_DSN. This variable replaces the normal $DBI_DSN or $OOPS_DSN. Correspondingly there is a $OOPSTEST_USER, $OOPSTEST_PASS, and $OOPSTEST_PREFIX. Set these variables to something different than what you use for your production data! Most of the tests take a long time to run and are disabled by default. If you can run the full suite in less than six hours please tell me about your configuration. Beware mysql logging. On Debian unstable, the default configuration for mysql logs every SQL statement. Running the test suite to completion will generate several gigabytes of log file. Running out of disk space will cause the tests to fail. On DragonflyBSD (FreeBSD 4.9) the default mysql configuration includes making replication master logs. THE COMPETITION There are a number of other modules that make perl objects persistent. For each module, I'll provde a sentence or two that explains why I wrote OOPS rather than using that one. OOPS does not require any schema defintion. Most other persistence package requires schema definition and allows the nature of the underlying RDBMS to show in both the kinds of objects you can use and how you use them. OOPS tries to let you work with perl objects without concerning yourself about how they'll be stored persistently. Persistence::Object::Postgres Uses Data::Dumper to serialize objects into BLOBs. Cannot handle cross-linking. No query support. pop Only supports Sybase. Uses a fixed schema defined in XML. SPOPS Objects are HASHes, values are scalars only. No nesting. DBIx::RecordSet Thin layer on top of DBI. Removes much pain from using DBI. Does not change basic relationship to data -- still relational. Object::Transaction Uses Storable to preserve objects in files. No query support. No cross-linking. Lightweight. Transactions. Class::DBI Must define schema and relationships to RDBMS. Accessor methods. Doesn't support many2many. Close tie to underlying RDBMS. Alzabo Must define schema and relationships as relates to RDBMS. Close tie to underlying RDBMS. Can help create RDBMS schema. Complex. Can cache database rows. Tangram Must define schmea. RDBMS schema is created from perl schema. Pixie Less trasnparent. Requires user to remember a cookie to retreive objects. Cannot handle cross-linking of objects? No query support? MLDBM / Tie::MLDBM / MLDBM::Easy / MLDBM::Sync Uses Data::Dumper or Storable or such to serialize objects and store them as strings. Lock granularity is the whole file. No query support. Only one (or two with MLDBM::Easy) level of access is automated. ObjStore Proprietary. Not as transparent as OOPS. see also has an overview of options. LICENSE Copyright(C) 2004 David Muir Sharnoff All rights reserved except as granted by a license or contract. This module is licensed both commercially and as free software. The free software license follows. The Play-Fair-To-Play Software License Preamble. I wish to let people use this software for free as long as they don't abuse the free software community or Internet in particular ways. I want all derrivitives of this software to be free. I want to be able to license this software commercially. This license is not compatabile with the GPL. It was not written by a lawyer and the exact wording will probably change when a legal expert examines it. This license applies only to this version of this program: other versions may exist with other licenses. I. This software may be used, copied, modified, and redistributed with no fee paid to the copyright holder (original licensor) so long as all the parts of this license are honored. Should any part be deemed invalid or otherwise contradicted, the entire license is invalidated and no right to use, copy, modify or redistribute exists. II. Any redistribution of this software must retain this license. Any redistribution of this software must include it's original (source code) form. When redistributing this software, you may not add additional licensing terms that would prevent the new recipient of the software from using, copying, modifying, or redistributing this software themselves under the terms of this license. III. This software is licensed free of charge. To the extent permitted by applicable law, there is no warranty for the software. The software "as is" without warranty of any kind, either expressed or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. The entire risk as to the quality and performance of the software is with you. should the software prove defective, you assume the cost of all necessary servicing, repair or correction. IV. All useful modifications to this software must be licensed back to the original licensor such that the original licensor can relicense the modifications to third parties under terms of their own choosing with no restrictions whatsoever and no need to even acknowledge the origin of such modifications. There are two easy ways to accomplish this: put all such modifications into the public domain or assign the copyright to the original licensor. V. Entities that have willfully violated an open source license are excluded from this license. For example, SCO for violating the GPL. VI. Entities that have attempted to enforce a software patent (except as a defensive move like IBM's counter to SCO) are excluded from this license. For example, Unisys for enforcing a patent on GIF image creation. VII. Entities that engage in or encourage sending SPAM emails are excluded from this license. VIII. Entities that are or have challenged this license in court are excluded from this license. IX. The following entities are excluded from this license: Network Solutions; Verisign; ICANN; and Internet-Journals. X. Entities that share a 5% or more common ownership interest with an excluded entity are also excluded from this license. XI. Entities that are excluded from this license are forbidden to use, copy, modify, or redistribute the covered software. This is version 1.1 of the Play-Fair-To-Play Sofware License.