NAME DBIx::QuickORM - Actively maintained Object Relational Mapping that makes getting started Quick and has a rich feature set. EXTREMELY EARLY VERSION WARNING! THIS IS A VERY EARLY VERSION! About 90% of the functionality from the features section is written. About 80% of the features have been listed. About 40% of the written code is tested. About 10% of the documentation has been written. If you want to try it, go for it. Some of the tests give a pretty good idea of how to use it. DO NOT USE THIS FOR ANYTHING PRODUCTION it is not ready yet. The API can and will change! DESCRIPTION An actively maintained ORM tool that is quick and easy to start with, but powerful and expandable for long term and larger projects. An alternative to DBIx::Class, but not a drop-in replacement. SCOPE The primary scope of this project is to write a good ORM for perl. It is very easy to add scope, and try to focus on things outside this scope. I am not opposed to such things being written around the ORM functionality, afterall the project has a lot of useful code, and knowledge of the database. But the primary focus must always be the ORM functionality, and it must not suffer in favor of functionality beyond that scope. SYNOPSIS FIXME! MOTIVATION The most widely accepted ORM for perl, DBIx::Class is for all intents and purposes, dead. There is only 1 maintainer, and that person has stated that the project is feature complete. The project will recieve no updates apart from critical bugs. The distribution has been marked such that it absolutely can never be transferred to anyone else. There are 4 ways forward: Use DBIx::Class it as it is. Many people continue to do this. Monkeypatch DBIx::Class I know a handful of people who are working on a way to do this that is not terrible and will effectively keep DBIx::Class on life support. Fork DBIx::Class I was initially going to take this route. But after a couple hours in the codebase I realized I dislike the internals of DBIx::Class almost as much as I dislike using its interface. Write an alternative I decided to take this route. I have never liked DBIx::Class, I find it difficult to approach, and it is complicated to start a project with it. The interface is unintuitive, and the internals are very opaque. My goal is to start with the interface, make it approachable, easy to start, etc. I also want the interface to be intuitive to use. I also want expandability. I also want to make sure I adopt the good ideas and capabilities from DBIx::Class. Only a fool would say DBIx::Class has nothing of value. MAINTENANCE COMMITMENT I want to be sure that what happened to DBIx::Class cannot happen to this project. I will maintain this as long as I am able. When I am not capable I will let others pick up where I left off. I am stating here, in the docs, for all to see for all time: If I become unable to maintain this project, I approve of others being given cpan and github permissions to develop and release this distribution. Peferably maint will be handed off to someone who has been a contributor, or to a group of contributors, If none can be found, or none are willing, I trust the cpan toolchain group to takeover. FEATURE/GOAL OVERVIEW Quick to start It should be very simple to start a project. The ORM should stay out of your way until you want to make it do something for you. Intuitive Names, interfaces, etc should make sense and be obvious. Declarative syntax Look at the "DECLARATIVE INTERFACE" section below, or the "SYNOPSIS" section above. SQL <-> Perl conversion It can go either way. Generate the perl schema from a populated database. my $orm = orm 'MyOrm' => sub { # First provide db credentials and connect info db { ... }; # Tell DBIx::QuickORM to do the rest autofill(); }; # Built for you by reading from the database. my $schema = $orm->schema; Generate SQL to populate a database from a schema defined in perl. See DBIx::QuickORM::Util::SchemaBuilder for more info. Async query support Async query support is a key and first class feature of DBIx::QuickORM. Single async query - single connection Launch an async query on the current connection, then do other stuff until it is ready. See DBIx::QuickORM::Select::Async for full details. but here are some teasers: # It can take more args than just \%where, this is just a simply case my $async = $orm->async(\%where)->start; until ($async->ready) { ... }; my @rows = $async->all; You can also turn any select into an async: my $select = $orm->select(...); my $async = $orm->async; $async->start; Multiple concurrent async query support - multiple connections on 1 process DBIx::QuickORM calls this an 'aside'. See DBIx::QuickORM::Select::Aside for more detail. In this case we have 2 queries executing simultaneously. my $aside = $orm->aside(\%where)->start; # Runs async query on a new connection my $select = $orm->select(\%where); my @rows1 = $select->all; my @rows2 = $aside->all; Note that if both queries return some of the same rows there will only be 1 copy in cache, and both @row arrays will have the same object reference. Multiple concurrent async query support - emulation via forking See DBIx::QuickORM::Select::Forked for more detail. Similar to the 'aside' functionality above, but instead of running an async query on a new connection, a new process is forked, and that process does a synchronous query and returns the results. This is useful for emulating aside/async with databases that do not support it such as SQLite. First class inflation and deflation (Conflation) Inflation and Deflation of columns is a first-class feature. But since saying 'inflation and deflation' every time is a chore DBIx::QuickORM shortens the concept to 'conflation'. No, the word "conflation" is not actually related to "inflation" or "deflation", but it is an amusing pun, specially since it still kind of works with the actual definition of "conflation". If you specify that a column has a conflator, then using my $val = $row->column('name') will give you the inflated form. You can also set the column by giving it either the inflated or deflated form. You also always have access to the raw values, and asking for either the 'stored' or 'dirty' value will give the raw form. You can also use inflated forms in the %where argument to select/find. The rows are also smart enough to check if your inflated forms have been mutated and consider the row dirty (in need of saving or discarding) after the mutation. This is done by deflating the values to compare to the stored form when checking for dirtyness. If your inflated values are readonly, locked restricted hashes, or objects that implement the 'qorm_immutible' method (and it returns true). Then the row is smart enough to skip checking them for mutations as they cannot be mutated. Oh, also of note, inflated forms do not need to be blessed, nor do they even need to be references. You could write a conflator that inflates string to have "inflated: " prefixed to them, and no prefix when they are raw/deflated. A conflator that encrypts/decrypts passively is also possible, assuming the encrypted and decrypted forms are easily distinguishable. UUID, UUID::Binary, UUID::Stringy Automatically inflate and deflate UUID's. Your database can store it as a native UUID, a BIN(16), a VARCHAR(36), or whatever. Tell the orm the row should be conflated as a UUID and it will just work. You can set the value by providing a string, binary data, or anything else the conflator recognizes. In the DB it will store the right type, and in perl you will get a UUID object. schema sub { table my_table => sub { column thing_uuid => sub { conflate 'UUID'; # OR provide '+Your::Conflator', adds 'DBIx::QuickORM::Conflator::' without the '+' }; }; }; DBIx::QuickORM::Conflator::UUID Inflates to an object of this class, deflates to whatever the database column type is. Object stringifies as a UUID string, and you can get both the string and binary value from it through accessors. If generating the SQL to populate the db this will tell it the column should be the 'UUID' type, and will throw an exception if that type is not supported by the db. DBIx::QuickORM::Conflator::UUID::Binary This is useful only if you are generating the schema SQL to populate the db and the db does not support UUID types. This will create the column using a binary data type like BIN(16). DBIx::QuickORM::Conflator::UUID::Stringy This is useful only if you are generating the schema SQL to populate the db and the db does not support UUID types. This will create the column using a stringy data type like VARCHAR(36). JSON, JSON::ASCII This conflator will inflate the JSON into a perl data structure and deflate it back into a JSON string. This uses Cpanel::JSON::XS under the hood. DBIx::QuickORM::Conflator::JSON Defaults to $json->utf8->encode_json This produces a utf8 encoded json string. DBIx::QuickORM::Conflator::JSON::ASCII Defaults to $json->ascii->encode_json This produces an ASCII encoded json string with non-ascii characters escaped. DateTime - Will not leave a mess with Data::Dumper! DBIx::QuickORM::Conflator::DateTime This conflator will inflate dates and times into DateTime objects. However it also wraps them in an DBIx::QuickORM::Util::Mask object. This object hides the DateTime object in a sub { $datetime }. When dumped by Data::Dumper you get something like this: bless( [ '2024-10-26T06:18:45', sub { "DUMMY" } ], 'DBIx::QuickORM::Conflator::DateTime' ); This is much better than spewing the DateTime internals, whcih can take several pages of scrollback. You can still call any valid DateTime method on this object and it will delegate it to the one that is masked beind the coderef. Custom conflator See the DBIx::QuickORM::Role::Conflator role. Custom on the fly Declarative: my $conflator = conflator NAME => sub { inflate { ... }; deflate { ... }; }; OOP: my $conflator = DBIx::QuickORM::Conflator->new( name => 'NAME', inflate => sub { ... }, defalte => sub { ... } ); Multiple ORM instances for different databases and schemas db develop => sub { ... }; db staging => sub { ... }; db production => sub { ... }; my $app1 = schema app1 => { ... }; my $app2 = schema app2 { ... }; orm app1_dev => sub { db 'develop'; schema 'app1'; }; orm app2_prod => sub { db 'production'; schema 'app2'; }; orm both_stage => sub { db 'staging'; # Builds a new schema object, does not modify either original schema $app1->merge($app2); }; "Select" object that is very similar to DBIx::Class's ResultSet ResultSet was a good idea, regardless of your opinion on DBIx::Class. The DBIx::QuickORM::Select objects implement most of the same things. my $sel = $orm->select('TABLE/SOURCE', \%where) my $sel = $orm->select('TABLE/SOURCE', \%where, $order_by) my $sel = $orm->select('TABLE/SOURCE', where => $where, order_by => $order_by, ... ); $sel = $sel->and(\%where); my @rows = $sel->all; my $row = $sel->next; my $total = $sel->count; Find exactly 1 row # Throws an exception if multiple rows are found. my $row = $orm->find($source, \%where); Fetch just the data, no row object (bypasses cache) my $data_hashref = $orm->fetch($source, \%where); Uses SQL::Abstract under the hood for familiar query syntax See SQL::Abstract. Built in support for transactions and nested transactions (savepoints) See DBIx::QuickORM::Transaction and "TRANSACTIONS" in DBIx::QuickORM::ORM for additional details. $orm->txn_do(sub { ... }); Void context will commit if there are no exceptions. It will rollback the transaction and re-throw the exception if it encounters one. $res = $orm->txn_do(sub { ... }); Scalar context. On success it will commit and return whatever the sub returns, or the number 1 if the sub returns nothing, or anything falsy. If you want to return a false value you must send it as a ref, or use the list context form. If an exception is thrown by the block then the transaction will be rolled back and $res will be false. ($ok, $res_or_err) = $orm->txn_do(sub { ... }); List context. On success it will commit and return (1, $result). If an exception occurs in the block then the transaction will be rolled back, $ok will be 0, and $ret_or_err will contain the exception. $orm->txn_do(sub { my $txn = shift; # Nested! my ($ok, $res_or_err) = $orm->txn_do(sub { ... }); if ($ok) { $txn->commit } else { $txn->rollback }; # Automatic rollback if an exception is thrown, or if commit is not called }); # Commit if no exception is thrown, rollback on exception $orm->txn_do(sub { ... }); Or manually: my $txn = $orm->start_txn; if ($ok) { $txn->commit } else { $txn->rollback }; # Force a rollback unless commit or rollback were called: $txn = undef; Caching system Each DBIx::QuickORM::ORM instance has its own cache object. Default cache: Naive, only 1 copy of any row in active memory DBIx::QuickORM::Cache::Naive is a basic caching system that insures you only have 1 copy of any specific row at any given time (assuming it has a primary key, no cahcing is attempted for rows with no primary key). Note: If you have multiple ORMs connecting to the same db, they do not share a cache and you can end up with the same row in memory twice with 2 different references. 'None' cache option to skip caching, every find/select gets a new row instance You can also choose to use DBIx::QuickORM::Cache::None which is basically a no-op for everything meaning there is no cache, every time you get an object from the db it is a new copy. Write your own cache if you do not like these Write your own based on the DBIx::QuickORM::Cache base class. Multiple databases supported: Database interactions are defined by DBIx::QuickORM::DB subclasses. The parent class provides a lot of generic functionality that is fairly universal. But the subclasses allow you to specify if a DB does or does not support things, how to translate type names from other DBs, etc. PostgreSQL Tells the ORM what features are supported by PostgreSQL, and how to access them. See DBIx::QuickORM::DB::PostgreSQL, which uses DBD::Pg under the hood. MySQL (Generic) Tells the ORM what features are supported by any generic MySQL, and how to access them. This FULLY supports both DBD::mysql and DBD::MariaDB for connections, pick whichever you prefer, the DBIx::QuickORM::DB::MySQL class is aware of the differences and will alter behavior accordingly. MySQL (Percona) Tells the ORM what features are supported by Percona MySQL, and how to access them. This FULLY supports both DBD::mysql and DBD::MariaDB for connections, pick whichever you prefer, the DBIx::QuickORM::DB::MySQL and DBIx::QuickORM::DB::Percona classes are aware of the differences and will alter behavior accordingly. MariaDB Tells the ORM what features are supported by MariaDB, and how to access them. This is essentially MySQL + the extra features MariaDB supports. This FULLY supports both DBD::mysql and DBD::MariaDB for connections, pick whichever you prefer, the DBIx::QuickORM::DB::MySQL and DBIx::QuickORM::DB::MariaDB classes are aware of the differences and will alter behavior accordingly. SQLite Tells the ORM what features are supported by SQLite, and how to access them. See DBIx::QuickORM::DB::SQLite, which uses DBD::SQLite under the hood. Write your own orm <-> db link class Take a look at DBIx::QuickORM::DB to see what you need to implement. Temporary tables and views Each ORM object DBIx::QuickORM::ORM has the static schema it is built with, but it also has a second 'connection' schema. Using this second schema you can define temporary views and tables (on supported databases). $orm->create_temp_table(...); $orm->create_temp_view(...); See the DBIx::QuickORM::ORM documentation for more details. Highly functional Row class, ability to use custom ones DBIx::QuickORM::Row is the base class for rows, and the default one used for rows that are returned. It provides several methods for getting/setting columns, including directly accessing stored, pending, and inflated values. It also has methods for finding and fetching relations. This row class does not provide any per-column accessors. For those you need one of the following: DBIx::QuickORM::Row::AutoAccessors This row class uses AUTOLOAD to generate accessors based on column names on the fly. So my $val = $row->foo is the same as $row->column('foo'). It also generates accessors for relationships on the fly. Create your own row subclasses and tell the schema to use them. table foo => sub { row_class 'My::Row::Class::Foo'; }; Create a class that defines the table and generates a table specific row class My::Table::Foo.pm: package My::Table::Foo use DBIx::QuickORM ':TABLE_CLASS'; use DBIx::QuickORM::MetaTable foo => sub { column id => ...; column foo => ...; # Declarative keywords are removed after this scope ends. }; # There are now accessors for all the columns and relationships. sub whatever_methods_you_want { my $self = shift; ... } Elsware... orm MyORM => sub { table My::Table::Foo; # or to load a bunch: tables 'My::Table'; # Loads all My::Table::* tables }; Relation mapping and pre-fetching TODO: Fill this in. Plugin system There are a lot of hooks, essentially a plugin is either a codered called for all hooks (with params telling you about the hook, or they are classes/objects that define the 'qorm_plugin_action()" method or that consume the DBIx::QuickORM::Role::Plugin role. plugin sub { ... }; # On the fly plugin writing plugin Some::Plugin; # Use a plugin class (does not have or need a new method) plugin Other::Plugin->new(...); Plugin that needs to be blessed Bigger example: plugin sub { my $self = shift; my %params = @_; my $hook = $params{hook}; my $return_ref = $params{return_ref}; ... # if the hook expects you to return a value, instead of modifying a ref # in %params, then the return_ref will have a scalar reference to set. ${return_ref} = $out if defined($return_ref); }; Define custom plugin hooks in your custom tools: plugin_hook NAME => \%params; # Any/All plugins can take action here. Current hooks auto_conflate => (data_type => $dtype, sql_type => $stype, column => $col, table => $table) Use this to automatically inject conflation when auto-generating perl-side schema from a populated db. post_build => (build_params => \%params, built => $out, built_ref => \$out) Called after building an object (ORM, Schema, DB, etc). pre_build => (build_params => \%params) Called before building an object (ORM, Schema, DB, etc). relation_name => (default_name => $alias, table => $table, table_name => $tname, fk => $fk) use to rename relations when auto-generating perl-side schema from a populated db. sql_spec => (column => $col, table => $table, sql_spec => $spec) Opportunity to modify the DBIx::QuickORM::SQLSpec data for a row. sql_spec => (table => $table, sql_spec => sql_spec()) Opportunity to modify the DBIx::QuickORM::SQLSpec data for a table. Ability to customize relationship names when auto-generating perl schema from SQL schema TODO: Fill this in. Does not use Moose under the hood (light weight) Most objects in DBIx::QuickORM use Object::HashBase which is what Test2 uses under the hood. Object::HashBase is very lightweight and performant. For roles DBIx::QuickORM uses Role::Tiny. Using Data::Dumper on a row does not dump all the ORM internals DBIx::QuickORM::Row objects need access to the source, and to the orm. If a reference to these was simply put into the row objects hashref then Data::Dumper is going to work hard to absolutely fill your scrollback with useless info every time you dump your row. DBIx::Class suffers from this issue. For DBIx::QuickORM the source is an DBIx::QuickORM::Source object. And it is put into the $row->{source} hash key. But first it is masked using DBIx::QuickORM::Util::Mask so that when dumped with Data::Dumper you see this: bless( { 'source' => bless( [ 'DBIx::QuickORM::Source=HASH(0x59d72c1c33c8)', sub { "DUMMY" } ], 'DBIx::QuickORM::Util::Mask' ), ... } }, 'DBIx::QuickORM::Row' ); All methods that are valid on DBIx::QuickORM::Source can be called on the masked form and they will be delegated to the masked object. This + the DateTime conflator mean that rows from DBIx::QuickORM can be dumped by Data::Dumper without wiping out your scrollback buffer. DECLARATIVE INTERFACE TODO - Fill this in. SOURCE The source code repository for DBIx-QuickORM can be found at http://github.com/exodist/DBIx-QuickORM/. MAINTAINERS Chad Granum AUTHORS Chad Granum COPYRIGHT Copyright Chad Granum . This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See http://dev.perl.org/licenses/