Что такое паттерн Active Record?

«Что такое паттерн Active Record?» — вопрос из категории Паттерны, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Active Record — это архитектурный паттерн, в котором объект инкапсулирует строку таблицы базы данных или представление, объединяя данные и поведение для работы с ними. Каждый экземпляр класса соответствует строке в таблице БД, а сам класс предоставляет методы для CRUD-операций.

Ключевые характеристики:

  1. Объект = запись БД — каждый экземпляр представляет одну строку таблицы
  2. Инкапсуляция данных и поведения — объект содержит как данные, так и методы для их сохранения/загрузки
  3. Прямое отображение — структура класса обычно mirrors структуру таблицы

Реализация в PHP (на примере Laravel Eloquent):

Базовое использование:

// Модель User соответствует таблице 'users'
class User extends IlluminateDatabaseEloquentModel
{
    // Необязательно - Eloquent сам определит по имени класса
    protected $table = 'users';

    protected $fillable = ['name', 'email', 'password'];

    protected $casts = [
        'is_active' => 'boolean',
        'settings' => 'array'
    ];
}

// CRUD операции
// CREATE
$user = new User();
$user->name = 'John Doe';
$user->email = 'john@example.com';
$user->save(); // Выполняет INSERT

// READ
$user = User::find(1); // SELECT * FROM users WHERE id = 1
$activeUsers = User::where('is_active', true)->get();

// UPDATE
$user = User::find(1);
$user->name = 'Jane Doe';
$user->save(); // Выполняет UPDATE

// DELETE
$user = User::find(1);
$user->delete(); // DELETE FROM users WHERE id = 1

Отношения (Relationships):

class User extends Model
{
    // Один ко многим
    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    // Многие ко многим
    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
}

// Использование отношений
$user = User::find(1);
$posts = $user->posts; // Автоматическая загрузка связанных постов
$latestPost = $user->posts()->latest()->first();

События (Events) и Observers:

class User extends Model
{
    protected static function booted()
    {
        static::creating(function ($user) {
            $user->api_token = Str::random(60);
        });

        static::created(function ($user) {
            // Отправка welcome email
            Mail::to($user->email)->send(new WelcomeEmail($user));
        });
    }
}

Преимущества Active Record:

1. Простота и скорость разработки:

// Быстрое прототипирование
$user = User::create([
    'name' => 'Test User',
    'email' => 'test@example.com'
]);
// Всего 3 строки для создания и сохранения объекта

2. Интуитивно понятный API:

// Чтение похоже на SQL, но более объектно-ориентированно
$users = User::where('age', '>', 18)
             ->orderBy('created_at', 'desc')
             ->paginate(20);

3. Минимальная конфигурация:

// В Laravel достаточно:
class Product extends Model {}
// Автоматически работает с таблицей 'products'

4. Встроенные функции:

// Валидация, события, скоупы, мягкое удаление из коробки
class Post extends Model
{
    use SoftDeletes;

    protected $dates = ['deleted_at'];

    public function scopePublished($query)
    {
        return $query->where('published', true);
    }
}

// Мягкое удаление
$post->delete(); // Устанавливает deleted_at, не удаляет физически
$post->forceDelete(); // Физическое удаление

Недостатки и критика:

1. Нарушение Single Responsibility Principle:

class User extends Model
{
    // Отвечает и за данные, и за бизнес-логику, и за персистентность
    public function calculateDiscount() { /* бизнес-логика */ }
    public function sendNotification() { /* логика уведомлений */ }
    // + все методы ActiveRecord для работы с БД
}
// Класс становится "God Object"

2. Сложность тестирования:

// Трудно тестировать из-за сильной связи с БД
public function testUserLogic(): void
{
    $user = new User(['name' => 'Test']);
    $user->save(); // Реальная запись в БД - медленный тест

    // Нужны моки для Eloquent или тестовая БД
}

3. Проблемы с производительностью:

// N+1 проблема
$users = User::all();
foreach ($users as $user) {
    echo $user->profile->bio; // Отдельный запрос для каждого пользователя
}

// Решение: eager loading
$users = User::with('profile')->get(); // Всего 2 запроса

4. Связь с конкретной СУБД:

// Код может стать зависимым от возможностей конкретной БД
$users = User::whereRaw('JSON_CONTAINS(settings, ?)', ['"dark_mode":true'])
             ->get();
// Использует MySQL-специфичный JSON_CONTAINS

Альтернативы:

Data Mapper (например, Doctrine):

// Отдельная сущность и маппер
class User
{
    private $id;
    private $name;
    // Только данные и бизнес-логика
}

class UserMapper
{
    public function save(User $user) { /* сохранение в БД */ }
    public function find($id): User { /* загрузка из БД */ }
}
// Более чистая архитектура, но более сложная настройка

Рекомендации по использованию:

  1. Для простых CRUD-приложений — Active Record отлично подходит
  2. Для сложной бизнес-логики — рассмотреть Data Mapper или разделить ответственность
  3. Использовать Repository Pattern для абстракции доступа к данным:
    class UserRepository
    {
    public function findActiveUsers(): Collection
    {
        return User::where('is_active', true)->get();
    }
    }
    // Контроллеры работают с репозиторием, а не напрямую с Eloquent

Популярные реализации в PHP:

  • Laravel Eloquent (наиболее популярная)
  • Yii2 ActiveRecord
  • CakePHP ORM
  • Propel (устаревший)

Active Record идеален для быстрой разработки и проектов средней сложности, но требует осторожности в крупных enterprise-приложениях.