NAME
DBIx::Class::Async::Schema - Asynchronous schema for DBIx::Class::Async
VERSION
Version 0.41
SYNOPSIS
use DBIx::Class::Async::Schema;
# Connect with async options
my $schema = DBIx::Class::Async::Schema->connect(
'dbi:mysql:database=test', # DBI connect string
'username', # Database username
'password', # Database password
{ RaiseError => 1 }, # DBI options
{ # Async options
schema_class => 'MyApp::Schema',
workers => 4,
}
);
# Get a resultset
my $users_rs = $schema->resultset('User');
# Asynchronous operations
$users_rs->search({ active => 1 })->all->then(sub {
my ($active_users) = @_;
foreach my $user (@$active_users) {
say "Active user: " . $user->name;
}
});
# Disconnect when done
$schema->disconnect;
DESCRIPTION
DBIx::Class::Async::Schema provides an asynchronous schema class that mimics the DBIx::Class::Schema API but performs all database operations asynchronously using Future objects.
This class acts as a bridge between the synchronous DBIx::Class API and the asynchronous backend provided by DBIx::Class::Async. It manages connection pooling, result sets, and transaction handling in an asynchronous context.
CONSTRUCTOR
connect
my $schema = DBIx::Class::Async::Schema->connect(
$dsn, # Database DSN
$user, # Database username
$password, # Database password
$db_options, # Hashref of DBI options
$async_options, # Hashref of async options
);
Connects to a database and creates an asynchronous schema instance.
- Parameters
-
The first four parameters are standard DBI connection parameters. The fifth parameter is a hash reference containing asynchronous configuration:
- Returns
-
A new
DBIx::Class::Async::Schemainstance. - Throws
-
Croaks if
schema_classis not provided.Croaks if the schema class cannot be loaded.
Croaks if the async instance cannot be created.
METHODS
class
my $class = $schema->class('User');
# Returns: 'MyApp::Schema::Result::User'
Returns the class name for the given source name or moniker.
- Arguments
- Returns
-
The full class name of the Result class (e.g., 'MyApp::Schema::Result::User').
- Throws
-
Dies if the source doesn't exist.
- Examples
-
# Get the Result class for User my $user_class = $schema->class('User'); # $user_class is now 'MyApp::Schema::Result::User' # Can be used to call class methods my $user_class = $schema->class('User'); my @columns = $user_class->columns; # Or to create new objects directly my $user = $schema->class('User')->new({ name => 'Alice', email => 'alice@example.com', });
clone
my $cloned_schema = $schema->clone;
Creates a clone of the schema with a fresh worker pool.
- Returns
-
A new
DBIx::Class::Async::Schemainstance with fresh connections. - Notes
-
The cloned schema shares no state with the original and has its own worker processes and source cache.
deploy
my $future = $schema->deploy(\%sqlt_args?, $dir?);
$future->on_done(sub {
my $self = shift;
say "Schema deployed successfully.";
});
- Arguments: \%sqlt_args?, $dir?
- Return Value: Future resolving to $self
Asynchronously deploys the database schema.
This is a non-blocking proxy for "deploy" in DBIx::Class::Schema. The actual SQL translation (via SQL::Translator) and DDL execution are performed in a background worker process to prevent the main event loop from stalling.
The optional \%sqlt_args are passed directly to the worker-side deploy method. Common options include:
add_drop_table- Prepends aDROP TABLEstatement for each table.quote_identifiers- Ensures database identifiers are correctly quoted.
On success, the returned Future resolves to the schema object ($self), allowing for easy method chaining. On failure, the Future fails with the error message generated by the worker.
disconnect
$schema->disconnect;
Disconnects all worker processes and cleans up resources.
- Notes
-
This method is called automatically when the schema object is destroyed, but it's good practice to call it explicitly when done with the schema.
populate
# Array of hashrefs format
my $users = $schema->populate('User', [
{ name => 'Alice', email => 'alice@example.com' },
{ name => 'Bob', email => 'bob@example.com' },
])->get;
# Column list + rows format
my $users = $schema->populate('User', [
[qw/ name email /],
['Alice', 'alice@example.com'],
['Bob', 'bob@example.com'],
])->get;
A convenience shortcut to "populate" in DBIx::Class::Async::ResultSet. Creates multiple rows efficiently.
- Arguments
- Returns
-
A Future that resolves to an arrayref of DBIx::Class::Async::Row objects in scalar context, or a list in list context.
- Examples
-
# Array of hashrefs $schema->populate('User', [ { name => 'Alice', email => 'alice@example.com', active => 1 }, { name => 'Bob', email => 'bob@example.com', active => 1 }, { name => 'Carol', email => 'carol@example.com', active => 0 }, ])->then(sub { my ($users) = @_; say "Created " . scalar(@$users) . " users"; }); # Column list + rows (more efficient for many rows) $schema->populate('User', [ [qw/ name email active /], ['Alice', 'alice@example.com', 1], ['Bob', 'bob@example.com', 1], ['Carol', 'carol@example.com', 0], ])->then(sub { my ($users) = @_; foreach my $user (@$users) { say "Created: " . $user->name; } });
register_class
$schema->register_class($source_name => $result_class);
Registers a new Result class with the schema. This is a convenience method that loads the Result class, gets its result source, and registers it.
Arguments
$source_name- String name for the source (e.g., 'User', 'Product')$result_class- Fully qualified Result class name
# Register a Result class
$schema->register_class('Product' => 'MyApp::Schema::Result::Product');
# Now you can use it
my $rs = $schema->resultset('Product');
This method will load the Result class if it hasn't been loaded yet.
register_source
$schema->register_source($source_name => $source);
Registers a new result source with the schema. This is used to add new tables or views to the schema at runtime.
Arguments
$source_name- String name for the source (e.g., 'User', 'Product')$source- A DBIx::Class::ResultSource instance
# Define a new Result class
package MyApp::Schema::Result::Product;
use base 'DBIx::Class::Core';
__PACKAGE__->table('products');
__PACKAGE__->add_columns(
id => { data_type => 'integer', is_auto_increment => 1 },
name => { data_type => 'text' },
);
__PACKAGE__->set_primary_key('id');
# Register it with the schema
$schema->register_source('Product',
MyApp::Schema::Result::Product->result_source_instance
);
resultset
my $rs = $schema->resultset('User');
Returns a result set for the specified source/table.
- Parameters
- Returns
-
A DBIx::Class::Async::ResultSet object.
- Throws
-
Croaks if
$source_nameis not provided.
schema_version
my $version = $schema->schema_version;
Returns the normalised version string of the underlying DBIx::Class::Schema class.
set_default_context
$schema->set_default_context;
Sets the default context for the schema.
- Returns
-
The schema object itself (for chaining).
- Notes
-
This is a no-op method provided for compatibility with DBIx::Class.
source
my $source = $schema->source('User');
Returns the result source object for the specified source/table.
- Parameters
- Returns
-
A DBIx::Class::ResultSource object.
- Notes
-
Sources are cached internally after first lookup to avoid repeated database connections.
sources
my @source_names = $schema->sources;
Returns a list of all available source/table names.
- Returns
-
Array of source names (strings).
- Notes
-
This method creates a temporary synchronous connection to the database to fetch the source list.
storage
my $storage = $schema->storage;
Returns a storage object for compatibility with DBIx::Class.
- Returns
-
A DBIx::Class::Async::Storage object.
- Notes
-
This storage object does not provide direct database handle access since operations are performed asynchronously by worker processes.
txn_batch
my $result = await $schema->txn_batch(@operations);
A proxy method for "txn_batch" in DBIx::Class::Async. Executes a series of database operations (create, update, delete, or raw SQL) atomically within a single background transaction.
txn_do
$schema->txn_do(sub {
my $txn_schema = shift;
# Perform async operations within transaction
return $txn_schema->resultset('User')->create({
name => 'Alice',
email => 'alice@example.com',
});
})->then(sub {
my ($result) = @_;
# Transaction committed successfully
})->catch(sub {
my ($error) = @_;
# Transaction rolled back
});
Executes a code reference within a database transaction.
- Parameters
- Returns
-
A Future that resolves to the return value of the code reference if the transaction commits, or rejects with an error if the transaction rolls back.
- Throws
-
Croaks if the first argument is not a code reference.
unregister_source
$schema->unregister_source($source_name);
Arguments: $source_name
Removes the specified DBIx::Class::ResultSource from the schema class. This is useful in test suites to ensure a clean state between tests.
AUTOLOAD
The schema uses AUTOLOAD to delegate unknown methods to the underlying DBIx::Class::Async instance. This allows direct access to async methods like search, find, create, etc., without going through a resultset.
Example:
# These are equivalent:
$schema->find('User', 123);
$schema->resultset('User')->find(123);
Methods that are not found in the schema or the async instance will throw an error.
DESTROY
The schema's destructor automatically calls disconnect to clean up worker processes and other resources.
DESIGN ARCHITECTURE
This module acts as an asynchronous proxy for DBIx::Class::Schema. While data-retrieval methods (like search, create, and find) return Future objects and execute in worker pools, metadata management methods (like unregister_source and schema_version) are delegated directly to the underlying synchronous schema class to ensure metadata consistency across processes.
ARCHITECTURAL NOTE
Removal of txn_scope_guard
In DBIx::Class::Async, the traditional txn_scope_guard pattern has been intentionally removed. While this pattern is a staple of synchronous DBIx::Class development, it is fundamentally incompatible with an asynchronous, worker-pool architecture.
The following analysis explains the technical limitations that led to this decision.
1. The Scope vs. Execution Race Condition
In a synchronous environment, a scope guard works because the program pauses until every line inside the block completes. The DESTROY method (which triggers an automatic rollback) only fires after all work is done.
In an asynchronous environment, the block often finishes execution and destroys the guard while the database requests are still in flight.
{
my $guard = $schema->txn_scope_guard;
$schema->resultset('User')->create({ name => 'Bob' });
# Request is sent to the worker; a Future is returned immediately.
}
# 1. The block ends here.
# 2. $guard is destroyed, triggering an immediate ROLLBACK command.
# 3. The ROLLBACK may arrive at the worker before the 'create' is processed.
This results in a "Silent Failure" where the application receives a success notification from the Future, but the data is never committed to the disk.
2. Worker Affinity and Statelessness
DBIx::Class::Async utilises a pool of background worker processes to achieve concurrency. Transactions are inherently "stateful" - a database handle must remain assigned to a specific transaction until it is finished.
The Affinity Problem: Without pinning a user session to a specific worker, Operation A might go to Worker 1, while Operation B goes to Worker 2. Worker 2 has no context regarding the transaction started on Worker 1.
Worker Starvation: To support a Scope Guard, a worker would have to "lock" itself to a single caller and refuse all other work until a
commitorrollbackis received. In a high-concurrency environment, a few unclosed guards could easily exhaust the worker pool, causing the entire application to hang.
3. The Recommended Alternative: txn_batch
To provide the same atomicity and safety as a transaction guard without the architectural risks, use the "txn_batch" method.
txn_batch packages all operations into a single atomic message sent to a single worker. The worker opens the transaction, executes all queries, and commits the results before returning the handle to the pool.
4. Comparison at a Glance
Feature | txn_scope_guard | txn_batch
-------------------|-------------------|------------------
Execution | Non-deterministic | Atomic / Single-hop
Worker Logic | Stateful (Risky) | Stateless (Safe)
Cleanup | Perl DESTROY | Internal Worker Logic
Race Conditions | High Risk | None
PERFORMANCE OPTIMISATION
Resolving the N+1 Query Problem
One of the most common performance bottlenecks in ORMs is the "N+1" query pattern. This occurs when you fetch a set of rows and then loop through them to fetch a related row for each member of the set.
In an asynchronous environment, this is particularly costly due to the overhead of message passing between the main process and the database worker pool.
DBIx::Class::Async::Schema resolves this by supporting the standard DBIx::Class prefetch attribute.
The Slow Way (N+1 Pattern)
In this example, if there are 50 orders, this code will perform 51 database round-trips.
my $orders = await $async_schema->resultset('Order')->all;
foreach my $order (@$orders) {
# Each call here triggers a NEW asynchronous find() query to a worker
my $user = await $order->user;
say "Order for: " . $user->name;
}
The Optimised Way (Eager Loading)
By using prefetch, you instruct the worker to perform a JOIN in the background. The related data is serialised and sent back in the primary payload. The library then automatically hydrates the nested data into blessed Row objects.
# Only ONE database round-trip for any number of orders
my $orders = await $async_schema->resultset('Order')->search(
{},
{ prefetch => 'user' }
);
foreach my $order (@$orders) {
# This returns a resolved Future immediately from the internal cache.
# No extra SQL or worker communication is required.
my $user = await $order->user;
say "Order for: " . $user->name;
}
Complex Prefetching
The hydration logic is recursive. You can prefetch multiple levels of relationships or multiple independent relationships simultaneously.
my $rs = $async_schema->resultset('Order')->search({}, {
prefetch => [
{ user => 'profile' }, # Nested: Order -> User -> Profile
'status_logs' # Direct: Order -> Logs
]
});
my $orders = await $rs->all;
# All the following are available instantly:
my $user = await $orders->[0]->user;
my $profile = await $user->profile;
Implementation Details
Automation: Prefetched data is automatically cached in an internal
_prefetchedslot within the DBIx::Class::Async::Row object during inflation.Transparency: The relationship accessors generated by DBIx::Class::Async::Row transparently check this cache before attempting a lazy-load via the worker pool.
Efficiency: By collapsing multiple queries into one, you significantly reduce the latency introduced by inter-process communication (IPC) and database contention.
SEE ALSO
DBIx::Class::Async - Core asynchronous DBIx::Class implementation
DBIx::Class::Async::ResultSet - Asynchronous result sets
DBIx::Class::Async::Row - Asynchronous row objects
DBIx::Class::Async::Storage - Storage compatibility layer
DBIx::Class::Schema - Standard DBIx::Class schema
Future - Asynchronous programming abstraction
AUTHOR
Mohammad Sajid Anwar, <mohammad.anwar at yahoo.com>
REPOSITORY
https://github.com/manwar/DBIx-Class-Async
BUGS
Please report any bugs or feature requests through the web interface at https://github.com/manwar/DBIx-Class-Async/issues. I will be notified and then you'll automatically be notified of progress on your bug as I make changes.
SUPPORT
You can find documentation for this module with the perldoc command.
perldoc DBIx::Class::Async::Schema
You can also look for information at:
BUG Report
CPAN Ratings
Search MetaCPAN
LICENSE AND COPYRIGHT
Copyright (C) 2026 Mohammad Sajid Anwar.
This program is free software; you can redistribute it and / or modify it under the terms of the the Artistic License (2.0). You may obtain a copy of the full license at:
http://www.perlfoundation.org/artistic_license_2_0
Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License.By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license.
If your Modified Version has been derived from a Modified Version made by someone other than you,you are nevertheless required to ensure that your Modified Version complies with the requirements of this license.
This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder.
This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement,then this Artistic License to you shall terminate on the date that such litigation is filed.
Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.