DBAL
This guide provides a comprehensive overview of how to use Doctrine DBAL (Database Abstraction Layer) with the ODY framework for database operations.
Overview
The ODY Database module provides a powerful DBAL implementation that:
- Utilizes Swoole coroutines for asynchronous database operations
- Implements connection pooling for efficient resource utilization
- Automatically manages connection lifecycle within coroutines
- Supports multiple database environments and configurations
Installation
composer require ody/database doctrine/dbal
Configuration
Database Configuration
In your config/database.php
:
return [
'charset' => 'utf8mb4',
'environments' => [
'local' => [
'adapter' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', 3306),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', 'root'),
'db_name' => env('DB_DATABASE', 'ody'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_general_ci',
'prefix' => '',
'options' => [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_CASE,
PDO::CASE_LOWER,
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
PDO::MYSQL_ATTR_DIRECT_QUERY => false,
],
'pool' => [
'enabled' => env('DB_ENABLE_POOL', false),
'connections_per_worker' => 128,
'minimum_idle' => 128,
'idle_timeout' => 60.0,
'max_lifetime' => 3600.0,
'borrowing_timeout' => 2,
'returning_timeout' => 1,
'leak_detection_threshold' => 10.0,
'keep_alive_check_interval' => 60.0,
]
],
'production' => [
// Production-specific configuration...
'pool' => [
'enabled' => env('DB_ENABLE_POOL', false),
'connections_per_worker' => 64,
'minimum_idle' => 32,
// Other pool settings...
]
],
],
'default_environment' => 'local',
// Other configuration settings...
];
Service Provider Registration
Register the required service providers in your application’s providers configuration:
'providers' => [
// Other providers...
Ody\DB\Providers\DatabaseServiceProvider::class,
Ody\DB\Doctrine\Providers\DBALServiceProvider::class,
],
Basic Usage
Establishing a Connection
The DBAL implementation manages connections automatically, but you can access the connection directly using:
use Doctrine\DBAL\Connection;
class YourService
{
private Connection $connection;
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
// Your methods using the connection...
}
Executing Queries
// Simple queries
$users = $this->connection->fetchAllAssociative('SELECT * FROM users WHERE active = ?', [1]);
// Single value queries
$count = $this->connection->fetchOne('SELECT COUNT(*) FROM users');
// Single row
$user = $this->connection->fetchAssociative('SELECT * FROM users WHERE id = ?', [42]);
// Executed statements (for INSERT, UPDATE, DELETE)
$affected = $this->connection->executeStatement(
'UPDATE users SET status = ? WHERE last_login < ?',
['inactive', new \DateTime('-30 days')]
);
Using Query Builder
DBAL provides a powerful query builder for constructing complex queries:
$queryBuilder = $this->connection->createQueryBuilder();
$results = $queryBuilder
->select('u.*')
->from('users', 'u')
->leftJoin('u', 'profiles', 'p', 'u.id = p.user_id')
->where('u.status = :status')
->andWhere('u.created_at > :date')
->setParameter('status', 'active')
->setParameter('date', new \DateTime('-7 days'))
->orderBy('u.name', 'ASC')
->setMaxResults(10)
->executeQuery()
->fetchAllAssociative();
Working with Transactions
// Method 1: Using transactional callback
$this->connection->transactional(function (Connection $conn) {
$conn->executeStatement('INSERT INTO orders (customer_id, total) VALUES (?, ?)', [123, 99.99]);
$orderId = $conn->lastInsertId();
$conn->executeStatement(
'INSERT INTO order_items (order_id, product_id, quantity) VALUES (?, ?, ?)',
[$orderId, 456, 1]
);
// Return value is passed through from the closure
return $orderId;
});
// Method 2: Manual transaction management
$this->connection->beginTransaction();
try {
// Execute queries...
$this->connection->commit();
} catch (\Exception $e) {
$this->connection->rollBack();
throw $e;
}
Advanced Usage
Schema Management
// Get the schema manager
$schemaManager = $this->connection->createSchemaManager();
// List tables
$tables = $schemaManager->listTableNames();
// Check if table exists
$tableExists = $schemaManager->tablesExist(['users']);
// Get table details
$table = $schemaManager->introspectTable('users');
// Create a new table
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Types;
$table = new Table('logs');
$table->addColumn('id', Types::INTEGER, ['autoincrement' => true]);
$table->addColumn('message', Types::TEXT);
$table->addColumn('level', Types::STRING, ['length' => 10]);
$table->addColumn('created_at', Types::DATETIME_IMMUTABLE);
$table->setPrimaryKey(['id']);
$table->addIndex(['level'], 'idx_log_level');
$schemaManager->createTable($table);
Prepared Statements
For queries that are executed repeatedly:
$stmt = $this->connection->prepare('INSERT INTO items (name, price) VALUES (?, ?)');
foreach ($items as $item) {
$stmt->bindValue(1, $item['name']);
$stmt->bindValue(2, $item['price']);
$stmt->executeStatement();
}
Custom Types
Register and use custom column types:
// In your service provider or bootstrap code
use Doctrine\DBAL\Types\Type;
use Ody\DB\Doctrine\Types\JsonType;
// Register the type if not already registered
if (!Type::hasType('json')) {
Type::addType('json', JsonType::class);
}
// Using the custom type in schema
$table->addColumn('metadata', 'json', ['notnull' => false]);
// When fetching data, the JSON will be automatically converted to PHP arrays
$data = $this->connection->fetchAssociative('SELECT * FROM items WHERE id = ?', [1]);
// $data['metadata'] is now a PHP array
Connection Pooling
The ODY DBAL implementation includes an advanced connection pooling system specifically designed for Swoole’s coroutine-based environment.
Configuring the Pool
'pool' => [
'enabled' => true, // Enable/disable connection pooling
'connections_per_worker' => 64, // Maximum connections per Swoole worker
'minimum_idle' => 32, // Minimum idle connections to maintain
'idle_timeout' => 60.0, // Seconds before idle connection is closed
'max_lifetime' => 3600.0, // Maximum lifetime of a connection
'borrowing_timeout' => 2, // Seconds to wait when borrowing a connection
'returning_timeout' => 1, // Seconds to wait when returning a connection
'leak_detection_threshold' => 10.0, // Seconds to wait before considering a connection leaked
'keep_alive_check_interval' => 60.0, // Keep-alive check interval (should be less than MySQL wait_timeout)
]
How the Pool Works
- Connections are automatically created and managed by the
ConnectionManager
- Each Swoole worker maintains its own connection pool
- When a new query is executed, a connection is borrowed from the pool
- When the coroutine finishes, the connection is automatically returned to the pool
- Idle connections are periodically checked and closed if they exceed the idle timeout
- Connections that aren’t returned to the pool are detected and logged
Performance Tips
Batch Operations
For large batch operations:
$this->connection->beginTransaction();
try {
$stmt = $this->connection->prepare('INSERT INTO logs (message, level) VALUES (?, ?)');
foreach ($logs as $i => $log) {
$stmt->bindValue(1, $log['message']);
$stmt->bindValue(2, $log['level']);
$stmt->executeStatement();
// Commit in batches to prevent memory issues
if ($i > 0 && $i % 1000 === 0) {
$this->connection->commit();
$this->connection->beginTransaction();
}
}
$this->connection->commit();
} catch (\Exception $e) {
$this->connection->rollBack();
throw $e;
}
Optimizing Queries
- Use specific columns instead of
SELECT *
- Add proper indexes for frequently queried columns
- Use parameter binding instead of string concatenation
- Limit result sets when possible
Working with Multiple Database Connections
To work with multiple database connections:
use Doctrine\DBAL\Connection;
use Psr\Container\ContainerInterface;
class MultiDatabaseService
{
private ContainerInterface $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function doSomethingWithMultipleDatabases()
{
// Get the default connection
$defaultConnection = $this->container->get(Connection::class);
// Get a specific connection
$factory = $this->container->get('dbal.connection.factory');
$analyticsConnection = $factory('analytics');
// Use both connections
$users = $defaultConnection->fetchAllAssociative('SELECT * FROM users');
$stats = $analyticsConnection->fetchAllAssociative('SELECT * FROM statistics');
// Process data from both sources...
}
}
License
This package is open-sourced software licensed under the MIT license.