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:

schema_class (required)

The name of the DBIx::Class schema class to use (e.g., 'MyApp::Schema').

workers

Number of worker processes (default: 4).

connect_timeout

Connection timeout in seconds (default: 10).

max_retries

Maximum number of retry attempts for failed operations (default: 3).

Returns

A new DBIx::Class::Async::Schema instance.

Throws
  • Croaks if schema_class is 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
$source_name

The name of the source/moniker (e.g., 'User', 'Order').

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::Schema instance 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 a DROP TABLE statement 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
$source_name

The name of the source/moniker (e.g., 'User', 'Order').

$data

Either:

- Array of hashrefs: [ \%col_data, \%col_data, ... ]

- Column list + rows: [ \@column_list, \@row_values, \@row_values, ... ]

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

# 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
$source_name

Name of the result source (table) to get a result set for.

Returns

A DBIx::Class::Async::ResultSet object.

Throws

Croaks if $source_name is 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
$source_name

Name of the result source (table).

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
$code

Code reference to execute within the transaction. The code receives the schema instance as its first argument.

@args

Additional arguments to pass to the code reference.

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 commit or rollback is received. In a high-concurrency environment, a few unclosed guards could easily exhaust the worker pool, causing the entire application to hang.

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 _prefetched slot 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

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:

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.