# Active Record Pattern

## Problem

For simple domain models, having a separate persistence layer (repositories, mappers) creates unnecessary indirection. Developers want to quickly save, load, and query objects without writing boilerplate mapping code.

## Solution

Combine the domain object and the data-access logic into a single class. Each instance corresponds to a row in a database table. The class provides methods like `save()`, `find()`, and `delete()` directly on the model. The object knows how to persist itself.

## When to Use

- Simple CRUD applications with straightforward domain logic
- Rapid prototyping where speed of development matters
- Small to medium projects with low domain complexity
- When using frameworks that already follow this pattern (Rails, Django, Laravel)

## When to Avoid

- Complex domains with rich business logic (violates SRP)
- When you need to test domain logic without a database
- Applications where the same data is accessed through different schemas
- When domain model and database schema need to evolve independently

## Pseudocode

```
class User(ActiveRecord):
    table = "users"
    fields = [id, name, email]

    function save():
        if id exists:
            UPDATE table SET fields WHERE id = self.id
        else:
            INSERT INTO table (fields) VALUES (...)
            self.id = last_insert_id

    class function find(id):
        row = SELECT * FROM table WHERE id = id
        return new User(row)

    function delete():
        DELETE FROM table WHERE id = self.id
```

## Python Implementation

```python
import sqlite3
from typing import Optional


class ActiveRecord:
    """Base class providing CRUD operations."""
    _table: str = ""
    _columns: list[str] = []
    _db: Optional[sqlite3.Connection] = None

    @classmethod
    def _get_db(cls):
        if cls._db is None:
            cls._db = sqlite3.connect(":memory:")
            cls._db.row_factory = sqlite3.Row
        return cls._db

    def __init__(self, **kwargs):
        self.id: Optional[int] = kwargs.get("id")
        for col in self._columns:
            setattr(self, col, kwargs.get(col))

    def save(self) -> "ActiveRecord":
        db = self._get_db()
        if self.id is not None:
            sets = ", ".join(f"{c} = ?" for c in self._columns)
            vals = [getattr(self, c) for c in self._columns] + [self.id]
            db.execute(f"UPDATE {self._table} SET {sets} WHERE id = ?", vals)
        else:
            cols = ", ".join(self._columns)
            placeholders = ", ".join("?" for _ in self._columns)
            vals = [getattr(self, c) for c in self._columns]
            cursor = db.execute(
                f"INSERT INTO {self._table} ({cols}) VALUES ({placeholders})", vals
            )
            self.id = cursor.lastrowid
        db.commit()
        return self

    @classmethod
    def find(cls, record_id: int) -> Optional["ActiveRecord"]:
        db = cls._get_db()
        row = db.execute(
            f"SELECT * FROM {cls._table} WHERE id = ?", (record_id,)
        ).fetchone()
        if row is None:
            return None
        return cls(**dict(row))

    @classmethod
    def all(cls) -> list["ActiveRecord"]:
        db = cls._get_db()
        rows = db.execute(f"SELECT * FROM {cls._table}").fetchall()
        return [cls(**dict(r)) for r in rows]

    def delete(self) -> None:
        db = self._get_db()
        db.execute(f"DELETE FROM {self._table} WHERE id = ?", (self.id,))
        db.commit()
        self.id = None

    def __repr__(self):
        attrs = ", ".join(f"{c}={getattr(self, c)!r}" for c in self._columns)
        return f"{self.__class__.__name__}(id={self.id}, {attrs})"


class User(ActiveRecord):
    _table = "users"
    _columns = ["name", "email"]

    @classmethod
    def create_table(cls):
        db = cls._get_db()
        db.execute("""
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                email TEXT NOT NULL
            )
        """)
        db.commit()


# --- Demo ---
User.create_table()

# Create
alice = User(name="Alice", email="alice@example.com").save()
bob = User(name="Bob", email="bob@example.com").save()
print(f"Created: {alice}")
print(f"Created: {bob}")

# Read
found = User.find(1)
print(f"Found by ID 1: {found}")

# Update
alice.email = "alice@newdomain.com"
alice.save()
print(f"Updated: {alice}")

# List all
print(f"All users: {User.all()}")

# Delete
bob.delete()
print(f"After deleting Bob: {User.all()}")
```

**Output:**
```
Created: User(id=1, name='Alice', email='alice@example.com')
Created: User(id=2, name='Bob', email='bob@example.com')
Found by ID 1: User(id=1, name='Alice', email='alice@example.com')
Updated: User(id=1, name='Alice', email='alice@newdomain.com')
All users: [User(id=1, name='Alice', email='alice@newdomain.com'), User(id=2, name='Bob', email='bob@example.com')]
After deleting Bob: [User(id=1, name='Alice', email='alice@newdomain.com')]
```

## JavaScript Implementation

```javascript
class ActiveRecord {
  static _table = "";
  static _columns = [];
  static _store = new Map(); // In-memory DB simulation
  static _nextId = 1;

  constructor(attrs = {}) {
    this.id = attrs.id || null;
    for (const col of this.constructor._columns) {
      this[col] = attrs[col] ?? null;
    }
  }

  save() {
    const Cls = this.constructor;
    if (this.id !== null) {
      Cls._store.set(this.id, { ...this });
    } else {
      this.id = Cls._nextId++;
      Cls._store.set(this.id, { ...this });
    }
    return this;
  }

  static find(id) {
    const data = this._store.get(id);
    if (!data) return null;
    return new this(data);
  }

  static all() {
    return [...this._store.values()].map((d) => new this(d));
  }

  delete() {
    this.constructor._store.delete(this.id);
    this.id = null;
  }

  toString() {
    const attrs = this.constructor._columns.map((c) => `${c}=${this[c]}`).join(", ");
    return `${this.constructor.name}(id=${this.id}, ${attrs})`;
  }
}

class User extends ActiveRecord {
  static _table = "users";
  static _columns = ["name", "email"];
}

// --- Demo ---
const alice = new User({ name: "Alice", email: "alice@example.com" }).save();
const bob = new User({ name: "Bob", email: "bob@example.com" }).save();
console.log(`Created: ${alice}`);
console.log(`Created: ${bob}`);

const found = User.find(1);
console.log(`Found by ID 1: ${found}`);

alice.email = "alice@newdomain.com";
alice.save();
console.log(`Updated: ${alice}`);

console.log(`All users: [${User.all().join(", ")}]`);

bob.delete();
console.log(`After deleting Bob: [${User.all().join(", ")}]`);
```

## Related Patterns

- **Data Mapper** — data mapper separates persistence logic from the domain object; use data mapper instead of active record for complex domains with rich business logic.
- **Repository** — repository adds a collection-like abstraction over data access; prefer repository over active record when domain complexity grows.
