- 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.
ARow Data Gatewaygives 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
ARow Data Gatewayacts as an object that exactly mimics a single record, such as one database row. In it each column in the database becomes one field. TheRow Data Gatewaywill 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 theRow Data Gatewaydirectly. The gateway acts as a good interface for each row of data. This approach works particularly well forTransaction Scripts (110).
With aRow Data Gatewayyou'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 aRow Data Gatewayand anActive Record (160).The crux of the matter is whether there's any domain logic present; if there is, you have anActive Record (160).ARow Data Gatewayshould contain only database access logic and no domain logic.
As with any other form of tabular encapsulation, you can use aRow Data Gatewaywith 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 twoRow Data Gatewaysthat operate on the same underlying tables, you may find that the secondRow Data Gatewayyou update undoes the changes on the first. There's no general way to prevent this; developers just have to be aware of how virtualRow Data Gatewaysare formed. After all, the same thing can happen with updatable views. Of course, you can choose not to provide update operations.
Row Data Gatewaystend to be somewhat tedious to write, but they're a very good candidate for code generation based on aMetadata 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 ofRow Data Gatewayoften takes two steps: first whether to use a gateway at all and second whether to useRow Data GatewayorTable Data Gateway (144).
I useRow Data Gatewaymost often when I'm using aTransaction Script (110).In this case it nicely factors out the database access code and allows it to be reused easily by differentTransaction Scripts (110).
I don't use aRow Data Gatewaywhen I'm using aDomain 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 theRow Data Gateway保护域对象从数据库中structure. That's a good thing if you're changing the database structure when usingRow Data Gatewayand 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 theRow Data Gateway,and one in the database—and that's one too many. For that reason I usually haveRow Data Gatewaysthat mirror the database structure.
Interestingly, I've seenRow Data Gatewayused very nicely withData Mapper (165).Although this seems like extra work, it can be effective iff theRow Data Gatewaysare automatically generated from metadata while theData Mappers (165)are done by hand.
If you useTransaction Script (110)withRow Data Gateway,you may notice that you have business logic that's repeated across multiple scripts; logic that would make sense in theRow Data Gateway.Moving that logic will gradually turn yourRow Data Gatewayinto anActive Record (160),which is often good as it reduces duplication in the business logic.
Example: A Person Record (Java)
Here's an example forRow 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.
类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.
类PersonGateway……私有静态最终字符串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 separatePersonFinder. This works with the gateway to create new gateway objects.
类PersonFinder……私人最终的静态字符串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.
类PersonFinder……私有静态最终字符串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 aRegistry (480)to holdIdentity Maps (195).
We can now use the gateways from aTransaction 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 useRow Data Gatewaymostly withTransaction Script (110).If we want to use theRow Data Gatewayfrom aDomain 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 theRow Data Gatewayas 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())); }