Build.PL100644000000000000 45514777361274 15353 0ustar00rootroot000000000000Data-ObjectDriver-0.25# ========================================================================= # THIS FILE IS AUTOMATICALLY GENERATED BY MINILLA. # DO NOT EDIT DIRECTLY. # ========================================================================= use 5.008_001; use strict; use Module::Build::Tiny 0.035; Build_PL(); Changes100644000000000000 3040614777361274 15411 0ustar00rootroot000000000000Data-ObjectDriver-0.250.25 2025-04-15T04:37:59Z - Do not add empty where expressions #47 - Remove __is_stored flag if the object is removed from the database #43 - Correct the pod for object_is_stored, which was called is_ephemeral and had the opposite meaning #44 - Do not skip cache repair based solely on the number of cache keys (Thanks to sewi-cpan) #29 0.23 2024-10-23T04:39:20Z - Introduce as_escape method to D::OD::SQL to allow specifying escape clause #45 0.22 2023-02-01T05:14:13Z - Clear %Handle cache if dbh is set to undef #42 - Skip fork test on win32 #41 0.21 2020-10-29T05:54:05Z - Add GitHub Actions #38 - Support DBD::MariaDB #39 - Fork safety #40 0.20 2020-09-14T12:15:54Z - Fix tests (Thanks to eseyman) #36 - Change license (Thanks to kentfredric) #34 0.19 2020-03-03T15:13:37Z - Parallel testing #33 0.18 2019-07-09T08:10:31Z - add_complex_where can not handle some case of nested group (usualoma) #30 0.17 2019-03-12T04:24:20Z - Bump version 0.16 2019-03-07T07:07:14Z - Avoid SQL syntax error: column IN () (charsbar) #26 - Add disconnect method explicitly to pass tests for windows (twata1) #25 0.15 2017-04-19T01:41:02Z - Fix failed tests with Perl 5.25.11 (miniuchi) #24 0.14 2016-01-14T03:03:02Z - Fix test for SQLite 3.10 compatibility (ziguzagu) #21 0.13 2015-05-20T02:26:46Z - Fix 0.12 changes with braking inserting new records (masiuchi) #20 0.12 2015-05-18T05:33:33Z - Fix broken $sth at inserting new records (masiuchi) #19 0.11 2015-03-28T10:04:24Z - Fix test (ambs) #16 - Migrate to Minilla (ziguzagu) #17 - Pass the whole args to fallback driver. (Akira Sawada) #18 - Remove a joined table from the "FROM" phrase for fix error on MySQL. (usualoma) #7 0.10 2015.01.14 - Document spelling corrections (dsteinbrunner) #8 #9 #10 #11 #12 #13 #14 0.10_1 2015.01.14 - Fix test (ambs) #16 0.09 2011.03.17 - Fix reuse_dbh behaviour when ping fails on driver->dbh (RT 65448) 0.08 2010.12.06 - Fixed issue where a profiler cannot be returned if $@ is set https://github.com/sixapart/data-objectdriver/pull/1 (Akira Sawada) - Adds a new restricted IO operation mode controlled by the $Data::ObjectDriver::RESTRICT_IO flag (Brad Whitaker) 0.07 2010.03.22 - When an object is changed, delete the cache instead of updating it. It is a bit more expensive, but should safer. - Fixed a uuv warning thanks to Kazuhiro Shibuya - Fixed a issue in the test suite for newer version of SQLite reported by Emmanuel Seyman http://rt.cpan.org/Ticket/Display.html?id=45186 - Fixed boggus bulk_insert() in the MySQL driver - Fixed a perl5.11 warning - In case you haven't noticed DOD is now on github, bye svn. 0.06 2009.01.28 - Added peek_next() method to ResultSet, q.v. - Localized creation of D::OD::Iterator object. Thanks to Hirotaka Ogawa for the patch. - Fixed compilation error with Perl 5.10. Thanks to smpeters for the patch. - Added a new $object->uncache_object as a mirror of cache_object(), which purge one object from the cache layer, for the cases where you want a manual control over it. - Added a "distinct" method to D::OD::SQL that forces the DISTINCT keyword in the generated SQL statement. Thanks to John Berthels for the patch. - Added a "window_size" argument for the search() method of the caching layer to constrain the number of objects loaded from the database for large or unbounded searches. - Added a "comment" argument to search parameter allowing the SQL queries to be watermarked with SQL comments. - Added a "object_is_stored" method on DOD objects, which returns true until the object has been saved in the persistent store. - Added a "pk_str" method on base objects has a nice shortcut for printing the primary key of an object. - Added a "reuse_dbh" option to D::OD::D::DBI, if enabled it caches and reuses $dbh using the dsn as the key. - Exposed the transaction mechanism built in the drivers at the object levels: D::OD::BO->begin_work now starts a global transaction across all drivers ending with a rollback or a commit on the same class. - Fix problem with prepare_cache in DBD::SQLite - Fix PerlCritic tests http://rt.cpan.org/Ticket/Display.html?id=37197 - Fix problems under 5.10 http://rt.cpan.org/Ticket/Display.html?id=30941 - Fix test failures under Win32 http://rt.cpan.org/Ticket/Display.html?id=24480 - Pg *can't* handle REPLACE https://rt.cpan.org/Ticket/Display.html?id=38840 - Fixed an issue where Pg would look into the wrong sequence if DBI has a 'prefix' configured (used in a undocumented TheSchwartz feature) https://rt.cpan.org/Ticket/Display.html?id=41880 - Added Oracle support, courtesy of Xiaoou Wu (Oracle) https://rt.cpan.org/Ticket/Display.html?id=41929 - Added an "add_index_hint" method to D::OD::SQL to allow specifying a "USE INDEX" hint. - Added an experimental GearmanDBI driver that provides query coalescing using Gearman workers (to sit in front of a direct DBI driver). 0.05 2008.02.24 - Added a new Data::ObjectDriver::ResultSet abstraction for building result sets with lazy-loading of the actual results. This allows for passing around a representation of a full result set (no limit, no offset, etc), and allowing callers to modify the set as needed. - search() now returns a subref blessed into the new D::OD::Iterator class. It's backwards-compatible (you can still call $iter->()), but it now supports $iter->next() as well. - Added a D::OD::SQL::add_complex_where method, for creating more complex WHERE clauses with boolean operations. - Added instrumentation/profiling for the memcached, Apache, and RAM caching drivers. - Improved "remove" support in the experimental Multiplexer driver. - Fixed an ordering bug with BaseCache->update: the cache is now updated after the fallback (a persistent store, usually) is updated, to prevent the cache being updated but the backend erroring out. - Let DSNs start with "DBI:" instead of only "dbi:" - Fix a bug where the iterator version of search() (search() called in scalar context) wasn't calling finish() on $sth. It was generating warnings on certain circumstances. - Fixed a circular reference when using has_a. 0.04 2007.05.02 - Fixed a bug where single-PK classes were not returning the objects correctly sorted during a lookup_multi. - Added support for MySQL 'REPLACE INTO' syntax with a new $obj->replace() method. - Added a new trigger 'post_inflate'. - Fixed a minor issue (warning) with no_changed_flags in column_func() - Added has_a() construct to build linking methods between classes. - remove() returns number of affected rows, with DBI semantics. - Bulk inserting of data can now be done with the new bulk_insert() Class method. It uses Postgres' COPY command or MySQL's multi-value inserts to load data quickly. - The new() constructor for objects now accepts column name/value pairs which are passed to a new init() method. - The new init() method can be called on any object to set many parameters in one call. - This init() method can also be overridden, allowing for custom initialization code. - Added parens around terms within complex SQL conditionals, to allow even more complex conditions to be created. - Made the second argument to D::OD::SQL::add_select optional. It defaults to the value of the first argument (the column name). - Pass along $orig_obj (original object) when calling post_save/post_update triggers, even when the object hasn't changed. - A non-numeric value in a LIMIT now causes an exception. - Fixed a bug where calling SQL->add_join twice would create an invalid SQL statement. - More documentation! - Added more DOD::DBD::* options: sql_for_unixtime, can_delete_with_limit, is_case_insensitive, can_replace, sql_class. - Added an experimental Multiplexer class to direct writes to multiple backend drivers. - Added a generic end_query method, analogous to start_query, which is called after each query. Useful for profiling etc. - Text::SimpleTable is now loaded dynamically so that it's no longer a requirement for D::OD. 0.03 2006.08.05 - Added an inflate and deflate mechanism to memcached caching. When objects are stored in memcached, they are now deflated to a hash reference containing only the column values; retrieving the object from memcached automatically inflates the object to the full representation. Classes can override inflate and deflate to store additional information in the memcached representation that's kept automatically up-to-date. - Added a SimplePartition driver, which helps to make partitioning, well, simpler. Still to come: documentation and a tutorial on partitioning. - Many, many bug fixes and improvements to the caching drivers. - Added detection of changed columns, such that only columns that have been changed are updated in an UPDATE statement. - Added a clear_cache method to the D::O::D::Cache::RAM class. - Added cross-DBD error handling, which maps local error codes to error constants in Data::ObjectDriver::Errors. The list of supported errors is pretty miniscule thus far (just one), but will be expanded as needed. - Added support for query profiling (Data::ObjectDriver::Profiler), which counts queries, calculates frequent queries, and can produce reports. - Added support for optional table prefixes, which simplifies setting up identical schemas in the same database. - Added an optional $terms argument to D::O::D::DBI->update, which can add additional terms to the UPDATE statement besides just the PK. - Added a D::O::D::DBI->begin_work method, and improved the commit and rollback methods. - Added a D::O::D::DBI->last_error method. - Added support for multiple JOIN clauses with a new D::O::D::SQL->add_join method. - Multiple OR values are now contained in an IN (...) clause rather than many joined OR clauses. - Added a for_update option to search, which allows constructing a SELECT ... FOR UPDATE query. - D::O::D::BaseObject->column is now removed and replaced with a column_func method, which returns a subroutine reference used to initialize the dynamically-created methods for each column. This allows some optimizations. 0.02 2006.02.21 - Added Data::ObjectDriver::BaseView, a base class for creating "views" across multiple tables, or involving more complex aggregate queries. - Added trigger/callback support for common operations. See the Data::ObjectDriver documentation for more details. - Added GROUP BY support in Data::ObjectDriver::SQL. - Data::ObjectDriver::BaseCache->search now uses lookup_multi to do a very fast PK lookup, which will hit the cache first before the backend DB. - Fixed bugs with BLOB columns in SQLite driver. - Added connect_options option to Data::ObjectDriver::Driver::DBI, for passing in custom options for a DBI->connect call. - Data::ObjectDriver::BaseObject->remove now works as a class method. - Added Data::ObjectDriver::BaseObject->primary_key_tuple for retrieving the primary key value(s) for an object. - Added Data::ObjectDriver::BaseObject->refresh to reload an object from the database. - Added support for HAVING clauses in Data::ObjectDriver::SQL. For views that are not attached to a particular datasource, any terms passed in to the query will automatically be turned into HAVING clauses. - Improved the lookup_multi method for all BaseCache subclasses: we now allow the subclass to look up multiple values in the cache and return any already-cached items, then make a list of the remaining IDs and send them to fallback->lookup_multi. - Driver::DBI->lookup_multi will now use an OR clause to look up multiple values in one query. - Added lots of test cases. - Pod fix (Thanks to Koichi Taniguchi) 0.01 2005.09.23 - Initial distribution. LICENSE100644000000000000 4336114777361274 15127 0ustar00rootroot000000000000Data-ObjectDriver-0.25Terms of the Perl programming language system itself a) the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version, or b) the "Artistic License" --- The GNU General Public License, Version 1, February 1989 --- This software is Copyright (c) 2005 by Six Apart Ltd. This is free software, licensed under: The GNU General Public License, Version 1, February 1989 GNU GENERAL PUBLIC LICENSE Version 1, February 1989 Copyright (C) 1989 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The license agreements of most software companies try to keep users at the mercy of those companies. By contrast, our General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. The General Public License applies to the Free Software Foundation's software and to any other program whose authors commit to using it. You can use it for your programs, too. When we speak of free software, we are referring to freedom, not price. Specifically, the General Public License is designed to make sure that you have the freedom to give away or sell copies of free software, that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of a such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must tell them their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any work containing the Program or a portion of it, either verbatim or with modifications. Each licensee is addressed as "you". 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this General Public License and to the absence of any warranty; and give any other recipients of the Program a copy of this General Public License along with the Program. You may charge a fee for the physical act of transferring a copy. 2. You may modify your copy or copies of the Program or any portion of it, and copy and distribute such modifications under the terms of Paragraph 1 above, provided that you also do the following: a) cause the modified files to carry prominent notices stating that you changed the files and the date of any change; and b) cause the whole of any work that you distribute or publish, that in whole or in part contains the Program or any part thereof, either with or without modifications, to be licensed at no charge to all third parties under the terms of this General Public License (except that you may choose to grant warranty protection to some or all third parties, at your option). c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the simplest and most usual way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this General Public License. d) You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. Mere aggregation of another independent work with the Program (or its derivative) on a volume of a storage or distribution medium does not bring the other work under the scope of these terms. 3. You may copy and distribute the Program (or a portion or derivative of it, under Paragraph 2) in object code or executable form under the terms of Paragraphs 1 and 2 above provided that you also do one of the following: a) accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Paragraphs 1 and 2 above; or, b) accompany it with a written offer, valid for at least three years, to give any third party free (except for a nominal charge for the cost of distribution) a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Paragraphs 1 and 2 above; or, c) accompany it with the information you received as to where the corresponding source code may be obtained. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form alone.) Source code for a work means the preferred form of the work for making modifications to it. For an executable file, complete source code means all the source code for all modules it contains; but, as a special exception, it need not include source code for modules which are standard libraries that accompany the operating system on which the executable file runs, or for standard header files or definitions files that accompany that operating system. 4. You may not copy, modify, sublicense, distribute or transfer the Program except as expressly provided under this General Public License. Any attempt otherwise to copy, modify, sublicense, distribute or transfer the Program is void, and will automatically terminate your rights to use the Program under this License. However, parties who have received copies, or rights to use copies, from you under this General Public License will not have their licenses terminated so long as such parties remain in full compliance. 5. By copying, distributing or modifying the Program (or any work based on the Program) you indicate your acceptance of this license to do so, and all its terms and conditions. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. 7. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of the license which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the license, you may choose any version ever published by the Free Software Foundation. 8. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS Appendix: How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to humanity, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19xx name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (a program to direct compilers to make passes at assemblers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice That's all there is to it! --- The Artistic License 1.0 --- This software is Copyright (c) 2005 by Six Apart Ltd. This is free software, licensed under: The Artistic License 1.0 The Artistic License Preamble The intent of this document is to state the conditions under which a Package may be copied, such that the Copyright Holder maintains some semblance of artistic control over the development of the package, while giving the users of the package the right to use and distribute the Package in a more-or-less customary fashion, plus the right to make reasonable modifications. Definitions: - "Package" refers to the collection of files distributed by the Copyright Holder, and derivatives of that collection of files created through textual modification. - "Standard Version" refers to such a Package if it has not been modified, or has been modified in accordance with the wishes of the Copyright Holder. - "Copyright Holder" is whoever is named in the copyright or copyrights for the package. - "You" is you, if you're thinking about copying or distributing this Package. - "Reasonable copying fee" is whatever you can justify on the basis of media cost, duplication charges, time of people involved, and so on. (You will not be required to justify it to the Copyright Holder, but only to the computing community at large as a market that must bear the fee.) - "Freely Available" means that no fee is charged for the item itself, though there may be fees involved in handling the item. It also means that recipients of the item may redistribute it under the same conditions they received it. 1. You may make and give away verbatim copies of the source form of the Standard Version of this Package without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may apply bug fixes, portability fixes and other modifications derived from the Public Domain or from the Copyright Holder. A Package modified in such a way shall still be considered the Standard Version. 3. You may otherwise modify your copy of this Package in any way, provided that you insert a prominent notice in each changed file stating how and when you changed that file, and provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or placing the modifications on a major archive site such as ftp.uu.net, or by allowing the Copyright Holder to include your modifications in the Standard Version of the Package. b) use the modified Package only within your corporation or organization. c) rename any non-standard executables so the names do not conflict with standard executables, which must also be provided, and provide a separate manual page for each non-standard executable that clearly documents how it differs from the Standard Version. d) make other distribution arrangements with the Copyright Holder. 4. You may distribute the programs of this Package in object code or executable form, provided that you do at least ONE of the following: a) distribute a Standard Version of the executables and library files, together with instructions (in the manual page or equivalent) on where to get the Standard Version. b) accompany the distribution with the machine-readable source of the Package with your modifications. c) accompany any non-standard executables with their corresponding Standard Version executables, giving the non-standard executables non-standard names, and clearly documenting the differences in manual pages (or equivalent), together with instructions on where to get the Standard Version. d) make other distribution arrangements with the Copyright Holder. 5. You may charge a reasonable copying fee for any distribution of this Package. You may charge any fee you choose for support of this Package. You may not charge a fee for this Package itself. However, you may distribute this Package in aggregate with other (possibly commercial) programs as part of a larger (possibly commercial) software distribution provided that you do not advertise this Package as a product of your own. 6. The scripts and library files supplied as input to or produced as output from the programs of this Package do not automatically fall under the copyright of this Package, but belong to whomever generated them, and may be sold commercially, and may be aggregated with this Package. 7. C or perl subroutines supplied by you and linked into this Package shall not be considered part of this Package. 8. The name of the Copyright Holder may not be used to endorse or promote products derived from this software without specific prior written permission. 9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. The End META.json100644000000000000 2030714777361274 15536 0ustar00rootroot000000000000Data-ObjectDriver-0.25{ "abstract" : "Simple, transparent data interface, with caching", "author" : [ "& COPYRIGHT" ], "dynamic_config" : 0, "generated_by" : "Minilla/v3.1.25", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : "2" }, "name" : "Data-ObjectDriver", "no_index" : { "directory" : [ "t", "xt", "inc", "share", "eg", "examples", "author", "builder" ] }, "optional_features" : { "test_fork" : { "description" : "Test Fork", "prereqs" : { "runtime" : { "requires" : { "DBI" : "1.614", "POSIX::AtFork" : "0", "Parallel::ForkManager" : "0", "Scalar::Util" : "0", "Test::SharedFork" : "0" } } } }, "test_mariadb" : { "description" : "Test MariaDB", "prereqs" : { "runtime" : { "requires" : { "DBD::MariaDB" : "0", "SQL::Translator" : "0", "Test::mysqld" : "0" } } } }, "test_mysql" : { "description" : "Test MySQL", "prereqs" : { "runtime" : { "requires" : { "DBD::mysql" : "0", "SQL::Translator" : "0", "Test::mysqld" : "0" } } } }, "test_postgresql" : { "description" : "Test PostgreSQL", "prereqs" : { "runtime" : { "requires" : { "DBD::Pg" : "0", "SQL::Translator" : "0", "Test::PostgreSQL" : "0" } } } }, "test_sqlite" : { "description" : "Test SQLite", "prereqs" : { "runtime" : { "requires" : { "DBD::SQLite" : "0" } } } } }, "prereqs" : { "build" : { "requires" : { "ExtUtils::MakeMaker" : "6.59", "Test::Exception" : "0" } }, "configure" : { "requires" : { "Module::Build::Tiny" : "0.035" } }, "develop" : { "requires" : { "DBD::SQLite" : "0", "Test::CPAN::Meta" : "0", "Test::MinimumVersion::Fast" : "0.04", "Test::PAUSE::Permissions" : "0.07", "Test::Pod" : "1.41", "Test::Spellunker" : "v0.2.7", "Text::SimpleTable" : "0" } }, "runtime" : { "recommends" : { "Text::SimpleTable" : "0" }, "requires" : { "Class::Accessor::Fast" : "0", "Class::Data::Inheritable" : "0", "Class::Trigger" : "0", "DBI" : "0", "List::Util" : "0", "perl" : "5.006001" } }, "test" : { "requires" : { "version" : "0" } } }, "provides" : { "Data::ObjectDriver" : { "file" : "lib/Data/ObjectDriver.pm", "version" : "0.25" }, "Data::ObjectDriver::BaseObject" : { "file" : "lib/Data/ObjectDriver/BaseObject.pm" }, "Data::ObjectDriver::BaseView" : { "file" : "lib/Data/ObjectDriver/BaseView.pm" }, "Data::ObjectDriver::Driver::BaseCache" : { "file" : "lib/Data/ObjectDriver/Driver/BaseCache.pm" }, "Data::ObjectDriver::Driver::Cache::Apache" : { "file" : "lib/Data/ObjectDriver/Driver/Cache/Apache.pm" }, "Data::ObjectDriver::Driver::Cache::Cache" : { "file" : "lib/Data/ObjectDriver/Driver/Cache/Cache.pm" }, "Data::ObjectDriver::Driver::Cache::Memcached" : { "file" : "lib/Data/ObjectDriver/Driver/Cache/Memcached.pm" }, "Data::ObjectDriver::Driver::Cache::RAM" : { "file" : "lib/Data/ObjectDriver/Driver/Cache/RAM.pm" }, "Data::ObjectDriver::Driver::DBD" : { "file" : "lib/Data/ObjectDriver/Driver/DBD.pm" }, "Data::ObjectDriver::Driver::DBD::MariaDB" : { "file" : "lib/Data/ObjectDriver/Driver/DBD/MariaDB.pm" }, "Data::ObjectDriver::Driver::DBD::Oracle" : { "file" : "lib/Data/ObjectDriver/Driver/DBD/Oracle.pm" }, "Data::ObjectDriver::Driver::DBD::Oracle::db" : { "file" : "lib/Data/ObjectDriver/Driver/DBD/Oracle.pm" }, "Data::ObjectDriver::Driver::DBD::Pg" : { "file" : "lib/Data/ObjectDriver/Driver/DBD/Pg.pm" }, "Data::ObjectDriver::Driver::DBD::SQLite" : { "file" : "lib/Data/ObjectDriver/Driver/DBD/SQLite.pm" }, "Data::ObjectDriver::Driver::DBD::mysql" : { "file" : "lib/Data/ObjectDriver/Driver/DBD/mysql.pm" }, "Data::ObjectDriver::Driver::DBI" : { "file" : "lib/Data/ObjectDriver/Driver/DBI.pm" }, "Data::ObjectDriver::Driver::GearmanDBI" : { "file" : "lib/Data/ObjectDriver/Driver/GearmanDBI.pm" }, "Data::ObjectDriver::Driver::MultiPartition" : { "file" : "lib/Data/ObjectDriver/Driver/MultiPartition.pm" }, "Data::ObjectDriver::Driver::Multiplexer" : { "file" : "lib/Data/ObjectDriver/Driver/Multiplexer.pm" }, "Data::ObjectDriver::Driver::Partition" : { "file" : "lib/Data/ObjectDriver/Driver/Partition.pm" }, "Data::ObjectDriver::Driver::SimplePartition" : { "file" : "lib/Data/ObjectDriver/Driver/SimplePartition.pm" }, "Data::ObjectDriver::Errors" : { "file" : "lib/Data/ObjectDriver/Errors.pm" }, "Data::ObjectDriver::Iterator" : { "file" : "lib/Data/ObjectDriver/Iterator.pm" }, "Data::ObjectDriver::Profiler" : { "file" : "lib/Data/ObjectDriver/Profiler.pm" }, "Data::ObjectDriver::ResultSet" : { "file" : "lib/Data/ObjectDriver/ResultSet.pm" }, "Data::ObjectDriver::SQL" : { "file" : "lib/Data/ObjectDriver/SQL.pm" }, "Data::ObjectDriver::SQL::Oracle" : { "file" : "lib/Data/ObjectDriver/SQL/Oracle.pm" } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "https://github.com/sixapart/data-objectdriver/issues" }, "homepage" : "https://github.com/sixapart/data-objectdriver", "repository" : { "type" : "git", "url" : "https://github.com/sixapart/data-objectdriver.git", "web" : "https://github.com/sixapart/data-objectdriver" } }, "version" : "0.25", "x_authority" : "cpan:SIXAPART", "x_contributors" : [ "Adam Thomason ", "Akira Sawada ", "Ben Trott ", "Brad Choate ", "Brad Fitzpatrick ", "Brad Whitaker ", "David Steinbrunner ", "Garth Webb ", "Graham Blankenbaker ", "Jonathan Steinert ", "Keita Jamadam Sugama ", "Kenichi Ishigaki ", "Kevin Goess ", "Mark Paschal ", "Mart Atkins ", "Masahiro Iuchi ", "Mischa ", "Paul Lindner ", "Sebastian Willing ", "Sekimura ", "Simon Wistow ", "Takatsugu Shigeta ", "Takatsugu Shigeta ", "Taku AMANO ", "Taku AMANO ", "Taku AMANO ", "Tatsuhiko Miyagawa ", "Tsuyoshi Watanabe ", "Yann Kerherve ", "ambs ", "ziguzagu " ], "x_serialization_backend" : "JSON::PP version 4.16", "x_static_install" : 1 } README.md100644000000000000 5204514777361274 15400 0ustar00rootroot000000000000Data-ObjectDriver-0.25[![Build Status](https://travis-ci.org/sixapart/data-objectdriver.svg?branch=master)](https://travis-ci.org/sixapart/data-objectdriver) [![Coverage Status](https://img.shields.io/coveralls/sixapart/data-objectdriver/master.svg?style=flat)](https://coveralls.io/r/sixapart/data-objectdriver?branch=master) # NAME Data::ObjectDriver - Simple, transparent data interface, with caching # SYNOPSIS ## Set up your database driver code. package FoodDriver; sub driver { Data::ObjectDriver::Driver::DBI->new( dsn => 'dbi:mysql:dbname', username => 'username', password => 'password', ) } ## Set up the classes for your recipe and ingredient objects. package Recipe; use base qw( Data::ObjectDriver::BaseObject ); __PACKAGE__->install_properties({ columns => [ 'recipe_id', 'title' ], datasource => 'recipe', primary_key => 'recipe_id', driver => FoodDriver->driver, }); package Ingredient; use base qw( Data::ObjectDriver::BaseObject ); __PACKAGE__->install_properties({ columns => [ 'ingredient_id', 'recipe_id', 'name', 'quantity' ], datasource => 'ingredient', primary_key => [ 'recipe_id', 'ingredient_id' ], driver => FoodDriver->driver, }); ## And now, use them! my $recipe = Recipe->new; $recipe->title('Banana Milkshake'); $recipe->save; my $ingredient = Ingredient->new; $ingredient->recipe_id($recipe->id); $ingredient->name('Bananas'); $ingredient->quantity(5); $ingredient->save; ## Needs more bananas! $ingredient->quantity(10); $ingredient->save; ## Shorthand constructor my $ingredient = Ingredient->new(recipe_id=> $recipe->id, name => 'Milk', quantity => 2); # DESCRIPTION _Data::ObjectDriver_ is an object relational mapper, meaning that it maps object-oriented design concepts onto a relational database. It's inspired by, and descended from, the _MT::ObjectDriver_ classes in Six Apart's Movable Type and TypePad weblogging products. But it adds in caching and partitioning layers, allowing you to spread data across multiple physical databases, without your application code needing to know where the data is stored. # METHODOLOGY _Data::ObjectDriver_ provides you with a framework for building database-backed applications. It provides built-in support for object caching and database partitioning, and uses a layered approach to allow building very sophisticated database interfaces without a lot of code. You can build a driver that uses any number of caching layers, plus a partitioning layer, then a final layer that actually knows how to load data from a backend datastore. For example, the following code: my $driver = Data::ObjectDriver::Driver::Cache::Memcached->new( cache => Cache::Memcached->new( servers => [ '127.0.0.1:11211' ], ), fallback => Data::ObjectDriver::Driver::Partition->new( get_driver => \&get_driver, ), ); creates a new driver that supports both caching (using memcached) and partitioning. It's useful to demonstrate the flow of a sample request through this driver framework. The following code: my $ingredient = Ingredient->lookup([ $recipe->recipe_id, 1 ]); would take the following path through the _Data::ObjectDriver_ framework: 1. The caching layer would look up the object with the given primary key in all of the specified memcached servers. If the object was found in the cache, it would be returned immediately. If the object was not found in the cache, the caching layer would fall back to the driver listed in the _fallback_ setting: the partitioning layer. 2. The partitioning layer does not know how to look up objects by itself--all it knows how to do is to give back a driver that _does_ know how to look up objects in a backend datastore. In our example above, imagine that we're partitioning our ingredient data based on the recipe that the ingredient is found in. For example, all of the ingredients for a "Banana Milkshake" would be found in one partition; all of the ingredients for a "Chocolate Sundae" might be found in another partition. So the partitioning layer needs to tell us which partition to look in to load the ingredients for _$recipe->recipe\_id_. If we store a _partition\_id_ column along with each _$recipe_ object, that information can be loaded very easily, and the partitioning layer will then instantiate a _DBI_ driver that knows how to load an ingredient from that recipe. 3. Using the _DBI_ driver that the partitioning layer created, _Data::ObjectDriver_ can look up the ingredient with the specified primary key. It will return that key back up the chain, giving each layer a chance to do something with it. 4. The caching layer, when it receives the object loaded in Step 3, will store the object in memcached. 5. The object will be passed back to the caller. Subsequent lookups of that same object will come from the cache. # HOW IS IT DIFFERENT? _Data::ObjectDriver_ differs from other similar frameworks (e.g. [Class::DBI](https://metacpan.org/pod/Class%3A%3ADBI)) in a couple of ways: - It has built-in support for caching. - It has built-in support for data partitioning. - Drivers are attached to classes, not to the application as a whole. This is essential for partitioning, because your partition drivers need to know how to load a specific class of data. But it can also be useful for caching, because you may find that it doesn't make sense to cache certain classes of data that change constantly. - The driver class != the base object class. All of the object classes you declare will descend from _Data::ObjectDriver::BaseObject_, and all of the drivers you instantiate or subclass will descend from _Data::ObjectDriver_ itself. This provides a useful distinction between your data/classes, and the drivers that describe how to **act** on that data, meaning that an object based on _Data::ObjectDriver::BaseObject_ is not tied to any particular type of driver. # USAGE ## Class->lookup($id) Looks up/retrieves a single object with the primary key _$id_, and returns the object. _$id_ can be either a scalar or a reference to an array, in the case of a class with a multiple column primary key. ## Class->lookup\_multi(\\@ids) Looks up/retrieves multiple objects with the IDs _\\@ids_, which should be a reference to an array of IDs. As in the case of _lookup_, an ID can be either a scalar or a reference to an array. Returns a reference to an array of objects **in the same order** as the IDs you passed in. Any objects that could not successfully be loaded will be represented in that array as an `undef` element. So, for example, if you wanted to load 2 objects with the primary keys `[ 5, 3 ]` and `[ 4, 2 ]`, you'd call _lookup\_multi_ like this: Class->lookup_multi([ [ 5, 3 ], [ 4, 2 ], ]); And if the first object in that list could not be loaded successfully, you'd get back a reference to an array like this: [ undef, $object ] where _$object_ is an instance of _Class_. ## Class->search(\\%terms \[, \\%options \]) Searches for objects matching the terms _%terms_. In list context, returns an array of matching objects; in scalar context, returns a reference to a subroutine that acts as an iterator object, like so: my $iter = Ingredient->search({ recipe_id => 5 }); while (my $ingredient = $iter->()) { ... } `$iter` is blessed in [Data::ObjectDriver::Iterator](https://metacpan.org/pod/Data%3A%3AObjectDriver%3A%3AIterator) package, so the above could also be written: my $iter = Ingredient->search({ recipe_id => 5 }); while (my $ingredient = $iter->next()) { ... } The keys in _%terms_ should be column names for the database table modeled by _Class_ (and the values should be the desired values for those columns). _%options_ can contain: - sort The name of a column to use to sort the result set. Optional. - direction The direction in which you want to sort the result set. Must be either `ascend` or `descend`. Optional. - limit The value for a _LIMIT_ clause, to limit the size of the result set. Optional. - offset The offset to start at when limiting the result set. Optional. - fetchonly A reference to an array of column names to fetch in the _SELECT_ statement. Optional; the default is to fetch the values of all of the columns. - for\_update If set to a true value, the _SELECT_ statement generated will include a _FOR UPDATE_ clause. - comment A sql comment to watermark the SQL query. - window\_size Used when requesting an iterator for the search method and selecting a large result set or a result set of unknown size. In such a case, no LIMIT clause is assigned, which can load all available objects into memory. Specifying `window_size` will load objects in manageable chunks. This will also cause any caching driver to be bypassed for issuing the search itself. Objects are still placed into the cache upon load. This attribute is ignored when the search method is invoked in an array context, or if a `limit` attribute is also specified that is smaller than the `window_size`. ## Class->search(\\@terms \[, \\%options \]) This is an alternative calling signature for the search method documented above. When providing an array of terms, it allows for constructing complex expressions that mix 'and' and 'or' clauses. For example: my $iter = Ingredient->search([ { recipe_id => 5 }, -or => { calories => { value => 300, op => '<' } } ]); while (my $ingredient = $iter->()) { ... } Supported logic operators are: '-and', '-or', '-and\_not', '-or\_not'. ## Class->add\_trigger($trigger, \\&callback) Adds a trigger to all objects of class _Class_, such that when the event _$trigger_ occurs to any of the objects, subroutine `&callback` is run. Note that triggers will not occur for instances of _subclasses_ of _Class_, only of _Class_ itself. See TRIGGERS for the available triggers. ## Class->call\_trigger($trigger, \[@callback\_params\]) Invokes the triggers watching class _Class_. The parameters to send to the callbacks (in addition to _Class_) are specified in _@callback\_params_. See TRIGGERS for the available triggers. ## $obj->save Saves the object _$obj_ to the database. If the object is not yet in the database, _save_ will automatically generate a primary key and insert the record into the database table. Otherwise, it will update the existing record. If an error occurs, _save_ will _croak_. Internally, _save_ calls _update_ for records that already exist in the database, and _insert_ for those that don't. ## $obj->remove Removes the object _$obj_ from the database. If an error occurs, _remove_ will _croak_. ## Class->remove(\\%terms, \\%args) Removes objects found with the _%terms_. So it's a shortcut of: my @obj = Class->search(\%terms, \%args); for my $obj (@obj) { $obj->remove; } However, when you pass `nofetch` option set to `%args`, it won't create objects with `search`, but issues _DELETE_ SQL directly to the database. ## issues "DELETE FROM tbl WHERE user_id = 2" Class->remove({ user_id => 2 }, { nofetch => 1 }); This might be much faster and useful for tables without Primary Key, but beware that in this case **Triggers won't be fired** because no objects are instantiated. ## Class->bulk\_insert(\[col1, col2\], \[\[d1,d2\], \[d1,d2\]\]); Bulk inserts data into the underlying table. The first argument is an array reference of columns names as specified in install\_properties ## $obj->add\_trigger($trigger, \\&callback) Adds a trigger to the object _$obj_, such that when the event _$trigger_ occurs to the object, subroutine `&callback` is run. See TRIGGERS for the available triggers. Triggers are invoked in the order in which they are added. ## $obj->call\_trigger($trigger, \[@callback\_params\]) Invokes the triggers watching all objects of _$obj_'s class and the object _$obj_ specifically for trigger event _$trigger_. The additional parameters besides _$obj_, if any, are passed as _@callback\_params_. See TRIGGERS for the available triggers. # TRIGGERS _Data::ObjectDriver_ provides a trigger mechanism by which callbacks can be called at certain points in the life cycle of an object. These can be set on a class as a whole or individual objects (see USAGE). Triggers can be added and called for these events: - pre\_save -> ($obj, $orig\_obj) Callbacks on the _pre\_save_ trigger are called when the object is about to be saved to the database. For example, use this callback to translate special code strings into numbers for storage in an integer column in the database. Note that this hook is also called when you `remove` the object. Modifications to _$obj_ will affect the values passed to subsequent triggers and saved in the database, but not the original object on which the _save_ method was invoked. - post\_save -> ($obj, $orig\_obj) Callbaks on the _post\_save_ triggers are called after the object is saved to the database. Use this trigger when your hook needs primary key which is automatically assigned (like auto\_increment and sequence). Note that this hooks is **NOT** called when you remove the object. - pre\_insert/post\_insert/pre\_update/post\_update/pre\_remove/post\_remove -> ($obj, $orig\_obj) Those triggers are fired before and after $obj is created, updated and deleted. - post\_load -> ($obj) Callbacks on the _post\_load_ trigger are called when an object is being created from a database query, such as with the _lookup_ and _search_ class methods. For example, use this callback to translate the numbers your _pre\_save_ callback caused to be saved _back_ into string codes. Modifications to _$obj_ will affect the object passed to subsequent triggers and returned from the loading method. Note _pre\_load_ should only be used as a trigger on a class, as the object to which the load is occurring was not previously available for triggers to be added. - pre\_search -> ($class, $terms, $args) Callbacks on the _pre\_search_ trigger are called when a content addressed query for objects of class _$class_ is performed with the _search_ method. For example, use this callback to translate the entry in _$terms_ for your code string field to its appropriate integer value. Modifications to _$terms_ and _$args_ will affect the parameters to subsequent triggers and what objects are loaded, but not the original hash references used in the _search_ query. Note _pre\_search_ should only be used as a trigger on a class, as _search_ is never invoked on specific objects. > The return values from your callbacks are ignored. > > Note that the invocation of callbacks is the responsibility of the object > driver. If you implement a driver that does not delegate to > _Data::ObjectDriver::Driver::DBI_, it is _your_ responsibility to invoke the > appropriate callbacks with the _call\_trigger_ method. # PROFILING For performance tuning, you can turn on query profiling by setting _$Data::ObjectDriver::PROFILE_ to a true value. Or, alternatively, you can set the _DOD\_PROFILE_ environment variable to a true value before starting your application. To obtain the profile statistics, get the global _Data::ObjectDriver::Profiler_ instance: my $profiler = Data::ObjectDriver->profiler; Then see the documentation for _Data::ObjectDriver::Profiler_ to see the methods on that class. In some applications there are phases of execution in which no I/O operations should occur, but sometimes it's difficult to tell when, where, or if those I/O operations are happening. One approach to surfacing these situations is to set, either globally or locally, the $Data::ObjectDriver::RESTRICT\_IO flag. If set, this will tell Data::ObjectDriver to die with some context rather than executing network calls for data. # TRANSACTIONS Transactions are supported by Data::ObjectDriver's default drivers. So each Driver is capable to deal with transactional state independently. Additionally class know how to turn transactions switch on for all objects. In the case of a global transaction all drivers used during this time are put in a transactional state until the end of the transaction. ## Example ## start a transaction Data::ObjectDriver::BaseObject->begin_work; $recipe = Recipe->new; $recipe->title('lasagnes'); $recipe->save; my $ingredient = Ingredient->new; $ingredient->recipe_id($recipe->recipe_id); $ingredient->name("more layers"); $ingredient->insert; $ingredient->remove; if ($you_are_sure) { Data::ObjectDriver::BaseObject->commit; } else { ## erase all trace of the above Data::ObjectDriver::BaseObject->rollback; } ## Driver implementation Drivers have to implement the following methods: - begin\_work to initialize a transaction - rollback - commit ## Nested transactions Are not supported and will result in warnings and the inner transactions to be ignored. Be sure to **end** each transaction and not to let et long running transaction open (i.e you should execute a rollback or commit for each open begin\_work). ## Transactions and DBI In order to make transactions work properly you have to make sure that the `$dbh` for each DBI drivers are shared among drivers using the same database (basically dsn). One way of doing that is to define a get\_dbh() subref in each DBI driver to return the same dbh if the dsn and attributes of the connection are identical. The other way is to use the new configuration flag on the DBI driver that has been added specifically for this purpose: `reuse_dbh`. ## example coming from the test suite __PACKAGE__->install_properties({ columns => [ 'recipe_id', 'partition_id', 'title' ], datasource => 'recipes', primary_key => 'recipe_id', driver => Data::ObjectDriver::Driver::Cache::Cache->new( cache => Cache::Memory->new, fallback => Data::ObjectDriver::Driver::DBI->new( dsn => 'dbi:SQLite:dbname=global.db', reuse_dbh => 1, ## be sure that the corresponding dbh is shared ), ), }); # EXAMPLES ## A Partitioned, Caching Driver package Ingredient; use strict; use base qw( Data::ObjectDriver::BaseObject ); use Data::ObjectDriver::Driver::DBI; use Data::ObjectDriver::Driver::Partition; use Data::ObjectDriver::Driver::Cache::Cache; use Cache::Memory; use Carp; our $IDs; __PACKAGE__->install_properties({ columns => [ 'ingredient_id', 'recipe_id', 'name', 'quantity', ], datasource => 'ingredients', primary_key => [ 'recipe_id', 'ingredient_id' ], driver => Data::ObjectDriver::Driver::Cache::Cache->new( cache => Cache::Memory->new( namespace => __PACKAGE__ ), fallback => Data::ObjectDriver::Driver::Partition->new( get_driver => \&get_driver, pk_generator => \&generate_pk, ), ), }); sub get_driver { my($terms) = @_; my $recipe; if (ref $terms eq 'HASH') { my $recipe_id = $terms->{recipe_id} or Carp::croak("recipe_id is required"); $recipe = Recipe->lookup($recipe_id); } elsif (ref $terms eq 'ARRAY') { $recipe = Recipe->lookup($terms->[0]); } Carp::croak("Unknown recipe") unless $recipe; Data::ObjectDriver::Driver::DBI->new( dsn => 'dbi:mysql:database=cluster' . $recipe->cluster_id, username => 'foo', pk_generator => \&generate_pk, ); } sub generate_pk { my($obj) = @_; $obj->ingredient_id(++$IDs{$obj->recipe_id}); 1; } 1; # FORK SAFETY As of version 0.21, _Data::ObjectDriver_ resets internal database handles after _fork(2)_ is called, but only if [POSIX::AtFork](https://metacpan.org/pod/POSIX%3A%3AAtFork) module is installed. Otherwise, _Data::ObjectDriver_ is not fork-safe. # SUPPORTED DATABASES _Data::ObjectDriver_ is very modular and it's not very difficult to add new drivers. - MySQL is well supported and has been heavily tested. - PostgreSQL has been used in production and should just work, too. - SQLite is supported, but YMMV depending on the version. This is the backend used for the test suite. - Oracle support has been added in 0.06 # LICENSE _Data::ObjectDriver_ is free software; you may redistribute it and/or modify it under the same terms as Perl itself. # AUTHOR & COPYRIGHT Except where otherwise noted, _Data::ObjectDriver_ is Copyright 2005-2006 Six Apart, cpan@sixapart.com. All rights reserved. ToDo100644000000000000 20314777361274 14636 0ustar00rootroot000000000000Data-ObjectDriver-0.25* multiple column primary keys should allow passing in object, and transparently getting correct column value based on pk column cpanfile100644000000000000 226614777361274 15605 0ustar00rootroot000000000000Data-ObjectDriver-0.25requires 'Class::Accessor::Fast'; requires 'Class::Data::Inheritable'; requires 'Class::Trigger'; requires 'DBI'; requires 'List::Util'; requires 'perl', '5.006001'; recommends 'Text::SimpleTable'; on configure => sub { requires 'Module::Build::Tiny', '0.035'; }; on build => sub { requires 'ExtUtils::MakeMaker', '6.59'; requires 'Test::Exception'; }; on develop => sub { requires 'DBD::SQLite'; requires 'Text::SimpleTable'; }; on test => sub { requires 'version'; }; feature 'test_sqlite', 'Test SQLite' => sub { requires 'DBD::SQLite'; }; feature 'test_mysql', 'Test MySQL' => sub { requires 'DBD::mysql'; requires 'Test::mysqld'; requires 'SQL::Translator'; }; feature 'test_mariadb', 'Test MariaDB' => sub { requires 'DBD::MariaDB'; requires 'Test::mysqld'; requires 'SQL::Translator'; }; feature 'test_postgresql', 'Test PostgreSQL' => sub { requires 'DBD::Pg'; requires 'Test::PostgreSQL'; requires 'SQL::Translator'; }; feature 'test_fork', 'Test Fork' => sub { requires 'DBI', '1.614'; requires 'Parallel::ForkManager'; requires 'POSIX::AtFork'; requires 'Scalar::Util'; requires 'Test::SharedFork'; }; ObjectDriver.pm100644000000000000 5665614777361274 20474 0ustar00rootroot000000000000Data-ObjectDriver-0.25/lib/Data# $Id$ package Data::ObjectDriver; use strict; use warnings; use 5.006_001; use Class::Accessor::Fast; use base qw( Class::Accessor::Fast ); use Data::ObjectDriver::Iterator; __PACKAGE__->mk_accessors(qw( pk_generator txn_active )); our $VERSION = '0.25'; our $DEBUG = $ENV{DOD_DEBUG} || 0; our $PROFILE = $ENV{DOD_PROFILE} || 0; our $PROFILER; our $LOGGER; sub new { my $class = shift; my $driver = bless {}, $class; $driver->init(@_); $driver; } sub logger { my $class = shift; if ( @_ ) { return $LOGGER = shift; } else { return $LOGGER ||= sub { print STDERR @_; }; } } sub init { my $driver = shift; my %param = @_; $driver->pk_generator($param{pk_generator}); $driver->txn_active(0); $driver; } # Alias record_query to start_query *record_query = \*start_query; sub start_query { my $driver = shift; my($sql, $bind) = @_; $driver->debug($sql, $bind) if $DEBUG; $driver->profiler($sql) if $PROFILE; return; } sub end_query { } sub begin_work { my $driver = shift; $driver->txn_active(1); $driver->debug(sprintf("%14s", "BEGIN_WORK") . ": driver=$driver"); } sub commit { my $driver = shift; _end_txn($driver, 'commit'); } sub rollback { my $driver = shift; _end_txn($driver, 'rollback'); } sub _end_txn { my $driver = shift; my $method = shift; $driver->txn_active(0); $driver->debug(sprintf("%14s", uc($method)) . ": driver=$driver"); } sub debug { my $driver = shift; return unless $DEBUG; my $class = ref $driver || $driver; my @caller; my $i = 0; while (1) { @caller = caller($i++); last if $caller[0] !~ /^(Data::ObjectDriver|$class)/; } my $where = " in file $caller[1] line $caller[2]\n"; if (@_ == 1 && !ref($_[0])) { $driver->logger->( @_, $where ); } else { require Data::Dumper; local $Data::Dumper::Indent = 1; $driver->logger->( Data::Dumper::Dumper(@_), $where ); } } sub profiler { my $driver = shift; my ($sql) = @_; local $@; $PROFILER ||= eval { require Data::ObjectDriver::Profiler; Data::ObjectDriver::Profiler->new; }; return $PROFILE = 0 if $@ || !$PROFILER; return $PROFILER unless @_; $PROFILER->record_query($driver, $sql); } sub list_or_iterator { my $driver = shift; my($objs) = @_; ## Emulate the standard search behavior of returning an ## iterator in scalar context, and the full list in list context. if (wantarray) { return @{$objs}; } else { my $iter = sub { shift @{$objs} }; return Data::ObjectDriver::Iterator->new($iter); } } sub cache_object { } sub uncache_object { } 1; __END__ =head1 NAME Data::ObjectDriver - Simple, transparent data interface, with caching =head1 SYNOPSIS ## Set up your database driver code. package FoodDriver; sub driver { Data::ObjectDriver::Driver::DBI->new( dsn => 'dbi:mysql:dbname', username => 'username', password => 'password', ) } ## Set up the classes for your recipe and ingredient objects. package Recipe; use base qw( Data::ObjectDriver::BaseObject ); __PACKAGE__->install_properties({ columns => [ 'recipe_id', 'title' ], datasource => 'recipe', primary_key => 'recipe_id', driver => FoodDriver->driver, }); package Ingredient; use base qw( Data::ObjectDriver::BaseObject ); __PACKAGE__->install_properties({ columns => [ 'ingredient_id', 'recipe_id', 'name', 'quantity' ], datasource => 'ingredient', primary_key => [ 'recipe_id', 'ingredient_id' ], driver => FoodDriver->driver, }); ## And now, use them! my $recipe = Recipe->new; $recipe->title('Banana Milkshake'); $recipe->save; my $ingredient = Ingredient->new; $ingredient->recipe_id($recipe->id); $ingredient->name('Bananas'); $ingredient->quantity(5); $ingredient->save; ## Needs more bananas! $ingredient->quantity(10); $ingredient->save; ## Shorthand constructor my $ingredient = Ingredient->new(recipe_id=> $recipe->id, name => 'Milk', quantity => 2); =head1 DESCRIPTION I is an object relational mapper, meaning that it maps object-oriented design concepts onto a relational database. It's inspired by, and descended from, the I classes in Six Apart's Movable Type and TypePad weblogging products. But it adds in caching and partitioning layers, allowing you to spread data across multiple physical databases, without your application code needing to know where the data is stored. =head1 METHODOLOGY I provides you with a framework for building database-backed applications. It provides built-in support for object caching and database partitioning, and uses a layered approach to allow building very sophisticated database interfaces without a lot of code. You can build a driver that uses any number of caching layers, plus a partitioning layer, then a final layer that actually knows how to load data from a backend datastore. For example, the following code: my $driver = Data::ObjectDriver::Driver::Cache::Memcached->new( cache => Cache::Memcached->new( servers => [ '127.0.0.1:11211' ], ), fallback => Data::ObjectDriver::Driver::Partition->new( get_driver => \&get_driver, ), ); creates a new driver that supports both caching (using memcached) and partitioning. It's useful to demonstrate the flow of a sample request through this driver framework. The following code: my $ingredient = Ingredient->lookup([ $recipe->recipe_id, 1 ]); would take the following path through the I framework: =over 4 =item 1. The caching layer would look up the object with the given primary key in all of the specified memcached servers. If the object was found in the cache, it would be returned immediately. If the object was not found in the cache, the caching layer would fall back to the driver listed in the I setting: the partitioning layer. =item 2. The partitioning layer does not know how to look up objects by itself--all it knows how to do is to give back a driver that I know how to look up objects in a backend datastore. In our example above, imagine that we're partitioning our ingredient data based on the recipe that the ingredient is found in. For example, all of the ingredients for a "Banana Milkshake" would be found in one partition; all of the ingredients for a "Chocolate Sundae" might be found in another partition. So the partitioning layer needs to tell us which partition to look in to load the ingredients for I<$recipe-Erecipe_id>. If we store a I column along with each I<$recipe> object, that information can be loaded very easily, and the partitioning layer will then instantiate a I driver that knows how to load an ingredient from that recipe. =item 3. Using the I driver that the partitioning layer created, I can look up the ingredient with the specified primary key. It will return that key back up the chain, giving each layer a chance to do something with it. =item 4. The caching layer, when it receives the object loaded in Step 3, will store the object in memcached. =item 5. The object will be passed back to the caller. Subsequent lookups of that same object will come from the cache. =back =head1 HOW IS IT DIFFERENT? I differs from other similar frameworks (e.g. L) in a couple of ways: =over 4 =item * It has built-in support for caching. =item * It has built-in support for data partitioning. =item * Drivers are attached to classes, not to the application as a whole. This is essential for partitioning, because your partition drivers need to know how to load a specific class of data. But it can also be useful for caching, because you may find that it doesn't make sense to cache certain classes of data that change constantly. =item * The driver class != the base object class. All of the object classes you declare will descend from I, and all of the drivers you instantiate or subclass will descend from I itself. This provides a useful distinction between your data/classes, and the drivers that describe how to B on that data, meaning that an object based on I is not tied to any particular type of driver. =back =head1 USAGE =head2 Class->lookup($id) Looks up/retrieves a single object with the primary key I<$id>, and returns the object. I<$id> can be either a scalar or a reference to an array, in the case of a class with a multiple column primary key. =head2 Class->lookup_multi(\@ids) Looks up/retrieves multiple objects with the IDs I<\@ids>, which should be a reference to an array of IDs. As in the case of I, an ID can be either a scalar or a reference to an array. Returns a reference to an array of objects B as the IDs you passed in. Any objects that could not successfully be loaded will be represented in that array as an C element. So, for example, if you wanted to load 2 objects with the primary keys C<[ 5, 3 ]> and C<[ 4, 2 ]>, you'd call I like this: Class->lookup_multi([ [ 5, 3 ], [ 4, 2 ], ]); And if the first object in that list could not be loaded successfully, you'd get back a reference to an array like this: [ undef, $object ] where I<$object> is an instance of I. =head2 Class->search(\%terms [, \%options ]) Searches for objects matching the terms I<%terms>. In list context, returns an array of matching objects; in scalar context, returns a reference to a subroutine that acts as an iterator object, like so: my $iter = Ingredient->search({ recipe_id => 5 }); while (my $ingredient = $iter->()) { ... } C<$iter> is blessed in L package, so the above could also be written: my $iter = Ingredient->search({ recipe_id => 5 }); while (my $ingredient = $iter->next()) { ... } The keys in I<%terms> should be column names for the database table modeled by I (and the values should be the desired values for those columns). I<%options> can contain: =over 4 =item * sort The name of a column to use to sort the result set. Optional. =item * direction The direction in which you want to sort the result set. Must be either C or C. Optional. =item * limit The value for a I clause, to limit the size of the result set. Optional. =item * offset The offset to start at when limiting the result set. Optional. =item * fetchonly A reference to an array of column names to fetch in the I statement generated will include a I clause. =item * comment A sql comment to watermark the SQL query. =item * window_size Used when requesting an iterator for the search method and selecting a large result set or a result set of unknown size. In such a case, no LIMIT clause is assigned, which can load all available objects into memory. Specifying C will load objects in manageable chunks. This will also cause any caching driver to be bypassed for issuing the search itself. Objects are still placed into the cache upon load. This attribute is ignored when the search method is invoked in an array context, or if a C attribute is also specified that is smaller than the C. =back =head2 Class->search(\@terms [, \%options ]) This is an alternative calling signature for the search method documented above. When providing an array of terms, it allows for constructing complex expressions that mix 'and' and 'or' clauses. For example: my $iter = Ingredient->search([ { recipe_id => 5 }, -or => { calories => { value => 300, op => '<' } } ]); while (my $ingredient = $iter->()) { ... } Supported logic operators are: '-and', '-or', '-and_not', '-or_not'. =head2 Class->add_trigger($trigger, \&callback) Adds a trigger to all objects of class I, such that when the event I<$trigger> occurs to any of the objects, subroutine C<&callback> is run. Note that triggers will not occur for instances of I of I, only of I itself. See TRIGGERS for the available triggers. =head2 Class->call_trigger($trigger, [@callback_params]) Invokes the triggers watching class I. The parameters to send to the callbacks (in addition to I) are specified in I<@callback_params>. See TRIGGERS for the available triggers. =head2 $obj->save Saves the object I<$obj> to the database. If the object is not yet in the database, I will automatically generate a primary key and insert the record into the database table. Otherwise, it will update the existing record. If an error occurs, I will I. Internally, I calls I for records that already exist in the database, and I for those that don't. =head2 $obj->remove Removes the object I<$obj> from the database. If an error occurs, I will I. =head2 Class->remove(\%terms, \%args) Removes objects found with the I<%terms>. So it's a shortcut of: my @obj = Class->search(\%terms, \%args); for my $obj (@obj) { $obj->remove; } However, when you pass C option set to C<%args>, it won't create objects with C, but issues I SQL directly to the database. ## issues "DELETE FROM tbl WHERE user_id = 2" Class->remove({ user_id => 2 }, { nofetch => 1 }); This might be much faster and useful for tables without Primary Key, but beware that in this case B because no objects are instantiated. =head2 Class->bulk_insert([col1, col2], [[d1,d2], [d1,d2]]); Bulk inserts data into the underlying table. The first argument is an array reference of columns names as specified in install_properties =head2 $obj->add_trigger($trigger, \&callback) Adds a trigger to the object I<$obj>, such that when the event I<$trigger> occurs to the object, subroutine C<&callback> is run. See TRIGGERS for the available triggers. Triggers are invoked in the order in which they are added. =head2 $obj->call_trigger($trigger, [@callback_params]) Invokes the triggers watching all objects of I<$obj>'s class and the object I<$obj> specifically for trigger event I<$trigger>. The additional parameters besides I<$obj>, if any, are passed as I<@callback_params>. See TRIGGERS for the available triggers. =head1 TRIGGERS I provides a trigger mechanism by which callbacks can be called at certain points in the life cycle of an object. These can be set on a class as a whole or individual objects (see USAGE). Triggers can be added and called for these events: =over 4 =item * pre_save -> ($obj, $orig_obj) Callbacks on the I trigger are called when the object is about to be saved to the database. For example, use this callback to translate special code strings into numbers for storage in an integer column in the database. Note that this hook is also called when you C the object. Modifications to I<$obj> will affect the values passed to subsequent triggers and saved in the database, but not the original object on which the I method was invoked. =item * post_save -> ($obj, $orig_obj) Callbaks on the I triggers are called after the object is saved to the database. Use this trigger when your hook needs primary key which is automatically assigned (like auto_increment and sequence). Note that this hooks is B called when you remove the object. =item * pre_insert/post_insert/pre_update/post_update/pre_remove/post_remove -> ($obj, $orig_obj) Those triggers are fired before and after $obj is created, updated and deleted. =item * post_load -> ($obj) Callbacks on the I trigger are called when an object is being created from a database query, such as with the I and I class methods. For example, use this callback to translate the numbers your I callback caused to be saved I into string codes. Modifications to I<$obj> will affect the object passed to subsequent triggers and returned from the loading method. Note I should only be used as a trigger on a class, as the object to which the load is occurring was not previously available for triggers to be added. =item * pre_search -> ($class, $terms, $args) Callbacks on the I trigger are called when a content addressed query for objects of class I<$class> is performed with the I method. For example, use this callback to translate the entry in I<$terms> for your code string field to its appropriate integer value. Modifications to I<$terms> and I<$args> will affect the parameters to subsequent triggers and what objects are loaded, but not the original hash references used in the I query. Note I should only be used as a trigger on a class, as I is never invoked on specific objects. =over The return values from your callbacks are ignored. Note that the invocation of callbacks is the responsibility of the object driver. If you implement a driver that does not delegate to I, it is I responsibility to invoke the appropriate callbacks with the I method. =back =back =head1 PROFILING For performance tuning, you can turn on query profiling by setting I<$Data::ObjectDriver::PROFILE> to a true value. Or, alternatively, you can set the I environment variable to a true value before starting your application. To obtain the profile statistics, get the global I instance: my $profiler = Data::ObjectDriver->profiler; Then see the documentation for I to see the methods on that class. In some applications there are phases of execution in which no I/O operations should occur, but sometimes it's difficult to tell when, where, or if those I/O operations are happening. One approach to surfacing these situations is to set, either globally or locally, the $Data::ObjectDriver::RESTRICT_IO flag. If set, this will tell Data::ObjectDriver to die with some context rather than executing network calls for data. =head1 TRANSACTIONS Transactions are supported by Data::ObjectDriver's default drivers. So each Driver is capable to deal with transactional state independently. Additionally class know how to turn transactions switch on for all objects. In the case of a global transaction all drivers used during this time are put in a transactional state until the end of the transaction. =head2 Example ## start a transaction Data::ObjectDriver::BaseObject->begin_work; $recipe = Recipe->new; $recipe->title('lasagnes'); $recipe->save; my $ingredient = Ingredient->new; $ingredient->recipe_id($recipe->recipe_id); $ingredient->name("more layers"); $ingredient->insert; $ingredient->remove; if ($you_are_sure) { Data::ObjectDriver::BaseObject->commit; } else { ## erase all trace of the above Data::ObjectDriver::BaseObject->rollback; } =head2 Driver implementation Drivers have to implement the following methods: =over 4 =item * begin_work to initialize a transaction =item * rollback =item * commit =back =head2 Nested transactions Are not supported and will result in warnings and the inner transactions to be ignored. Be sure to B each transaction and not to let et long running transaction open (i.e you should execute a rollback or commit for each open begin_work). =head2 Transactions and DBI In order to make transactions work properly you have to make sure that the C<$dbh> for each DBI drivers are shared among drivers using the same database (basically dsn). One way of doing that is to define a get_dbh() subref in each DBI driver to return the same dbh if the dsn and attributes of the connection are identical. The other way is to use the new configuration flag on the DBI driver that has been added specifically for this purpose: C. ## example coming from the test suite __PACKAGE__->install_properties({ columns => [ 'recipe_id', 'partition_id', 'title' ], datasource => 'recipes', primary_key => 'recipe_id', driver => Data::ObjectDriver::Driver::Cache::Cache->new( cache => Cache::Memory->new, fallback => Data::ObjectDriver::Driver::DBI->new( dsn => 'dbi:SQLite:dbname=global.db', reuse_dbh => 1, ## be sure that the corresponding dbh is shared ), ), }); =head1 EXAMPLES =head2 A Partitioned, Caching Driver package Ingredient; use strict; use base qw( Data::ObjectDriver::BaseObject ); use Data::ObjectDriver::Driver::DBI; use Data::ObjectDriver::Driver::Partition; use Data::ObjectDriver::Driver::Cache::Cache; use Cache::Memory; use Carp; our $IDs; __PACKAGE__->install_properties({ columns => [ 'ingredient_id', 'recipe_id', 'name', 'quantity', ], datasource => 'ingredients', primary_key => [ 'recipe_id', 'ingredient_id' ], driver => Data::ObjectDriver::Driver::Cache::Cache->new( cache => Cache::Memory->new( namespace => __PACKAGE__ ), fallback => Data::ObjectDriver::Driver::Partition->new( get_driver => \&get_driver, pk_generator => \&generate_pk, ), ), }); sub get_driver { my($terms) = @_; my $recipe; if (ref $terms eq 'HASH') { my $recipe_id = $terms->{recipe_id} or Carp::croak("recipe_id is required"); $recipe = Recipe->lookup($recipe_id); } elsif (ref $terms eq 'ARRAY') { $recipe = Recipe->lookup($terms->[0]); } Carp::croak("Unknown recipe") unless $recipe; Data::ObjectDriver::Driver::DBI->new( dsn => 'dbi:mysql:database=cluster' . $recipe->cluster_id, username => 'foo', pk_generator => \&generate_pk, ); } sub generate_pk { my($obj) = @_; $obj->ingredient_id(++$IDs{$obj->recipe_id}); 1; } 1; =head1 FORK SAFETY As of version 0.21, I resets internal database handles after I is called, but only if L module is installed. Otherwise, I is not fork-safe. =head1 SUPPORTED DATABASES I is very modular and it's not very difficult to add new drivers. =over 4 =item * MySQL is well supported and has been heavily tested. =item * PostgreSQL has been used in production and should just work, too. =item * SQLite is supported, but YMMV depending on the version. This is the backend used for the test suite. =item * Oracle support has been added in 0.06 =back =head1 LICENSE I is free software; you may redistribute it and/or modify it under the same terms as Perl itself. =head1 AUTHOR & COPYRIGHT Except where otherwise noted, I is Copyright 2005-2006 Six Apart, cpan@sixapart.com. All rights reserved. =cut BaseObject.pm100644000000000000 11117614777361274 22502 0ustar00rootroot000000000000Data-ObjectDriver-0.25/lib/Data/ObjectDriver# $Id$ package Data::ObjectDriver::BaseObject; use strict; use warnings; our $HasWeaken; eval q{ use Scalar::Util qw(weaken) }; ## no critic $HasWeaken = !$@; use Carp (); use Class::Trigger qw( pre_save post_save post_load pre_search pre_insert post_insert pre_update post_update pre_remove post_remove post_inflate ); use Data::ObjectDriver::ResultSet; ## Global Transaction variables our @WorkingDrivers; our $TransactionLevel = 0; sub install_properties { my $class = shift; my($props) = @_; my $columns = delete $props->{columns}; $props->{columns} = []; { no strict 'refs'; ## no critic *{"${class}::__properties"} = sub { $props }; } foreach my $col (@$columns) { $class->install_column($col); } return $props; } sub install_column { my($class, $col, $type) = @_; my $props = $class->properties; push @{ $props->{columns} }, $col; $props->{column_names}{$col} = (); # predefine getter/setter methods here # Skip adding this method if the class overloads it. # this lets the SUPER::columnname magic do it's thing if (! $class->can($col)) { no strict 'refs'; ## no critic *{"${class}::$col"} = $class->column_func($col); } if ($type) { $props->{column_defs}{$col} = $type; } } sub properties { my $this = shift; my $class = ref($this) || $this; $class->__properties; } # see docs below sub has_a { my $class = shift; my @args = @_; # Iterate over each remote object foreach my $config (@args) { my $parentclass = $config->{class}; # Parameters my $column = $config->{column}; my $method = $config->{method}; my $cached = $config->{cached} || 0; my $parent_method = $config->{parent_method}; # column is required if (!defined($column)) { die "Please specify a valid column for $parentclass" } # create a method name based on the column if (! defined $method) { if (!ref($column)) { $method = $column; $method =~ s/_id$//; $method .= "_obj"; } elsif (ref($column) eq 'ARRAY') { foreach my $col (@{$column}) { my $part = $col; $part =~ s/_id$//; $method .= $part . '_'; } $method .= "obj"; } } # die if we have clashing methods method if (! defined $method || defined(*{"${class}::$method"})) { die "Please define a valid method for $class->$column"; } if ($cached) { # Store cached item inside this object's namespace my $cachekey = "__cache_$method"; no strict 'refs'; ## no critic *{"${class}::$method"} = sub { my $obj = shift; return $obj->{$cachekey} if defined $obj->{$cachekey}; my $id = (ref($column) eq 'ARRAY') ? [ map { $obj->{column_values}->{$_} } @{$column}] : $obj->{column_values}->{$column} ; ## Hold in a variable here too, so we don't lose it immediately ## by having only the weak reference. my $ret = $parentclass->lookup($id); if ($HasWeaken) { $obj->{$cachekey} = $ret; weaken($obj->{$cachekey}); } return $ret; }; } else { if (ref($column)) { no strict 'refs'; ## no critic *{"${class}::$method"} = sub { my $obj = shift; return $parentclass->lookup([ map{ $obj->{column_values}->{$_} } @{$column}]); }; } else { no strict 'refs'; ## no critic *{"${class}::$method"} = sub { return $parentclass->lookup(shift()->{column_values}->{$column}); }; } } # now add to the parent if (!defined $parent_method) { $parent_method = lc($class); $parent_method =~ s/^.*:://; $parent_method .= '_objs'; } if (ref($column)) { no strict 'refs'; ## no critic *{"${parentclass}::$parent_method"} = sub { my $obj = shift; my $terms = shift || {}; my $args = shift; my $primary_key = $obj->primary_key; # inject pk search into given terms. # composite key, ugh foreach my $key (@$column) { $terms->{$key} = shift(@{$primary_key}); } return $class->search($terms, $args); }; } else { no strict 'refs'; ## no critic *{"${parentclass}::$parent_method"} = sub { my $obj = shift; my $terms = shift || {}; my $args = shift; # TBD - use primary_key_to_terms $terms->{$column} = $obj->primary_key; return $class->search($terms, $args); }; }; } # end of loop over class names return; } sub driver { my $class = shift; $class->properties->{driver} ||= $class->properties->{get_driver}->(); } sub get_driver { my $class = shift; $class->properties->{get_driver} = shift if @_; } sub new { my $obj = bless {}, shift; return $obj->init(@_); } sub init { my $self = shift; while (@_) { my $field = shift; my $val = shift; $self->$field($val); } return $self; } sub is_pkless { my $obj = shift; my $prop_pk = $obj->properties->{primary_key}; return 1 if ! $prop_pk; return 1 if ref $prop_pk eq 'ARRAY' && ! @$prop_pk; } sub is_primary_key { my $obj = shift; my($col) = @_; my $prop_pk = $obj->properties->{primary_key}; if (ref($prop_pk)) { for my $pk (@$prop_pk) { return 1 if $pk eq $col; } } else { return 1 if $prop_pk eq $col; } return; } sub primary_key_tuple { my $obj = shift; my $pk = $obj->properties->{primary_key} || return; $pk = [ $pk ] unless ref($pk) eq 'ARRAY'; $pk; } sub primary_key { my $obj = shift; my $pk = $obj->primary_key_tuple; my @val = map { $obj->$_() } @$pk; @val == 1 ? $val[0] : \@val; } sub is_same_array { my($a1, $a2) = @_; return if ($#$a1 != $#$a2); for (my $i = 0; $i <= $#$a1; $i++) { return if $a1->[$i] ne $a2->[$i]; } return 1; } sub primary_key_to_terms { my($obj, $id) = @_; my $pk = $obj->primary_key_tuple; if (! defined $id) { $id = $obj->primary_key; } else { if (ref($id) eq 'HASH') { my @keys = sort keys %$id; unless (is_same_array(\@keys, [ sort @$pk ])) { Carp::confess("keys don't match with primary keys: @keys|@$pk"); } return $id; } } $id = [ $id ] unless ref($id) eq 'ARRAY'; my %terms; @terms{@$pk} = @$id; \%terms; } sub is_same { my($obj, $other) = @_; my @a; for my $o ($obj, $other) { push @a, [ map { $o->$_() } @{ $o->primary_key_tuple }]; } return is_same_array( @a ); } sub object_is_stored { my $obj = shift; return $obj->{__is_stored} ? 1 : 0; } sub pk_str { my ($obj) = @_; my $pk = $obj->primary_key; return $pk unless ref ($pk) eq 'ARRAY'; return join (":", @$pk); } sub has_primary_key { my $obj = shift; return unless @{$obj->primary_key_tuple}; my $val = $obj->primary_key; $val = [ $val ] unless ref($val) eq 'ARRAY'; for my $v (@$val) { return unless defined $v; } 1; } sub datasource { $_[0]->properties->{datasource} } sub columns_of_type { my $obj = shift; my($type) = @_; my $props = $obj->properties; my $cols = $props->{columns}; my $col_defs = $props->{column_defs}; my @cols; for my $col (@$cols) { push @cols, $col if $col_defs->{$col} && $col_defs->{$col} eq $type; } \@cols; } sub set_values { my $obj = shift; my $values = shift; for my $col (keys %$values) { unless ( $obj->has_column($col) ) { Carp::croak("You tried to set non-existent column $col to value $values->{$col} on " . ref($obj)); } $obj->$col($values->{$col}); } } sub set_values_internal { my $obj = shift; my $values = shift; for my $col (keys %$values) { # Not needed for the internal version of this method #unless ( $obj->has_column($col) ) { # Carp::croak("You tried to set inexistent column $col to value $values->{$col} on " . ref($obj)); #} $obj->column_values->{$col} = $values->{$col}; } } sub clone { my $obj = shift; my $clone = $obj->clone_all; for my $pk (@{ $obj->primary_key_tuple }) { $clone->$pk(undef); } $clone; } sub clone_all { my $obj = shift; my $clone = ref($obj)->new(); $clone->set_values_internal($obj->column_values); $clone->{changed_cols} = defined $obj->{changed_cols} ? { %{$obj->{changed_cols}} } : undef; $clone; } sub has_column { return exists $_[0]->properties->{column_names}{$_[1]}; } sub column_names { ## Reference to a copy. [ @{ shift->properties->{columns} } ] } sub column_values { $_[0]->{'column_values'} ||= {} } ## In 0.1 version we didn't die on inexistent column ## which might lead to silent bugs ## You should override column if you want to find the old ## behaviour sub column { my $obj = shift; my $col = shift or return; unless ($obj->has_column($col)) { Carp::croak("Cannot find column '$col' for class '" . ref($obj) . "'"); } # set some values if (@_) { $obj->{column_values}->{$col} = shift; unless ($_[0] && ref($_[0]) eq 'HASH' && $_[0]->{no_changed_flag}) { $obj->{changed_cols}->{$col}++; } } $obj->{column_values}->{$col}; } sub column_func { my $obj = shift; my $col = shift or die "Must specify column"; return sub { my $obj = shift; # getter return $obj->{column_values}->{$col} unless (@_); # setter my ($val, $flags) = @_; $obj->{column_values}->{$col} = $val; unless ($flags && ref($flags) eq 'HASH' && $flags->{no_changed_flag}) { $obj->{changed_cols}->{$col}++; } return $obj->{column_values}->{$col}; }; } sub changed_cols_and_pk { my $obj = shift; keys %{$obj->{changed_cols}}; } sub changed_cols { my $obj = shift; my $pk = $obj->primary_key_tuple; my %pk = map { $_ => 1 } @$pk; grep !$pk{$_}, $obj->changed_cols_and_pk; } sub is_changed { my $obj = shift; if (@_) { return exists $obj->{changed_cols}->{$_[0]}; } else { return $obj->changed_cols > 0; } } sub exists { my $obj = shift; return 0 unless $obj->has_primary_key; $obj->_proxy('exists', @_); } sub save { my $obj = shift; if ($obj->exists(@_)) { return $obj->update(@_); } else { return $obj->insert(@_); } } sub bulk_insert { my $class = shift; my $driver = $class->driver; return $driver->bulk_insert($class, @_); } sub lookup { my $class = shift; my $driver = $class->driver; my $obj = $driver->lookup($class, @_) or return; $driver->cache_object($obj); $obj; } sub lookup_multi { my $class = shift; my $driver = $class->driver; my $objs = $driver->lookup_multi($class, @_) or return; for my $obj (@$objs) { $driver->cache_object($obj) if $obj; } $objs; } sub result { my $class = shift; my ($terms, $args) = @_; return Data::ObjectDriver::ResultSet->new({ class => (ref $class || $class), page_size => delete $args->{page_size}, paging => delete $args->{no_paging}, terms => $terms, args => $args, }); } sub search { my $class = shift; my($terms, $args) = @_; my $driver = $class->driver; if (wantarray) { my @objs = $driver->search($class, $terms, $args); ## Don't attempt to cache objects where the caller specified fetchonly, ## because they won't be complete. ## Also skip this step if we don't get any objects back from the search if (!$args->{fetchonly} || !@objs) { for my $obj (@objs) { $driver->cache_object($obj) if $obj; } } return @objs; } else { my $iter = $driver->search($class, $terms, $args); return $iter if $args->{fetchonly}; my $caching_iter = sub { my $d = $driver; my $o = $iter->(); unless ($o) { $iter->end; return; } $driver->cache_object($o); return $o; }; return Data::ObjectDriver::Iterator->new($caching_iter, sub { $iter->end }); } } sub remove { shift->_proxy( 'remove', @_ ) } sub update { shift->_proxy( 'update', @_ ) } sub insert { shift->_proxy( 'insert', @_ ) } sub replace { shift->_proxy( 'replace', @_ ) } sub fetch_data { shift->_proxy( 'fetch_data', @_ ) } sub uncache_object { shift->_proxy( 'uncache_object', @_ ) } sub refresh { my $obj = shift; return unless $obj->has_primary_key; my $fields = $obj->fetch_data; $obj->set_values_internal($fields); $obj->call_trigger('post_load'); $obj->driver->cache_object($obj); return 1; } ## NOTE: I wonder if it could be useful to BaseObject superclass ## to override the global transaction flag. If so, I'd add methods ## to manipulate this flag and the working drivers. -- Yann sub _proxy { my $obj = shift; my($meth, @args) = @_; my $driver = $obj->driver; ## faster than $obj->txn_active && ! $driver->txn_active but see note. if ($TransactionLevel && ! $driver->txn_active) { $driver->begin_work; push @WorkingDrivers, $driver; } $driver->$meth($obj, @args); } sub txn_active { $TransactionLevel } sub begin_work { my $class = shift; if ( $TransactionLevel > 0 ) { Carp::carp( $TransactionLevel > 1 ? "$TransactionLevel transactions already active" : "Transaction already active" ); } $TransactionLevel++; } sub commit { my $class = shift; $class->_end_txn('commit'); } sub rollback { my $class = shift; $class->_end_txn('rollback'); } sub _end_txn { my $class = shift; my $meth = shift; ## Ignore nested transactions if ($TransactionLevel > 1) { $TransactionLevel--; return; } if (! $TransactionLevel) { Carp::carp("No active transaction to end; ignoring $meth"); return; } my @wd = @WorkingDrivers; $TransactionLevel--; @WorkingDrivers = (); for my $driver (@wd) { $driver->$meth; } } sub txn_debug { my $class = shift; return { txn => $TransactionLevel, drivers => \@WorkingDrivers, }; } sub deflate { { columns => shift->column_values } } sub inflate { my $class = shift; my($deflated) = @_; my $obj = $class->new; $obj->set_values_internal($deflated->{columns}); $obj->call_trigger('post_inflate'); return $obj; } sub DESTROY { } sub AUTOLOAD { my $obj = $_[0]; (my $col = our $AUTOLOAD) =~ s!.+::!!; Carp::croak("Cannot find method '$col' for class '$obj'") unless ref $obj; unless ($obj->has_column($col)) { Carp::croak("Cannot find column '$col' for class '" . ref($obj) . "'"); } { no strict 'refs'; ## no critic *$AUTOLOAD = $obj->column_func($col); } goto &$AUTOLOAD; } sub has_partitions { my $class = shift; my(%param) = @_; my $how_many = delete $param{number} or Carp::croak("number (of partitions) is required"); ## save the number of partitions in the class $class->properties->{number_of_partitions} = $how_many; ## Save the get_driver subref that we were passed, so that the ## SimplePartition driver can access it. $class->properties->{partition_get_driver} = delete $param{get_driver} or Carp::croak("get_driver is required"); ## When creating a new $class object, we should automatically fill in ## the partition ID by selecting one at random, unless a partition_id ## is already defined. This allows us to keep it simple but for the ## caller to do something more complex, if it wants to. $class->add_trigger(pre_insert => sub { my($obj, $orig_obj) = @_; unless (defined $obj->partition_id) { my $partition_id = int(rand $how_many) + 1; $obj->partition_id($partition_id); $orig_obj->partition_id($partition_id); } }); } 1; __END__ =head1 NAME Data::ObjectDriver::BaseObject - base class for modeled objects =head1 SYNOPSIS package Ingredient; use base qw( Data::ObjectDriver::BaseObject ); __PACKAGE__->install_properties({ columns => [ 'ingredient_id', 'recipe_id', 'name', 'quantity' ], datasource => 'ingredient', primary_key => [ 'recipe_id', 'ingredient_id' ], driver => FoodDriver->driver, }); __PACKAGE__->has_a( { class => 'Recipe', column => 'recipe_id', } ); package main; my ($ingredient) = Ingredient->search({ recipe_id => 4, name => 'rutabaga' }); $ingredient->quantity(7); $ingredient->save(); =head1 DESCRIPTION I provides services to data objects modeled with the I object relational mapper. =head1 CLASS DEFINITION =head2 Cinstall_properties(\%params)> Defines all the properties of the specified object class. Generally you should call C in the body of your class definition, so the properties can be set when the class is Cd or Cd. Required members of C<%params> are: =over 4 =item * C All the columns in the object class. This property is an arrayref. =item * C The identifier of the table in which the object class's data are stored. Usually the datasource is simply the table name, but the datasource can be decorated into the table name by the C module if the database requires special formatting of table names. =item * C or C The driver used to perform database operations (lookup, update, etc) for the object class. C is the instance of C to use. If your driver requires configuration options not available when the properties are initially set, specify a coderef as C instead. It will be called the first time the driver is needed, storing the driver in the class's C property for subsequent calls. =back The optional members of C<%params> are: =over 4 =item * C The column or columns used to uniquely identify an instance of the object class. If one column (such as a simple numeric ID) identifies the class, C should be a scalar. Otherwise, C is an arrayref. =item * C Specifies types for specially typed columns, if any, as a hashref. For example, if a column holds a timestamp, name it in C as a C for proper handling with some C database drivers. Columns for which types aren't specified are handled as C columns. Known C types are: =over 4 =item * C A blob of binary data. C maps this to C, C to C and C to C. =item * C A non-blob string of binary data. C maps this to C. =back Other types may be defined by custom database drivers as needed, so consult their documentation. =item * C The name of the database. When used with C type object drivers, this name is passed to the C method when the actual database handle is being created. =back Custom object drivers may define other properties for your object classes. Consult the documentation of those object drivers for more information. =head2 Cinstall_column($col, $def)> Modify the Class definition to declare a new column C<$col> of definition <$def> (see L). =head2 Chas_a(@definitions)> B C is an experimental system, likely to both be buggy and change in future versions. Defines a foreign key reference between two classes, creating accessor methods to retrieve objects both ways across the reference. For each defined reference, two methods are created: one for objects of class C to load the objects they reference, and one for objects of the referenced class to load the set of C objects that reference I. For example, this definition: package Ingredient; __PACKAGE__->has_a( { class => 'Recipe', column => 'recipe_id' }, ); would create Crecipe_obj> and Cingredient_objs> instance methods. Each member of C<@definitions> is a hashref containing the parameters for creating one accessor method. The required members of these hashes are: =over 4 =item * C The class to associate. =item * C The column or columns in this class that identify the primary key of the associated object. As with primary keys, use a single scalar string for a single column or an arrayref for a composite key. =back The optional members of C definitions are: =over 4 =item * C The name of the accessor method to create. By default, the method name is the concatenated set of column names with each C<_id> suffix removed, and the suffix C<_obj> appended at the end of the method name. For example, if C were C<['recipe_id', 'ingredient_id']>, the resulting method would be called C by default. =item * C Whether to keep a reference to the foreign object once it's loaded. Subsequent calls to the accessor method would return that reference immediately. =item * C The name of the reciprocal method created in the referenced class named in C. By default, that method is named with the lowercased name of the current class with the suffix C<_objs>. For example, if in your C class you defined a relationship with C on the column C, this would create a C<$recipe-Eingredient_objs> method. Note that if you reference one class with multiple sets of fields, you can omit only one parent_method; otherwise the methods would be named the same thing. For instance, if you had a C class with two references to C objects in its C and C columns, one of them would need a C. =back =head2 Chas_partitions(%param)> Defines that the given class is partitioned, configuring it for use with the C object driver. Required members of C<%param> are: =over 4 =item * C The number of partitions in which objects of this class may be stored. =item * C A function that returns an object driver, given a partition ID and any extra parameters specified when the class's C was instantiated. =back Note that only the parent object for use with the C driver should use C. See C for more about partitioning. =head1 BASIC USAGE =head2 Clookup($id)> Returns the instance of C with the given value for its primary key. If C has a complex primary key (more than one column), C<$id> should be an arrayref specifying the column values in the same order as specified in the C property. =head2 Csearch(\%terms, [\%args])> Returns all instances of C that match the values specified in C<\%terms>, keyed on column names. In list context, C returns the objects containing those values. In scalar context, C returns an iterator function containing the same set of objects. Your search can be customized with parameters specified in C<\%args>. Commonly recognized parameters (those implemented by the standard C object drivers) are: =over 4 =item * C A column by which to order the object results. =item * C If set to C, the results (ordered by the C column) are returned in descending order. Otherwise, results will be in ascending order. =item * C The number of results to return, at most. You can use this with C to paginate your C results. =item * C The number of results to skip before the first returned result. Use this with C to paginate your C results. =item * C A list (arrayref) of columns that should be requested. If specified, only the specified columns of the resulting objects are guaranteed to be set to the correct values. Note that any caching object drivers you use may opt to ignore C instructions, or decline to cache objects queried with C. =item * C If true, instructs the object driver to indicate the query is a search, but the application may want to update the data after. That is, the generated SQL C. =head2 $profiler->query_frequency Returns a reference to a hash containing, as keys, all of the SQL statements in the query log, where the value for each of the keys is a number representing the number of times the query was executed. =head2 $profiler->reset Resets the statistics and the query log. =head2 $profiler->total_queries Returns the total number of queries currently logged in the profiler. =head2 $profiler->report_queries_by_type Returns a string containing a pretty report of information about the current number of each type of query in the profiler (e.g. C (arrayref) The database columns to select in a C query should return DISTINCT rows only. =head2 C (hashref) The map of database column names to object fields in a C list to column names. =head2 C (hashref) The map of object fields to database column names in a C query. Note if you perform a C query. The requested object member will be indicated to be C<$term> in the statement's C and C attributes. C<$term> is optional, and defaults to the same value as C<$column>. =head2 C<$sql-Eadd_join($table, \@joins)> Adds the join statement indicated by C<$table> and C<\@joins> to the list of C table references for the statement. The structure for the set of joins are as described for the C attribute member above. =head2 C<$sql-Eadd_index_hint($table, $index)> Specifies a particular index to use for a particular table. =head2 C<$sql-Eadd_where($column, $value)> Adds a condition on the value of the database column C<$column> to the statement's C clause. A record will be tested against the below conditions according to what type of data structure C<$value> is: =over 4 =item * a scalar The value of C<$column> must equal C<$value>. =item * a reference to a scalar The value of C<$column> must evaluate true against the SQL given in C<$$value>. For example, if C<$$value> were C, C<$column> must be C for a record to pass. =item * a hashref The value of C<$column> must compare against the condition represented by C<$value>, which can contain the members: =over 4 =item * C The value with which to compare (required). =item * C The SQL operator with which to compare C and the value of C<$column> (required). =item * C The column name for the comparison. If this is present, it overrides the column name C<$column>, allowing you to build more complex conditions like C<((foo = 1 AND bar = 2) OR (baz = 3))>. =back For example, if C were C and C were C, a record's C<$column> column would have to be C to match. =item * an arrayref of scalars The value of C<$column> may equal any of the members of C<@$value>. The generated SQL performs the comparison with as an C expression. =item * an arrayref of (mostly) references The value of C<$column> must compare against I of the expressions represented in C<@$value>. Each member of the list can be any of the structures described here as possible forms of C<$value>. If the first member of the C<@$value> array is the scalar string C<-and>, I subsequent members of <@$value> must be met for the record to match. Note this is not very useful unless contained as one option of a larger C alternation. =back All individual conditions specified with C must be true for a record to be a result of the query. Beware that you can create a circular reference that will recursively generate an infinite SQL statement (for example, by specifying a arrayref C<$value> that itself contains C<$value>). As C evaluates your expressions before storing the conditions in the C attribute as a generated SQL string, this will occur when calling C, not C. So don't do that. =head2 C<$sql-Eadd_complex_where(\@list)> This method accepts an array reference of clauses that are glued together with logical operators. With it, you can express where clauses that mix logical operators together to produce more complex queries. For instance: [ { foo => 1, bar => 2 }, -or => { baz => 3 } ] The values given for the columns support all the variants documented for the C method above. Logical operators used inbetween the hashref elements can be one of: '-or', '-and', '-or_not', '-and_not'. =head2 C<$sql-Ehas_where($column, [$value])> Returns whether a where clause for the column C<$column> was added to the statement with the C method. The C<$value> argument is currently ignored. =head2 C<$sql-Eadd_having($column, $value)> Adds an expression to the C portion of the statement's C clause. The expression compares C<$column> using C<$value>, which can be any of the structures described above for the C method. =head2 C<$sql-Eadd_index_hint($table, \@hints)> Addes the index hint into a C