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
MAINTAINERS
Chad Granum <exodist@cpan.org>
AUTHORS
Chad Granum <exodist@cpan.org>
COPYRIGHT
Copyright Chad Granum <exodist7@gmail.com>.
This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.