Pular para o conteúdo principal

Active Record

Pattern Overview

Active Record is an implementation of Martin Fowler's Active Record pattern:

"An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data."

— Martin Fowler, Patterns of Enterprise Application Architecture

Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. It facilitates the creation and use of business objects whose data requires persistent storage to a database.

Key Characteristics:

  • Each Active Record instance represents a single row in the database
  • The object carries both data and behavior
  • Provides methods like save(), delete(), get(), all(), etc.
  • Database operations are directly available on the domain object

When to use Active Record vs Repository/Data Mapper:

AspectActive RecordRepository + Data Mapper
ComplexitySimple, straightforwardMore complex, more layers
Best forSimple domains, CRUD-heavy appsComplex domain logic, DDD
CouplingDomain objects know about databaseDomain objects are pure
Learning curveLowerHigher
TestabilityHarder (objects tied to DB)Easier (dependency injection)

💡 Tip: For simple applications or prototypes, Active Record is perfect. For complex domain logic or when following Domain-Driven Design principles, prefer Repository + Data Mapper.

How to Use Active Record

  1. Create a model and add the annotations to the class and properties ( see Getting Started with Models and Attributes)
  2. Add the ActiveRecord trait to your model
  3. Initialize the Active Record with the initialize static method (need to do only once)

Example

#[TableAttribute(tableName: 'my_table')]
class MyClass
{
// Add the ActiveRecord trait to enable the Active Record
use ActiveRecord;

#[FieldAttribute(primaryKey: true)]
public ?int $id;

#[FieldAttribute(fieldName: "some_property")]
public ?int $someProperty;
}

If you have more than one database connection, you can define a default database connection:

// Set a default DBDriver
ORM::defaultDriver($dbDriver);

or call the initialize method with the database connection:

// Initialize the Active Record with a specific DBDriver
MyClass::initialize($dbDriver);

Using the Active Record

Once properly configured, you can use the Active Record pattern for database operations:

Insert a New Record

// Create a new instance
$myClass = MyClass::new();
$myClass->someProperty = 123;
$myClass->save();

// Or create with initial values
$myClass = MyClass::new(['someProperty' => 123]);
$myClass->save();

Retrieve a Record

$myClass = MyClass::get(1);
$myClass->someProperty = 456;
$myClass->save();

Query with Fluent API

The new fluent query API provides a more intuitive way to build and execute queries:

// Get first record matching criteria
$myClass = MyClass::where('someProperty = :value', ['value' => 123])
->first();

// Get first record or throw exception if not found
$myClass = MyClass::where('someProperty = :value', ['value' => 123])
->firstOrFail();

// Check if records exist
if (MyClass::where('someProperty > :min', ['min' => 100])->exists()) {
echo "Records found!";
}

// Get all matching records as array
$myClassList = MyClass::where('someProperty = :value', ['value' => 123])
->orderBy(['id DESC'])
->toArray();

// Chain multiple conditions
$myClass = MyClass::where('someProperty > :min', ['min' => 100])
->where('someProperty < :max', ['max' => 200])
->orderBy(['someProperty'])
->first();

// Build query step by step
$query = MyClass::newQuery()
->where('someProperty = :value', ['value' => 123])
->orderBy(['id DESC'])
->limit(0, 10);

$results = $query->toArray();

Complex Filtering (Legacy)

$myClassList = MyClass::filter((new IteratorFilter())->and('someProperty', Relation::EQUAL, 123));
foreach ($myClassList as $myClass) {
echo $myClass->someProperty;
}

Get All Records

// Get all records (paginated, default is page 0, limit 50)
$myClassList = MyClass::all();

// Get page 2 with 20 records per page
$myClassList = MyClass::all(2, 20);

Delete a Record

$myClass = MyClass::get(1);
$myClass->delete();

Refresh a Record

// Retrieve a record
$myClass = MyClass::get(1);

// do some changes in the database
// OR
// expect that the record was changed by another process

// Get the updated data from the database
$myClass->refresh();

Update a Model from Another Model or Array

// Get a record
$myClass = MyClass::get(1);

// Update from array
$myClass->fill(['someProperty' => 789]);

// Update from another model
$anotherModel = MyClass::new(['someProperty' => 789]);
$myClass->fill($anotherModel);

// Save changes
$myClass->save();

Convert to Array

$myClass = MyClass::get(1);

// Convert to array (excluding null values)
$array = $myClass->toArray();

// Convert to array (including null values)
$array = $myClass->toArray(true);

Using the Query Class

$query = MyClass::joinWith('other_table');
// do some query here
// ...
// Execute the query
$myClassList = MyClass::query($query);

Get Table Name

$tableName = MyClass::tableName();

Custom Mapper Configuration

By default, the Active Record uses the class attributes to discover the mapper configuration. You can override this behavior by implementing the discoverClass method:

class MyClass
{
use ActiveRecord;

// Override the default mapper discovery
protected static function discoverClass(): string|Mapper
{
// Return a custom mapper
return new Mapper(
self::class,
'custom_table',
['id']
);
}
}

Advantages of Active Record

The Active Record pattern offers several advantages:

  1. Simplicity: It provides a simple, intuitive interface for database operations
  2. Encapsulation: Database operations are encapsulated within the model class
  3. Reduced Boilerplate: Eliminates the need for separate repository classes for basic operations
  4. Fluent Interface: Enables method chaining for a more readable code style

When to Use Active Record vs. Repository

Both patterns have their place in application development:

  • Use Active Record when:

    • You prefer a simpler, more direct approach
    • Your application has straightforward database operations
    • You want to reduce the number of classes in your codebase
  • Use Repository when:

    • You need more control over database operations
    • Your application requires complex queries
    • You prefer a more explicit separation between models and database operations
    • You're implementing a domain-driven design approach