--------------------------------------------------------------------------------
Table Data Gateway
--------------------------------------------------------------------------------
An object that acts as a Gateway to a database table
一个作为数据库表Gateway的对象
(图) http://martinfowler.com/isa/dbgateTable.gif
分离访问数据库的代码与程序的其他代码是一件好事。一个简单的Table Data Gateway掌
握了所有用于访问单个数据表的SQL语句:select, insert, update, 和delete。其他程序
调用它的子程序来更改数据库,以及使用它的find程序来实施查询。每个find程序都回传
合适的数据结构。
How it Works
一个Table Data Gateway有一个简单的接口,通常由一些从数据库中取数据的find方法和
update,insert,delete方法组成。每一个方法都把输入的参数映射到SQL语句中并向一个数
据库连接提交这个SQL。Table Data Gateway通常是stateless(?)的(译注:原句为The
Table Data Gateway is usually stateless),因为它扮演的角色是存取数据。
Table Data Gateway中最麻烦的事情是怎样从一个查询中返回信息。即使是一个简单的使
用id查找的查询都需要返回多个数据项目。在你能够一次返回多个值的环境中,你可以用这种
技术来返回一条记录。但许多语言仅仅允许你使用一个返回值,而且许多查询会返回多条记录。
一个替代的办法是返回一些简单的数据结构,例如一个map。map有一定作用,但为了使数据
从数据库中进入map中,数据必须从record set中复制出来。我发现使用map来传递数据是一
个坏方法,它使编译期的检查失效,而且没有提供一个显式的接口 - 导致了当编程人员对
map拼写错误而形成的bug。一个更好的选择是使用Data Transfer Object(译注:另一种
模式。一个仅仅用于传递数据的类,不包含其他复杂的处理),这需要创建另一个对象 - 但
它可能在别处已被用到了。(anther object to be create - but one that may well be
used elsewhere.)
为了节省以上提到的所有这些代价,你可以返回一个从SQL查询中得出的Record Set。但这
在概念上是很模糊的,因为理想中我们希望内存中的对象不需要知道关于SQL接口的细节。
而且如果你无法简单地在你代码中建立起record set机制,这种方法还使以数据库来代替文件变
得困难。但是在许多普遍使用Record Set的环境中,例如.NET,这是一个非常有效的手段。
这样,一个基于数据表的Table Data Gateway与Table Module结合的非常好。如果你所有
的更新都通过Table Data Gateway提交, 那返回的数据可以基于视图(view)而不是实际的数
据表。这一做法减少了你的代码与数据库的耦合。
如果你正在使用一个Domain Model,你可以使用Table Data Gateway返回合适的业务对象。
这样做的问题是在gateway与业务对象之间存在双向依赖。由于这两者关系紧密所以这并不
是一件很可怕的事情,但这总是一件我不希望发生的事。
在你使用Table Data Gateway的大部分情况下,你会为数据库中每个数据表设置一个
Table Data Gateway。但对于非常简单的情况,你可以用一个Table Data Gateway处理全
部数据表的所有方法
When to Use it
与Row Data Gateway一样,选择使用Table Data Gateway时首先要确定是否使用Gateway手
段,然后确定使用哪种Gateway.
我发现Table Data Gateway可能是可用的最简单的数据接口模式了,因为它很好地实现了到
数据表或记录类型的映射。它还提供了一个自然的结合点来为封装精确的数据源访问逻辑。
我很少将它与Domain Model一起使用,因为我发现Data Mapper把Domain Model与数据库分
离得更好,而且用起来并不是太过于复杂。
Table Data Gateway与Table Module结合时工作得特别好,这时Table Data Gateway生产出
一个record set数据结构给Table Module进行处理。事实上,我不能为Table Module想出其
他数据库映射方案了。
就象Row Data Gateway, Table Data Gateway对Transaction Scripts(译注:另一模式,
就是最常用的一事务一子程序模式)非常适合。在这两者间(Row Data Gateway vs. Table Data Gateway ?)
做出选择的标准在于怎样处理多条数据记录。许多人喜欢使用Data Transfer Object,但
这似乎要做许多不值得的工作,除非同一个Data Transfer Object在其他地方也被用到。当
结果集的表达方式能很方便地被Transaction Script使用的时候,我倾向于使用Table Data Gateway。
有趣地,把Table Data Gateway与Data Mapper结合起来使用常常是有意义的,
Data Mapper可以通过Table Data Gateway与数据库对话。虽然当所有东西都被硬编码的
时候,这样做意义不大。但如果你希望为Table Data Gateway使用metadata(译注:元数
据?)但又喜欢把实际的映射硬编码到domain object中的话,把Table Data Gateway与
Data Mapper结合起来的做法就非常有效了。
使用Table Data Gateway来封装数据库访问的一个好处是同样的接口能够用于两种情形:
使用SQL来操作数据库的情形,或者使用储存过程的情形。实际上储存过程本身常常被组织
为一个Table Data Gateway,这种情况下实际的表结构被封装在insert和update储存过程
后面。find过程可以返回视图,这些视图有助于隐藏实际的表结构。
例子:Person Gateway (c#)
Table Data Gateway 是在windows世界中一种很常见的数据库访问形式,因此举一个C#的
例子是很有意义的。但在这样做的同时,我必须强调这个Table Data Gateway的标准形式
并不仅仅适合.NET环境,因为它并没有从ADO.NET的data set中得到多少好处。相反地,它
使用了Data Reader,一个游标类型的数据库记录接口。在你要处理大量的信息因而不想一
次性把所有东西都放进内存的时候,data reader是一个很好的选择。
在这个例子中我使用了一个连接数据库中一个人员表的person gateway类。这个person
gateway包括了查找代码,返回一个ADO.NET的data reader来访问返回的数据。
class PersonGateway...
public IDataReader FindAll() {
String sql = "select * from person";
return new OleDbCommand(sql, DB.Connection).ExecuteReader();
}
public IDataReader FindWithLastName(String lastName) {
String sql = "SELECT * FROM person WHERE lastname = ?";
IDbCommand comm = new OleDbCommand(sql, DB.Connection);
comm.Parameters.Add(new OleDbParameter("lastname", lastName));
return comm.ExecuteReader();
}
public IDataReader FindWhere(String whereClause) {
String sql = String.Format("select * from person where {0}", whereClause);
return new OleDbCommand(sql, DB.Connection).ExecuteReader();
}
========== DELPHI ============
TPersonGateway = class...
public
function FindAll() : TDataSet;
function FindWithLastName(lastName : String) : TDataSet;
function FindWhere(whereClause : String) : TDataSet;
end;
function TPersonGateway.FindAll() : TDataSet;
begin
result := TQuery.create(Application);
try
result.DatabaseName := Query.DatabaseName;
//Query是gateway用于储存于数据库连接的TQuery对象。
result.SQL.Text := 'select * from person';
result.Open;
except
FreeAndNil(result)
end;
end;
function TPersonGateway.FindWithLastName(lastName:string) : TDataSet;
begin
result := TQuery.create(Application);
try
result.DatabaseName := Query.DatabaseName;
result.SQL.Text := 'select * from person where lastname = :lastName';
result.Params[0].asstring := lastName;
result.Open;
except
FreeAndNil(result)
end;
end;
function TPersonGateway.FindWhere(whereClause : string) : TDataSet;
begin
result := TQuery.create(Application);
try
result.DatabaseName := Query.DatabaseName;
result.SQL.Text := 'select * from person where ' + whereClause;
result.Open;
except
FreeAndNil(result)
end;
end;
=========== END ================
你几乎总是希望用一个reader提交一批记录。在一个罕有的情形下,你可能会希望用一个
方法提取某一条记录的数据。
class PersonGateway...
public Object[] FindRow (long key) {
String sql = "SELECT * FROM person WHERE id = ?";
IDbCommand comm = new OleDbCommand(sql, DB.Connection);
comm.Parameters.Add(new OleDbParameter("key",key));
IDataReader reader = comm.ExecuteReader();
reader.Read();
Object [] result = new Object[reader.FieldCount];
reader.GetValues(result);
reader.Close();
return result;
}
=========== DELPHI ====================
TPersonGateway = class...
public
function FindrRow(key : integer) : TFields;
end;
function TPersonGateway.FindRow() : TFields;
begin
Query.SQL.Text := 'select * from person WHERE id = :id';
Query.params[0].asinteger := key;
Query.Open;
result := GetRowValues(Query); //GetRowValues是一个成员方法,把当前DataSet的活动记录内容复制到一个TFields对象中。
Query.Close;
end;
============= END =====================
update和insert方法从参数中取得需要的数据并调用合适的SQL语句。
class PersonGateway...
public void Update (long key, String lastname, String firstname, long numberOfDependents){
String sql = "UPDATE person SET lastname = ?, firstname = ?, numberOfDependents = ? WHERE id = ?";
IDbCommand comm = new OleDbCommand(sql, DB.Connection);
comm.Parameters.Add(new OleDbParameter ("last", lastname));
comm.Parameters.Add(new OleDbParameter ("first", firstname));
comm.Parameters.Add(new OleDbParameter ("numDep", numberOfDependents));
comm.Parameters.Add(new OleDbParameter ("key", key));
comm.ExecuteNonQuery();
}
========= DELPHI ===========
TPersonGateway = class...
public
procedure Update(key:integer; lastname,firstname:string; numberOfDependents:integer);
end;
procedure TPersonGateway.Update(key:integer; lastname,firstname:string; numberOfDependents:integer);
begin
Query.SQL.Text := 'UPDATE person SET lastname = :lastname, firstname = :firstname, numberOfDependents = :NOD where id = :id';
Query.Params[0].asstring := lastname;
Query.Params[1].asstring := firstname;
Query.Params[2].asinteger := numberOfDependents;
Query.Params[3].asinteger := key;
Query.Execute;
end;
===========END==============
class PersonGateway...
public long Insert(String lastName, String firstName, long numberOfDependents) {
String sql = "INSERT INTO person VALUES (?,?,?,?)";
long key = GetNextID();
IDbCommand comm = new OleDbCommand(sql, DB.Connection);
comm.Parameters.Add(new OleDbParameter ("key", key));
comm.Parameters.Add(new OleDbParameter ("last", lastName));
comm.Parameters.Add(new OleDbParameter ("first", firstName));
comm.Parameters.Add(new OleDbParameter ("numDep", numberOfDependents));
comm.ExecuteNonQuery();
return key;
}
========= DELPHI ===========
TPersonGateway = class...
public
function Insert(lastname,firstname:string; numberOfDependents:integer):integer;
end;
function TPersonGateway.Insert(lastname,firstname:string; numberOfDependents:integer) : integer;
begin
Query.SQL.Text := 'INSERT INTO person VALUES
key, :lastname, :firstname, :NOD)';
result := GetNextID();
Query.Params[0].asinteger := result;
Query.Params[1].asstring := lastname;
Query.Params[2].asstring := firstname;
Query.Params[3].asinteger := numberOfDependents;
Query.Execute;
end;
===========END==============
一个delete方法仅仅需要一个key。
class PersonGateway...
public void Delete (long key) {
String sql = "DELETE FROM person WHERE id = ?";
IDbCommand comm = new OleDbCommand(sql, DB.Connection);
comm.Parameters.Add(new OleDbParameter ("key", key));
comm.ExecuteNonQuery();
}
========= DELPHI ===========
TPersonGateway = class...
public
procedure Delete(key:integer);
end;
procedure TPersonGateway.Delete(key:integer);
begin
Query.SQL.Text := 'DELETE FROM person WHERE id = :id';
Query.Params[0].asinteger := key;
Query.Execute;
end;
===========END==============
例子:使用ADO.NET的Data Sets (C#)
(译注,由于在delphi中使用ADO.NET的方法我并不熟悉,因此以下代码不作转换)
一般的Table Data Gateway可以在任何平台下运作,因为它仅仅是SQL语句的一层包装。当
你使用.NET的时候你会经常使用data set,但Table Data Gateway仍然有用,虽然它变为
了一种不同的形式。
一个data set需要数据适配器(data adapters)来读取数据到data set中并对数据进行更新。
因此我发现为这个data set和存取数据的adapters定义一个容器非常有用。这样,一个
gateway使用这个容器来保存data sets和adapters。大部分行为都是有普遍意义的,可以
放在超类中。
图1: 面向data set的gateway以及支持它的容器(holder)类图
http://martinfowler.com/isa/dateSetGateway.gif
容器储存了一个data set和一个以表名来索引的adapter集合。
class DataSetHolder...
public DataSet Data = new DataSet();
private Hashtable DataAdapters = new Hashtable();
gateway 储存这个容器,并把data set暴露给用户。
class DataGateway...
public DataSetHolder Holder;
public DataSet Data {
get {return Holder.Data;}
}
gateway可以对一个已存在的容器进行操作,或者创建一个。
class DataGateway...
protected DataSetGateway() {
Holder = new DataSetHolder();
}
protected DataSetGateway(DataSetHolder holder) {
this.Holder = holder;
}
在这里,find方法可以以不同的方式来工作。因为一个data set是一个面向数据表的容器,而且一个data set可以包含来自几个数据表的数据。因此把数据读入data set是更好的做法。
class DataGateway...
public void LoadAll() {
String commandString = String.Format("select * from {0}", TableName);
Holder.FillData(commandString, TableName);
}
public void LoadWhere(String whereClause) {
String commandString =
String.Format("select * from {0} where {1}", TableName,whereClause);
Holder.FillData(commandString, TableName);
}
abstract public String TableName {get;}
class PersonGateway...
public override String TableName {
get {return "Person";}
}
class DataSetHolder...
public void FillData(String query, String tableName) {
if (DataAdapters.Contains(tableName)) throw new MutlipleLoadException();
OleDbDataAdapter da = new OleDbDataAdapter(query, DB.Connection);
OleDbCommandBuilder builder = new OleDbCommandBuilder(da);
da.Fill(Data, tableName);
DataAdapters.Add(tableName, da);
}
更新数据的时候,你直接在客户代码中对data set进行操作。
person.LoadAll();
person[key]["lastname"] = "Odell";
person.Holder.Update();
gateway可以提供一个索引器来简化取得特定记录的操作。
class DataGateway...
public DataRow this[long key] {
get {
String filter = String.Format("id = {0}", key);
return Table.Select(filter)[0];
}
}
public override DataTable Table {
get { return Data.Tables[TableName];}
}
update方法触发容器(holder)中的update行为。
class DataSetHolder...
public void Update() {
foreach (String table in DataAdapters.Keys)
((OleDbDataAdapter)DataAdapters
).Update(Data, table);
}
public DataTable this[String tableName] {
get {return Data.Tables[tableName];}
}
插入可以用差不多同样的办法完成:获得一个data set, 向数据表插入一条新的记录,并向每一字段填入数据。但是使用一个update方法在一个调用中实现插入也是很有用的。
class DataGateway...
public long Insert(String lastName, String firstname, int numberOfDependents) {
long key = new PersonGatewayDS().GetNextID();
DataRow newRow = Table.NewRow();
newRow["id"] = key;
newRow["lastName"] = lastName;
newRow["firstName"] = firstname;
newRow["numberOfDependents"] = numberOfDependents;
Table.Rows.Add(newRow);
return key;
}
--------------------------------------------------------------------------------