Learn

Architecture & pattern guides

Read the idea, build it on a Grid, prove it in a challenge. Short, practical guides — illustrated with RodGrid's own nodes — on system design, design patterns, flow modeling, and how to get the most out of the app.

All articles

Active Record vs Data Mapper

Who knows how to load and save a domain object: the object itself, wrapping its row, or a separate mapper that keeps objects and tables apart. What each costs, when each fits, and the mapping problems a mapper brings with it.

A class that saves itself

A climbing gym keeps its routes in one table, routes: an id, the wall, the grade and the day the route was set. The app's Route class has the same fields, and the first version gives it the database work too: Route.find(42) loads a row and returns a Route, and route.save() writes it back. The setters' screen, the climbers' app and the nightly report all use that one class.

That design is Active Record, as Martin Fowler named it: an object that wraps a row in a table, carries the database access for it, and holds the domain logic for that data. Rails says its Active Record is an implementation of the pattern, and Django's design notes say its models follow it. For a small team it is quick to build and easy to read: everything about a route is in one file, and an edit form is little more than its fields. Designing the tables, keys and relationships is a separate job, for Entity-relationship modelling.

One class, one row

Each field of Route is a column of routes, and the base class it extends does the SQL. One object stands for one row.

Rules move in, and the database follows

Then the gym's rules arrive, and they land in Route, because that is where everything about a route lives. A route cannot be set on a wall closed for resurfacing. Once ten climbers have voted on a grade, the app shows the median of their votes beside the setter's grade. A route older than eight weeks goes on the list to strip. The quickest home for the first two is a callback, code the framework runs around save(): before saving, check that the wall is open; after saving a vote, recompute the community grade.

Together they change what save() means. The script that imports 300 routes from the setters' spreadsheet now runs a wall query for every row. A test of the median rule has to save votes to make the rule run, so it needs a database with a wall, a route and ten votes in it, and a suite that took seconds takes minutes. Rules written as plain methods can still be tested without a database; the ones hung on save(), or that query other tables, pull it in.

The schema adds a second cost. The gym grades boulders on the V scale and roped routes in French grades, so the rules want a Grade that knows its scale and compares with another; the table has a grade text column that the reports read. An Active Record class is shaped like its table, so either the domain speaks in strings or the class takes on the conversion, through a custom attribute type or code of its own, and the further the object model and the schema drift apart, the more the class becomes a translator.

A mapper between the two

Data Mapper separates them: in Fowler's words, a layer of mappers that moves data between objects and a database while keeping them independent of each other. Route becomes a plain class with a Grade, a recordVote(vote) and an isDueForStrip(today), and nothing in it mentions a table. A RouteMapper knows both sides: find(id) reads a row and builds a Route from it, turning the grade text into a Grade, and save(route) does the reverse. SQLAlchemy describes its ORM as built on this pattern.

The same route, mapped

Route has no arrow to anything that persists. The mapper depends on both sides, and neither side depends on the mapper.

Now the median test builds a Route in memory, records ten votes and reads the grade, with no database. The wall check moves to the code that sets a route, where it is visible and the import can run it once for all 300 rows, instead of firing on every save. Few projects write the mapper by hand: you describe the mapping, in annotations or a mapping file, and the ORM generates the SQL. With annotations the class still carries mapping metadata, but none of its methods touches the database.

The price of the separation

There are now two shapes of the same route, the class and the row, and a new column the domain needs means a change to the table, the class and the mapping between them. And since Route no longer saves itself, something else has to know what to write and when.

  • Change tracking. An ORM session remembers the objects it loaded and, at flush, sends the changed ones as writes inside its transaction; in the usual setup a commit flushes first, then makes those writes durable. The session implements a Unit of Work.
  • Lazy loading. A route's votes load only when first touched, so a wall screen listing 200 routes with their vote counts sends 201 queries when each count touches the votes, the N+1 problem, which Active Record ORMs share; loading every route's votes in one query, called eager loading, fixes it, and a grouped count is lighter still when only the numbers show.
  • Identity. A mapper that builds a new object on every load gives two objects for route 42 loaded twice, and the two copies can disagree about its grade; ORMs such as SQLAlchemy keep an identity map from id to object within a session to prevent it, keyed on the id that makes the route an Entity.

Where Repository fits

With a mapper in place, the domain still needs a way to ask for routes, such as routes.onWall(wallId): a Repository is that collection-like interface, declared in the domain and implemented with the mapper, one per aggregate root, the unit Aggregate defines. Over Active Record it adds much less, because Route.where(wall_id: 7) already is a finder: a Repository on top mostly gathers those finders in one place, and the object it returns still carries save(). The mapper is also what lets the core stay free of persistence code, the part of Hexagonal (Ports & Adapters) that concerns the database.

When the row is the model

Data Mapper earns its cost where the rules are. The gym's admin screens, for opening hours, membership types and the price of a day pass, are tables with forms on them. Their objects are their rows, their rules fit in a validation or two, and a mapper there would be a second copy of the schema to keep in step. Active Record is the better choice for that kind of code: CRUD-shaped, with fields that match columns and few rules, written by a small team that values one file per concept. Both can live in one codebase: Active Record for the admin tables, a mapper for Route.

Callbacks and empty models

  • Rules in callbacks. They run on every save, from every caller, the one-off admin fix included, and nothing at the call site shows it.
  • A mapper over an empty model. If Route is fields with getters and setters, and the rules live in services that pull its data out, the team pays for the mapping layer and gets little of what it was for: there is no behaviour in the model to test without a database.

Point the arrows on a Grid

The Layered Dependencies Challenge asks for the dependency that Data Mapper makes possible. Put domain::Route and the domain::RouteRepository contract in the domain package, and infrastructure::SqlRouteRepository, which works through the mapping, in infrastructure, its connection to the contract labelled «realize», with every arrow pointing toward the domain. Under Active Record, Route would extend the ORM's base class from infrastructure, and the domain package would hold an arrow pointing outward, the one dependency this Challenge asks you not to draw. Aggregate & Repository practises the Repository on top of the mapper.

Practice this