← Назад к вопросам

Какие знаешь паттерны для организации слоя работы с БД?

2.2 Middle🔥 231 комментариев
#Архитектура и паттерны#Базы данных и SQL

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Паттерны организации слоя работы с БД в PHP Backend

При разработке бэкенда на PHP организация слоя работы с базой данных — ключевая задача, влияющая на гибкость, тестируемость и устойчивость приложения к изменениям. Использование проверенных паттернов позволяет отделить бизнес-логику от деталей реализации хранения данных.

Основные паттерны

Active Record (Активная запись)

Это один из наиболее прямолинейных и распространенных в ранних PHP фреймворках (например, Laravel Eloquent) подходов. Модель одновременно представляет объект бизнес-логики и отвечает за свои собственные операции с БД.

<?php
// Примерная концепция Active Record
class User extends Model
{
    protected $table = 'users';
    
    public function getOrders()
    {
        return $this->hasMany(Order::class);
    }
    
    // Модель сама знает, как сохранить себя
    public static function create(array $attributes)
    {
        $user = new static($attributes);
        $user->save();
        return $user;
    }
}
  • Принцип: Объект инкапсулирует данные, бизнес-правила и методы для CRUD.
  • Преимущества: Простота и скорость разработки для небольших проектов.
  • Недостатки: Смешивание ответственности нарушает принцип единой ответственности (SRP). Модель становится тесно связанной с структурой БД, что затрудняет тестирование бизнес-логики без БД и изменение источника данных.

Data Mapper (Преобразователь данных)

Это более сложный и гибкий паттерн, который полностью разделяет бизнес-объект (Domain Model) и механизм его сохранения. Преобразователь отвечает за трансформацию данных между объектом домена и таблицей БД.

<?php
// Пример доменного объекта (не знает о БД)
class User
{
    private $id;
    private $name;
    private $email;
    
    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }
    
    public function getId(): ?int
    {
        return $this->id;
    }
    
    public function getName(): string
    {
        return $this->name;
    }
}

// Преобразователь (Mapper) отвечает за сохранение/извлечение
class UserMapper
{
    private $pdo;
    
    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }
    
    public function save(User $user): void
    {
        if ($user->getId() === null) {
            // INSERT
            $stmt = $this->pdo->prepare("INSERT INTO users (name, email) VALUES (:name, :email)");
            $stmt->execute(['name' => $user->getName(), 'email' => $user->getEmail()]);
            $user->id = $this->pdo->lastInsertId();
        } else {
            // UPDATE
            $stmt = $this->pdo->prepare("UPDATE users SET name = :name, email = :email WHERE id = :id");
            $stmt->execute(['id' => $user->getId(), 'name' => $user->getName(), 'email' => $user->getEmail()]);
        }
    }
    
    public function findById(int $id): ?User
    {
        $stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = :id");
        $stmt->execute(['id' => $id]);
        $data = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$data) {
            return null;
        }
        
        $user = new User($data['name'], $data['email']);
        $user->id = $data['id'];
        return $user;
    }
}
  • Принцип: Чистый доменный объект и отдельный класс-преобразователь, который управляет персистентностью.
  • Преимущества:
    * Полное **разделение ответственности**. Бизнес-логика не зависит от БД.
    * Доменные модели можно легко тестировать (unit tests) без подключения к БД.
    * Возможность использовать сложные доменные структуры (агрегаты, коллекции), не совпадающие с табличной структурой.
  • Недостатки: Большая сложность реализации, большее количество кода.

Repository (Репозиторий)

Это паттерн, который абстрагирует коллекцию объектов домена и предоставляет интерфейс для их получения и сохранения, часто используя механизмы Data Mapper или Query Builder внутри. Репозиторий работает как своего рода "память объектов".

<?php
// Интерфейс репозитория (контракт)
interface UserRepositoryInterface
{
    public function findById(int $id): ?User;
    public function findByEmail(string $email): ?User;
    public function save(User $user): void;
    public function getAllActive(): array;
}

// Конкретная реализация с использованием, например, Doctrine DBAL или ORM
class DoctrineUserRepository implements UserRepositoryInterface
{
    private $entityManager;
    
    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }
    
    public function findById(int $id): ?User
    {
        return $this->entityManager->find(User::class, $id);
    }
    
    public function save(User $user): void
    {
        $this->entityManager->persist($user);
        $this->entityManager->flush();
    }
}
  • Принцип: Репозиторий предоставляет коллекционный интерфейс для работы с доменными объектами, скрывая детали запросов и персистентности.
  • Преимущества:
    * Чистый, тестируемый интерфейс для сервисов бизнес-логики.
    * Возможность легко подменить реализацию (например, на mock-объект для тестов).
    * Централизация сложных запросов (например, `getAllActive()`).
  • Недостатки: Может стать "мусорной корзиной" для всех методов запросов, если не структурировать правильно.

Query Object (Объект запроса)

Это паттерн для инкапсуляции критериев выборки данных в отдельный объект. Особенно полезен для сложных поисковых или фильтрующих операций.

<?php
class ActiveUsersWithOrdersQuery
{
    private $minOrderCount;
    private $registrationDateFrom;
    
    public function setMinOrderCount(int $count): void
    {
        $this->minOrderCount = $count;
    }
    
    public function setRegistrationDateFrom(DateTime $date): void
    {
        $this->registrationDateFrom = $date;
    }
    
    public function getSQL(): string
    {
        // Построение безопасного SQL на основе параметров
        $sql = "SELECT u.* FROM users u 
                JOIN orders o ON u.id = o.user_id 
                WHERE u.is_active = 1";
        
        if ($this->minOrderCount !== null) {
            $sql .= " AND (SELECT COUNT(*) FROM orders o2 WHERE o2.user_id = u.id) >= :minOrderCount";
        }
        
        if ($this->registrationDateFrom !== null) {
            $sql .= " AND u.created_at >= :registrationDateFrom";
        }
        
        return $sql;
    }
    
    public function getParameters(): array
    {
        // Возвращает параметры для безопасного выполнения
        $params = [];
        if ($this->minOrderCount !== null) {
            $params['minOrderCount'] = $this->minOrderCount;
        }
        if ($this->registrationDateFrom !== null) {
            $params['registrationDateFrom'] = $this->registrationDateFrom->format('Y-m-d');
        }
        return $params;
    }
}
  • Принцип: Изоляция логики построения запроса в отдельный, переиспользуемый и тестируемый объект.
  • Преимущества: Улучшает читаемость и переиспользование сложных запросов, избегая их размазывания по репозиториям или сервисам.

Практическое применение и выбор паттерна

  • Для быстрых и простых проектов (CRUD-приложений) часто выбирают Active Record (Eloquent, Yii AR) — это эффективно и требует меньше кода.
  • Для комплексных доменных моделей в крупных приложениях (DDD) почти всегда используют комбинацию Data Mapper + Repository. Современные ORM, такие как Doctrine, реализуют именно эту пару паттернов.
  • Query Object хорошо дополняет репозитории, когда нужно инкапсулировать особенно сложную логику выборки.

Ключевой критерий выбора — степень связности бизнес-логики и слоя данных. Если бизнес-правила сложные и должны существовать без зависимости от БД, то Data Mapper и Repository необходимы. Если приложение по сути является интерфейсом к таблицам БД, Active Record может быть оптимальным выбором.

В современной практике PHP также часто используют гибридные подходы, например, Eloquent модели (Active Record) внутри сервисов, но с добавлением Repository-интерфейсов для абстрагирования в бизнес-логике, что дает баланс между простотой и тестируемостью.