- Table Data Gateway
- Row Data Gateway
- Active Record
- Data Mapper
Row Data Gateway
An object that acts as a Gateway (466) to a single record in a data source. There is one instance per row.
Embedding database access code in in-memory objects can leave you with a few disadvantages. For a start, if your in-memory objects have business logic of their own, adding the database manipulation code increases complexity. Testing is awkward too since, if your in-memory objects are tied to a database, tests are slower to run because of all the database access. You may have to access multiple databases with all those annoying little variations on their SQL.
A Row Data Gateway gives you objects that look exactly like the record in your record structure but can be accessed with the regular mechanisms of your programming language. All details of data source access are hidden behind this interface.
How It Works
A Row Data Gateway acts as an object that exactly mimics a single record, such as one database row. In it each column in the database becomes one field. The Row Data Gateway will usually do any type conversion from the data source types to the in-memory types, but this conversion is pretty simple. This pattern holds the data about a row so that a client can then access the Row Data Gateway directly. The gateway acts as a good interface for each row of data. This approach works particularly well for Transaction Scripts (110).
With a Row Data Gateway you're faced with the questions of where to put the find operations that generate this pattern. You can use static find methods, but they preclude polymorphism should you want to substitute different finder methods for different data sources. In this case it often makes sense to have separate finder objects so that each table in a relational database will have one finder class and one gateway class for the results (Figure 10.2).
It's often hard to tell the difference between a Row Data Gateway and an Active Record (160). The crux of the matter is whether there's any domain logic present; if there is, you have an Active Record (160). A Row Data Gateway should contain only database access logic and no domain logic.
As with any other form of tabular encapsulation, you can use a Row Data Gateway with a view or query as well as a table. Updates often turn out to be more complicated this way, as you have to update the underlying tables. Also, if you have two Row Data Gateways that operate on the same underlying tables, you may find that the second Row Data Gateway you update undoes the changes on the first. There's no general way to prevent this; developers just have to be aware of how virtual Row Data Gateways are formed. After all, the same thing can happen with updatable views. Of course, you can choose not to provide update operations.
Row Data Gateways tend to be somewhat tedious to write, but they're a very good candidate for code generation based on a Metadata Mapping (306). This way all your database access code can be automatically built for you during your automated build process.
When to Use It
The choice of Row Data Gateway often takes two steps: first whether to use a gateway at all and second whether to use Row Data Gateway or Table Data Gateway (144).
I use Row Data Gateway most often when I'm using a Transaction Script (110). In this case it nicely factors out the database access code and allows it to be reused easily by different Transaction Scripts (110).
I don't use a Row Data Gateway when I'm using a Domain Model (116). If the mapping is simple, Active Record (160) does the same job without an additional layer of code. If the mapping is complex, Data Mapper (165) works better, as it's better at decoupling the data structure from the domain objects because the domain objects don't need to know the layout of the database. Of course, you can use the Row Data Gateway to shield the domain objects from the database structure. That's a good thing if you're changing the database structure when using Row Data Gateway and you don't want to change the domain logic. However, doing this on a large scale leads you to three data representations: one in the business logic, one in the Row Data Gateway, and one in the database—and that's one too many. For that reason I usually have Row Data Gateways that mirror the database structure.
Interestingly, I've seen Row Data Gateway used very nicely with Data Mapper (165). Although this seems like extra work, it can be effective iff the Row Data Gateways are automatically generated from metadata while the Data Mappers (165) are done by hand.
If you use Transaction Script (110) with Row Data Gateway, you may notice that you have business logic that's repeated across multiple scripts; logic that would make sense in the Row Data Gateway. Moving that logic will gradually turn your Row Data Gateway into an Active Record (160), which is often good as it reduces duplication in the business logic.
Example: A Person Record (Java)
Here's an example for Row Data Gateway. It's a simple person table.
create table people (ID int primary key, lastname varchar, firstname varchar, number_of_dependents int)
PersonGateway is a gateway for the table. It starts with data fields and accessors.
class PersonGateway... private String lastName; private String firstName; private int numberOfDependents; public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public int getNumberOfDependents() { return numberOfDependents; } public void setNumberOfDependents(int numberOfDependents) { this.numberOfDependents = numberOfDependents; }
The gateway class itself can handle updates and inserts.
class PersonGateway... private static final String updateStatementString = "UPDATE people " + " set lastname = ?, firstname = ?, number_of_dependents = ? " + " where id = ?"; public void update() { PreparedStatement updateStatement = null; try { updateStatement = DB.prepare(updateStatementString); updateStatement.setString(1, lastName); updateStatement.setString(2, firstName); updateStatement.setInt(3, numberOfDependents); updateStatement.setInt(4, getID().intValue()); updateStatement.execute(); } catch (Exception e) { throw new ApplicationException(e); } finally {DB.cleanUp(updateStatement); } } private static final String insertStatementString = "INSERT INTO people VALUES (?, ?, ?, ?)"; public Long insert() { PreparedStatement insertStatement = null; try { insertStatement = DB.prepare(insertStatementString); setID(findNextDatabaseId()); insertStatement.setInt(1, getID().intValue()); insertStatement.setString(2, lastName); insertStatement.setString(3, firstName); insertStatement.setInt(4, numberOfDependents); insertStatement.execute(); Registry.addPerson(this); return getID(); } catch (SQLException e) { throw new ApplicationException(e); } finally {DB.cleanUp(insertStatement); } }
To pull people out of the database, we have a separate PersonFinder. This works with the gateway to create new gateway objects.
class PersonFinder... private final static String findStatementString = "SELECT id, lastname, firstname, number_of_dependents " + " from people " + " WHERE id = ?"; public PersonGateway find(Long id) { PersonGateway result = (PersonGateway) Registry.getPerson(id); if (result != null) return result; PreparedStatement findStatement = null; ResultSet rs = null; try { findStatement = DB.prepare(findStatementString); findStatement.setLong(1, id.longValue()); rs = findStatement.executeQuery(); rs.next(); result = PersonGateway.load(rs); return result; } catch (SQLException e) { throw new ApplicationException(e); } finally {DB.cleanUp(findStatement, rs); } } public PersonGateway find(long id) { return find(new Long(id)); } class PersonGateway... public static PersonGateway load(ResultSet rs) throws SQLException { Long id = new Long(rs.getLong(1)); PersonGateway result = (PersonGateway) Registry.getPerson(id); if (result != null) return result; String lastNameArg = rs.getString(2); String firstNameArg = rs.getString(3); int numDependentsArg = rs.getInt(4); result = new PersonGateway(id, lastNameArg, firstNameArg, numDependentsArg); Registry.addPerson(result); return result; }
To find more than one person according to some criteria we can provide a suitable finder method.
class PersonFinder... private static final String findResponsibleStatement = "SELECT id, lastname, firstname, number_of_dependents " + " from people " + " WHERE number_of_dependents > 0"; public List findResponsibles() { List result = new ArrayList(); PreparedStatement stmt = null; ResultSet rs = null; try { stmt = DB.prepare(findResponsibleStatement); rs = stmt.executeQuery(); while (rs.next()) { result.add(PersonGateway.load(rs)); } return result; } catch (SQLException e) { throw new ApplicationException(e); } finally {DB.cleanUp(stmt, rs); } }
The finder uses a Registry (480) to hold Identity Maps (195).
We can now use the gateways from a Transaction Script (110)
PersonFinder finder = new PersonFinder(); Iterator people = finder.findResponsibles().iterator(); StringBuffer result = new StringBuffer(); while (people.hasNext()) { PersonGateway each = (PersonGateway) people.next(); result.append(each.getLastName()); result.append(" "); result.append(each.getFirstName()); result.append(" "); result.append(String.valueOf(each.getNumberOfDependents())); result.append(" } return result.toString();
Example: A Data Holder for a Domain Object (Java)
I use Row Data Gateway mostly with Transaction Script (110). If we want to use the Row Data Gateway from a Domain Model (116), the domain objects need to get at the data from the gateway. Instead of copying the data to the domain object we can use the Row Data Gateway as a data holder for the domain object.
class Person... private PersonGateway data; public Person(PersonGateway data) { this.data = data; }
Accessors on the domain logic can then delegate to the gateway for the data.
class Person... public int getNumberOfDependents() { return data.getNumberOfDependents(); }
The domain logic uses the getters to pull the data from the gateway.
class Person... public Money getExemption() { Money baseExemption = Money.dollars(1500); Money dependentExemption = Money.dollars(750); return baseExemption.add(dependentExemption.multiply(this.getNumberOfDependents())); }