Oracle 的 DBTool - 第一部分






4.92/5 (44投票s)
编写自己的工具,提高生产力和可靠性。
引言
构建专门的代码生成工具是开发团队可以用来提高生产力和可靠性的方法。它可以是一个处理领域特定语言的工具,旨在有效地表达特定问题领域的信息;也可以是一个为一组关键字生成完美哈希函数的工具;或者是一个为多个输入范围预先计算一组数学结果的工具——生成大量将输入映射到结果的结构,以避免在运行时进行耗时的计算。
工具匠是 Harlan Mills 外科手术团队[^] 的重要组成部分。
外科医生必须是唯一判断他所能获得的服务是否足够的裁判。他需要一个工具匠,负责确保基础服务的充足性,并构建、维护和升级他团队所需的专用工具——主要是交互式计算机服务。每个团队都需要自己的工具匠,无论任何集中提供的服务多么出色和可靠,因为他的工作是确保他外科医生需要或想要的工具,而不考虑任何其他团队的需求。
处理来自数据库服务器的元数据的工具,是开发团队最常需要的工具之一——因为我们通常希望创建的不仅仅是现有商业或开源 ORM 创建的表或表集之间的映射层。
DBTool 的主要目的是为一个多层数据访问框架生成代码,该框架根据工业管理系统的要求量身定制。它不创建一个 IMS,但可以用来显著减少创建 IMS 所需的工作量。
DBTool 具备您期望从 ORM 工具获得的大部分功能,并允许我们指定某些表的处理方式与“普通”表完全不同。
-
一个连接另外两个表形成多对多关系的表可以被标记为此类。
-
一对多关系中的“多”端可以标记为时间序列,表示存在一条当前记录和多条包含历史数据的记录。标记为时间序列的表必须有一个唯一键,由对一对多关系中“一”端的引用和一个 TIMESTAMP 组成。DBTool 会生成代码,允许我们通过传递一个落在从当前记录的 TIMESTAMP 到下一条记录的 TIMESTAMP 之间的时间间隔内的时间来检索记录——从而轻松检索当时有效的记录。
-
一个连接另外两个表形成多对多关系的表可以同时被标记为时间序列和多对多关系,表示我们有一个当前关系,以及维护记录之间关系历史的记录。
-
一个表可以被标记为标签表。标签表必须位于与名为 ITEMS 的表的一对多关系的“多”端。这种构造的目的是为 ITEM 附加标签,或者更确切地说是属性。
-
一个表可以被标记为值表。值表是一个模板,用于存放标签表中一条记录的值。系统会为标签表中的每一行动态创建一个表。DBTool 使用此信息为每种标记为值表的表创建一个单独的标签类型,从而增加了向 ITEMS 表中的项目附加各种类型标签的能力。这些动态生成的表通常会包含大量包含测量数据的记录。
DBTool 创建的代码能够与消息队列系统集成,而无需指定特定的供应商。消息队列系统,如 WebSphere MQ[^],通常被认为是构成分布式处理系统的组件之间最可靠的通信机制。
访问元数据
DBTool 允许您浏览 Oracle 数据库的内容,但它肯定不能替代 PL/SQL Developer[^] 和其他类似的软件包。您还可以对数据库执行查询。
DBTool 本身就是一个如何使用生成代码的例子,因为它使用从 SYS 模式中各种视图生成的 Reader 类来实现。
当 DBTool 用于检查列信息时,它会显示关于该列的两组信息。第一组是 IDataReader.GetSchemaTable[^] 从 ADO.Net 驱动程序检索的信息,而其余部分是直接从 Oracle 数据库检索的列信息。
在使用 Oracle.DataAccess 程序集时,从 GetSchemaTable 检索到的信息非常有用,因为驱动程序执行的转换并不总是符合预期。
ADO.NET 列信息 | Oracle '原生' 列信息 |
---|---|
![]() |
![]() |
生成的代码
DBTool 生成的代码可以简要地分为以下几类:
- 客户端和服务器代码共有的类。
- 诸如
IDataProvider
、IDataObserver
和IDataContextProvider
之类的接口,它们指定了可以对数据库执行的操作。 - 用于在 WCF 客户端和服务器之间传输数据的类。
- 可用于使用消息队列系统查询和更新数据的类。
- 诸如
- 服务器端
- 低级别的 Accessor 和 Reader 类。
DataObserver
类,用于监视对数据所做的更改,当您想使用消息队列系统实现更改通知时非常有用。-
OperationsHandlerBase
是DataObserver
类的派生类,它为所有插入、更新和删除操作创建OperationNotification
对象,并将它们传递给虚方法HandleNotification
。通过从OperationsHandlerBase
类派生并重写HandleNotification
方法,您将能够使用消息队列技术实现异步更改通知。OperationsHandlerBase
的ProcessOperationRequest
方法以OperationRequest
为参数,并返回一个OperationReply
。代码生成器为IDataProvider
接口声明的所有操作创建OperationRequest
/OperationReply
类。OperationsHandlerBase
将OperationRequest
转换为对IDataContextProvider
的调用,并将结果转换为返回的OperationReply
。这可以用来使用消息队列技术执行所有的 CRUD 操作。 ServiceImplementation
类,它使用低级类实现了针对 Oracle 数据访问提供程序的IDataContextProvider
接口。DataService
是 WCF 服务实现,它通过IDataContextProvider
接口执行所有操作。
- 客户端
-
DataClient
实现了IDataContextProvider
接口,它通过由 svcutil[^] 生成的 WCF 客户端执行所有操作。DataClient
实现了HandleNotification
方法,该方法以OperationNotification
对象作为参数。代码生成器为插入、更新和删除操作创建OperationNotification
的派生类。DataClient
类将这些通知转换为IDataContextProvider
接口声明的相应事件。 EntityContext
和为添加到项目中的每个表创建的实体类。实体存在于其上下文之中,上下文保证了数据库中的一行最多只有一个对象来表示。EntitySource
是一个组件,它使用 Visual Studio 提供的设计工具提供快速应用程序开发。
-
下图提供了一种可能使用生成代码方式的粗略轮廓。
黑色箭头表示层之间的双向通信,而红色箭头显示变更通知的流向。
示例项目
下载内容包括一个示例,该示例是一个托管在 Windows Forms 应用程序中的 WCF 服务,它是使用 DBTool 生成的代码构建的。DBTool 创建了一个可以轻松托管在 Windows Forms 或 Windows 服务应用程序中的组件。当您调用 Start()
方法时,服务启动。
private void startToolStripMenuItem_Click(object sender, EventArgs e)
{
try
{
serverComponent.Start();
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
}
}
当您调用 Stop()
方法时,服务将停止。
private void stopToolStripMenuItem_Click(object sender, EventArgs e)
{
try
{
serverComponent.Stop();
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
}
}
只有属于 DBToolExampleUsers 组的用户才能连接到 WCF 服务。
设置客户端与 WCF 服务通信需要以下步骤:
public MainForm()
{
InitializeComponent();
创建一个 DataClient
对象,它实现了 IDataContextProvider
。
dataClient = new DataClient();
使用 DataClient
对象创建一个 EntityContext
对象。
entityContext = new EntityContext(dataClient);
将窗体赋给 EntityContext
对象的 SynchronizationControl
属性。
entityContext.SynchronizationControl = this;
将新的 EntityContext
对象设为 EntitySource
对象的默认 EntityContext
。
DefaultEntitySourceEntityContext.Context = entityContext;
}
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
Utils.Logging.EventAppender.Instance.MessageLoggedEvent += Instance_MessageLoggedEvent;
try
{
连接到 WCF 服务。
dataClient.Connect();
此时,客户端已连接到服务器。
RefreshNodes();
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
}
}
本文的大部分内容是关于 DBTool 生成的代码,所以有很多代码清单——这些都是 DBTool 为您省去编写功夫的东西。
由于所有操作都通过 IDataContextProvider
接口完成,因此 EntityContext
可以绕过 WCF,或者您可以设置一个通过 DataClient
操作的 DataService
。
DBA 功能
诚然,DBTool 还不能替代专业的 DBA 工具,但它有许多 DBA 通常也感兴趣的功能。
DBTool 查询 SYS.ALL_OBJECTS 视图以获取特定用户拥有的对象的基本信息。
SELECT
OWNER,
OBJECT_NAME,
SUBOBJECT_NAME,
OBJECT_ID,
DATA_OBJECT_ID,
OBJECT_TYPE,
CREATED,
LAST_DDL_TIME,
TIMESTAMP,
STATUS,
TEMPORARY,
GENERATED,
SECONDARY,
NAMESPACE,
EDITION_NAME
FROM
SYS.ALL_OBJECTS
where OWNER = :owner
该查询使用 ObjectReader
类执行,该类将字段公开为只读属性,从而在遍历结果集时可以轻松且非常可读地访问数据。
提取以下数据库对象类型的定义
- 集群
- 函数
- 目录
- 库
- 运算符
- 包
- Procedure
- 序列
- 同义词
- 表格
- 触发器
- 类型
- 视图
是使用一个简单的 select 语句完成的。
select
SYS.DBMS_METADATA.GET_DDL(:typeName,:objectName,:owner)
FROM DUAL
其中 SYS.DBMS_METADATA 是 Oracle 提供的一个 PL/SQL 包,它使这一切变得非常简单。
Oracle 没有 IDENTITY
列,取而代之的是我们有一种称为 SEQUENCE
的机制,它能根据请求生成唯一值。具体方法将在后面关于插入操作的演练中展示。SEQUENCE
的属性
DBTool 查询 ALL_SEQUENCES 视图
SELECT
SEQUENCE_OWNER,
SEQUENCE_NAME,
MIN_VALUE,
MAX_VALUE,
INCREMENT_BY,
CYCLE_FLAG,
ORDER_FLAG,
CACHE_SIZE,
LAST_NUMBER
FROM ALL_SEQUENCES
使用 SequenceReader
类来检索序列的属性。
表的属性提供了关于该表的大量信息。这将告诉您服务器如何配置表的增长方式,服务器是否会尝试将其缓存在内存中,或者是否启用了日志记录或监视,以及许多关于 Oracle 如何管理该表的其他有趣细节。
该信息从 ALL_TABLES 视图中检索
SELECT
OWNER, TABLE_NAME, TABLESPACE_NAME, CLUSTER_NAME, IOT_NAME, STATUS, PCT_FREE, PCT_USED,
INI_TRANS, MAX_TRANS, INITIAL_EXTENT, NEXT_EXTENT, MIN_EXTENTS, MAX_EXTENTS, PCT_INCREASE,
FREELISTS, FREELIST_GROUPS, LOGGING, BACKED_UP, NUM_ROWS, BLOCKS, EMPTY_BLOCKS, AVG_SPACE,
CHAIN_CNT, AVG_ROW_LEN, AVG_SPACE_FREELIST_BLOCKS, NUM_FREELIST_BLOCKS, DEGREE, INSTANCES,
CACHE, TABLE_LOCK, SAMPLE_SIZE, LAST_ANALYZED, PARTITIONED, IOT_TYPE, TEMPORARY, SECONDARY,
NESTED, BUFFER_POOL, FLASH_CACHE, CELL_FLASH_CACHE, ROW_MOVEMENT, GLOBAL_STATS, USER_STATS,
DURATION, SKIP_CORRUPT, MONITORING, CLUSTER_OWNER, DEPENDENCIES, COMPRESSION, COMPRESS_FOR,
DROPPED, READ_ONLY, SEGMENT_CREATED, RESULT_CACHE
FROM ALL_TABLES
并使用 TableReader
类从结果集中提取信息。有关视图的信息可通过 ALL_VIEWS 视图获取。
SELECT OWNER,VIEW_NAME,TEXT_LENGTH,TEXT,TYPE_TEXT_LENGTH,TYPE_TEXT,OID_TEXT_LENGTH,OID_TEXT,
VIEW_TYPE_OWNER,VIEW_TYPE,SUPERVIEW_NAME,EDITIONING_VIEW,READ_ONLY
FROM SYS.ALL_VIEWS
DBTool 使用 ViewReader
类来检索此信息。
有关视图返回或表中包含的列的信息是通过查询 SYS.ALL_TAB_COLUMNS 视图来检索的。
SELECT
OWNER,TABLE_NAME,COLUMN_NAME,DATA_TYPE,DATA_TYPE_MOD,DATA_TYPE_OWNER,DATA_LENGTH,
DATA_PRECISION,DATA_SCALE,NULLABLE,COLUMN_ID,DEFAULT_LENGTH,DATA_DEFAULT,NUM_DISTINCT,
LOW_VALUE,HIGH_VALUE,DENSITY,NUM_NULLS,NUM_BUCKETS,LAST_ANALYZED,SAMPLE_SIZE,
CHARACTER_SET_NAME,CHAR_COL_DECL_LENGTH,GLOBAL_STATS,USER_STATS,AVG_COL_LEN,CHAR_LENGTH,
CHAR_USED,V80_FMT_IMAGE,DATA_UPGRADED,HISTOGRAM
FROM SYS.ALL_TAB_COLUMNS
并且使用 ColumnReader
类提取信息。
表上约束的属性
DBTool 可用于快速获取表或视图的详细信息。
或者一个函数及其结果和参数
参数的详细信息对 DBA 和开发人员都很有意义。
设置一个简单的项目
在我们讨论 DBTool 的输出之前,我们需要创建一个简单的项目,我们将用它来生成该输出。以下是我们即将要处理的两个表:
create table SCOTT.DEPT
(
DEPTNO NUMBER(2) not null,
DNAME VARCHAR2(14),
LOC VARCHAR2(13)
);
alter table SCOTT.DEPT
add constraint PK_DEPT primary key (DEPTNO);
create table SCOTT.EMP
(
EMPNO NUMBER(4) not null,
ENAME VARCHAR2(10),
JOB VARCHAR2(9),
MGR NUMBER(4),
HIREDATE DATE,
SAL NUMBER(7,2),
COMM NUMBER(7,2),
DEPTNO NUMBER(2)
);
alter table SCOTT.EMP
add constraint PK_EMP primary key (EMPNO);
alter table SCOTT.EMP
add constraint FK_DEPTNO foreign key (DEPTNO)
references SCOTT.DEPT (DEPTNO);
两个表都有主键,并且它们之间存在一对多的关系。
首先要做的是将这两个表添加到当前项目中。
完成后,我们就有一个包含这两个表的简单项目。
现在让我们看一下项目的属性
目前,我们将接受项目的默认设置,但很高兴知道它们是可以更改的。
低级别的 Accessor 和 Reader 类
Accessor
和 Reader
类是实现对数据库基本 CRUD 操作的类。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Reflection;
using Oracle.DataAccess.Client;
using Oracle.DataAccess.Types;
using Harlinn.Oracle.DBTool.Common;
using Harlinn.Oracle.DBTool.Types;
namespace Harlinn.Oracle.DBTool.DB
{
关于生成的访问器类,首先值得注意的一点是,该类被标记了 DataObject
[^] 属性,因此它可以与 ASP.NET 的 ObjectDataSource
[^] 一起使用。
[DataObject]
public partial class DeptElementAccessor : Accessor
{
生成的代码使用 Log4Net 来记录异常。
private static readonly log4net.ILog sfLog =
log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private static void LogException(Exception exc, MethodBase method)
{
Logger.LogException(sfLog, exc, method);
}
public const string SCHEMA = "SCOTT";
public const string TABLE = "DEPT";
public const string DB_QUALIFIED_NAME = "SCOTT.DEPT";
CreateDataTable()
方法创建一个空的 DataTable
[^]。有很多现成的代码可以处理 DataTable
,所以这个方法通常很方便。
public static DataTable CreateDataTable()
{
try
{
DataTable result = new DataTable();
DataColumn deptnoDataColumn = new DataColumn( "DEPTNO", typeof(short) );
deptnoDataColumn.AllowDBNull = false;
result.Columns.Add(deptnoDataColumn);
DataColumn dnameDataColumn = new DataColumn( "DNAME", typeof(string) );
dnameDataColumn.AllowDBNull = true;
result.Columns.Add(dnameDataColumn);
DataColumn locDataColumn = new DataColumn( "LOC", typeof(string) );
locDataColumn.AllowDBNull = true;
result.Columns.Add(locDataColumn);
DataColumn[] keys = new DataColumn[1];
keys[0] = deptnoDataColumn;
result.PrimaryKey = keys;
return result;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
CreateDataTable
的这个重载方法接受一个 List<DeptElementData>
,并创建一个包含这些信息的 DataTable
。
public static DataTable CreateDataTable( List<DeptElementData> elements )
{
try
{
DataTable result = CreateDataTable();
foreach(DeptElementData element in elements)
{
object deptno = element.Deptno;
object dname;
if( element.Dname != null )
{
dname = element.Dname;
}
else
{
dname = null;
}
object loc;
if( element.Loc != null )
{
loc = element.Loc;
}
else
{
loc = null;
}
result.Rows.Add(deptno,dname,loc );
}
return result;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
WriteToServer
接受一个 DataTable
并使用 OracleBulkCopy
将数据写入数据库,当您有大量记录时,这非常高效。
public static void WriteToServer( OracleConnection oracleConnection,
string qualifiedDBName, DataTable dataTable )
{
try
{
using ( OracleBulkCopy bulkCopy = new OracleBulkCopy( oracleConnection ) )
{
bulkCopy.DestinationTableName = qualifiedDBName;
bulkCopy.WriteToServer( dataTable );
}
}
catch ( Exception exc )
{
LogException( exc, MethodBase.GetCurrentMethod( ) );
throw;
}
}
GetAll
是用于从数据库检索所有记录的默认 DataObjectMethod
。
[DataObjectMethod(DataObjectMethodType.Select,true)]
public static List<DeptElementData> GetAll( )
{
try
{
string qualifiedDBName = DB_QUALIFIED_NAME;
List<DeptElementData> result = new List<DeptElementData>( );
它使用 DeptElementReader
,这是 DEPT 表的读取器实现。
DeptElementReader elementReader = new DeptElementReader( qualifiedDBName );
using( elementReader )
{
while( elementReader.Read( ) )
{
DeptElementData element = elementReader.Dept;
result.Add(element);
}
}
return result;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
[DataObjectMethod(DataObjectMethodType.Select,false)]
public static List<DeptElementData> GetAll( OracleConnection oracleConnection )
{
try
{
string qualifiedDBName = DB_QUALIFIED_NAME;
List<DeptElementData> result = new List<DeptElementData>( );
DeptElementReader elementReader =
new DeptElementReader( oracleConnection, qualifiedDBName );
using( elementReader )
{
while( elementReader.Read( ) )
{
DeptElementData element = elementReader.Dept;
result.Add(element);
}
}
return result;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
GetKeyedCollection
检索一个 KeyedDeptCollection
,它是一个 KeyedCollection<TKey, TItem>
[^],其 TKey
与 DEPT 表的主键匹配,TItem
的类型为 DeptElementData
。
public static KeyedDeptCollection GetKeyedCollection( )
{
try
{
string qualifiedDBName = DB_QUALIFIED_NAME;
KeyedDeptCollection result = new KeyedDeptCollection( );
DeptElementReader elementReader = new DeptElementReader( qualifiedDBName );
using( elementReader )
{
while( elementReader.Read( ) )
{
DeptElementData element = elementReader.Dept;
result.Add(element);
}
}
return result;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
GetByDeptno
检索单个 DeptElementData
对象。
[DataObjectMethod(DataObjectMethodType.Select,false)]
public static DeptElementData GetByDeptno( short deptno )
{
try
{
string qualifiedDBName = DB_QUALIFIED_NAME;
DeptElementData result = null;
DeptElementReader elementReader =
DeptElementReader.CreateReaderByDeptno( qualifiedDBName,deptno );
using( elementReader )
{
if( elementReader.Read( ) )
{
DeptElementData element = elementReader.Dept;
result = element;
}
}
return result;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
[DataObjectMethod(DataObjectMethodType.Select,false)]
public static DeptElementData GetByDeptno(
OracleConnection oracleConnection, short deptno )
{
try
{
string qualifiedDBName = DB_QUALIFIED_NAME;
DeptElementData result = null;
DeptElementReader elementReader =
DeptElementReader.CreateReaderByDeptno( oracleConnection ,
qualifiedDBName,deptno );
using( elementReader )
{
if( elementReader.Read( ) )
{
DeptElementData element = elementReader.Dept;
result = element;
}
}
return result;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
public static int Insert( DeptElementData element )
{
try
{
DeptElementData result = null;
int recordsInserted = Insert( GetConnection(), element, out result );
return recordsInserted;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
public static int Insert( DeptElementData element, out DeptElementData result )
{
try
{
int recordsInserted = Insert( GetConnection(), element, out result );
return recordsInserted;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
此 Insert 的重载执行实际操作。
public static int Insert( OracleConnection oracleConnection,
DeptElementData element, out DeptElementData result )
{
try
{
string qualifiedDBName = DB_QUALIFIED_NAME;
int recordsInserted = 0;
result = null;
OracleCommand oracleCommand = oracleConnection.CreateCommand();
using (oracleCommand)
{
oracleCommand.BindByName = true;
string insertSQLStatement = "INSERT INTO {0}(DEPTNO,DNAME,LOC) " +
" VALUES(:deptno_, :dname_, :loc_)";
string finalInsertSQLStatement =
string.Format(insertSQLStatement,qualifiedDBName);
oracleCommand.CommandText = finalInsertSQLStatement;
值得注意的是,所有数据都通过参数传递给数据库驱动程序,这有助于防止 SQL 注入。
OracleParameter deptnoParameter =
oracleCommand.Parameters.Add(new OracleParameter( ":deptno_",
OracleDbType.Int16 ));
deptnoParameter.Value = element.Deptno;
OracleParameter dnameParameter =
oracleCommand.Parameters.Add(new OracleParameter( ":dname_",
OracleDbType.Varchar2 ));
以下是生成的代码处理可空列的方式。
if( element.Dname != null )
{
dnameParameter.Value = element.Dname;
}
else
{
dnameParameter.IsNullable = true;
dnameParameter.Value = null;
}
OracleParameter locParameter =
oracleCommand.Parameters.Add(new OracleParameter( ":loc_",
OracleDbType.Varchar2 ));
if( element.Loc != null )
{
locParameter.Value = element.Loc;
}
else
{
locParameter.IsNullable = true;
locParameter.Value = null;
}
此时所有参数都已分配,因此我们将执行权交给 Oracle 数据库。
recordsInserted = oracleCommand.ExecuteNonQuery();
if( recordsInserted != 0)
{
result = element;
}
}
return recordsInserted;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
如果我们在项目中为 SCOTT.DEPT 分配了一个 SEQUENCE
,那将会对生成的插入操作代码产生一些微小的变化,从 SQL 语句开始:
string insertSQLStatement = "INSERT INTO {0}(DEPTNO,DNAME,LOC) " +
" VALUES(DEPTNO_SEQ.NEXTVAL, :dname_, :loc_)" +
" RETURNING DEPTNO INTO :deptno_";
为了使其按预期工作,我们需要更改 :deptno_
参数的方向。
deptnoParameter.Direction = ParameterDirection.Output;
这样我们就可以像这样检索生成的值:
recordsInserted = oracleCommand.ExecuteNonQuery();
if( recordsInserted != 0)
{
result = element;
result.Deptno = deptnoParameter.Value;
}
public static void Insert( List<DeptElementData> elements )
{
try
{
Insert( GetConnection(), elements);
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
public static void Insert( OracleConnection oracleConnection,
List<DeptElementData> elements )
{
try
{
string qualifiedDBName = DB_QUALIFIED_NAME;
DataTable dataTable = CreateDataTable( elements );
using (dataTable)
{
WriteToServer(oracleConnection,qualifiedDBName,dataTable);
}
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
public static int Update( DeptElementData element )
{
try
{
DeptElementData result = null;
int recordsUpdated = Update(GetConnection(), element, out result);
return recordsUpdated;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
public static int Update( DeptElementData element, out DeptElementData result )
{
try
{
int recordsUpdated = Update(GetConnection(), element, out result);
return recordsUpdated;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
Update
方法的工作方式与 Insert
方法大致相同。
public static int Update( OracleConnection oracleConnection, DeptElementData element,
out DeptElementData result )
{
try
{
string qualifiedDBName = DB_QUALIFIED_NAME;
int recordsUpdated = 0;
result = null;
OracleCommand oracleCommand = oracleConnection.CreateCommand();
using (oracleCommand)
{
oracleCommand.BindByName = true;
string updateSQLStatement = "UPDATE {0} SET " +
"DNAME = :dname_ , " +
"LOC = :loc_ " +
" WHERE " +
"(DEPTNO = :deptno_) ";
string finalUpdateSQLStatement = string.Format(updateSQLStatement,qualifiedDBName);
oracleCommand.CommandText = finalUpdateSQLStatement;
OracleParameter deptnoParameter =
oracleCommand.Parameters.Add(new OracleParameter( ":deptno_",
OracleDbType.Int16 ));
deptnoParameter.Value = element.Deptno;
OracleParameter dnameParameter =
oracleCommand.Parameters.Add(new OracleParameter( ":dname_",
OracleDbType.Varchar2 ));
if( element.Dname != null )
{
dnameParameter.Value = element.Dname;
}
else
{
dnameParameter.IsNullable = true;
dnameParameter.Value = null;
}
OracleParameter locParameter =
oracleCommand.Parameters.Add(new OracleParameter( ":loc_",
OracleDbType.Varchar2 ));
if( element.Loc != null )
{
locParameter.Value = element.Loc;
}
else
{
locParameter.IsNullable = true;
locParameter.Value = null;
}
recordsUpdated = oracleCommand.ExecuteNonQuery();
if( recordsUpdated != 0)
{
result = element;
}
else
{
DeptElementReader reader =
DeptElementReader.CreateReaderByDeptno(oracleConnection,
qualifiedDBName,element.Deptno);
using (reader)
{
result = element;
}
}
}
return recordsUpdated;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
当对表启用并发性时,Update 操作会略有改变,这是通过检查用于乐观锁定的字段的 Concurrency
属性来完成的。
从 SQL 语句开始:
string updateSQLStatement = "UPDATE {0} SET " +
"PROCESS = :process_ , " +
"NAME = :name_ , " +
"OPTIMISTIC_LOCK = OPTIMISTIC_LOCK + 1, " +
"DESCRIPTION = :description_ , " +
"COMMENTS = :comments_ " +
" WHERE " +
"(ID = :id_) AND " +
"(OPTIMISTIC_LOCK = :optimisticLock_) " +
" RETURNING OPTIMISTIC_LOCK INTO :optimisticLock_";
启用乐观锁定时,DBTool 会生成代码,将 ElementState
属性设置为 ElementState.ConcurrencyConflict
或 ElementState.Deleted
。当 ElementState
设置为 ElementState.ConcurrencyConflict
时,冲突的数据会被赋给结果的 ConcurrencyConflictElement
属性。
recordsUpdated = oracleCommand.ExecuteNonQuery();
if( recordsUpdated != 0)
{
result = element;
result.OptimisticLock = ((OracleDecimal)optimisticLockParameter.Value).ToInt64();
result.ElementState = ElementState.Stored;
}
else
{
ModelElementReader reader =
ModelElementReader.CreateReaderById(oracleConnection, qualifiedDBName,element.Id);
using (reader)
{
result = element;
if (reader.Read())
{
result.ElementState = ElementState.ConcurrencyConflict;
result.ConcurrencyConflictElement = reader.Model;
}
else
{
result.ElementState = ElementState.Deleted;
}
}
}
当启用乐观锁定时,Delete 操作的实现会以类似的方式改变。现在,回到我们对 DEPT 表生成的代码的演练。
public static int Delete( short deptno )
{
try
{
int result = Delete( GetConnection(), deptno );
return result;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
最后我们有 Delete
方法。
private const string DELETE_STATEMENT = "DELETE FROM {0} WHERE DEPTNO = :deptno";
public static int Delete( OracleConnection oracleConnection, short deptno )
{
try
{
string qualifiedDBName = DB_QUALIFIED_NAME;
int result = 0;
OracleCommand oracleCommand = oracleConnection.CreateCommand();
using (oracleCommand)
{
string deleteStatement = string.Format(DELETE_STATEMENT, qualifiedDBName);
oracleCommand.CommandText = deleteStatement;
OracleParameter deptnoParameter =
oracleCommand.Parameters.Add(new OracleParameter( ":deptno",
OracleDbType.Int16 ));
deptnoParameter.Value = deptno;
result = oracleCommand.ExecuteNonQuery();
}
return result;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
}
}
以上是 DEPT 表的访问器类,我之所以包含全部内容,是因为我觉得它充分说明了代码生成器的一般价值。这是一个非常小的项目,对于一个正常的项目来说,手动映射参数是一个既耗时又容易出错的过程。
Reader 是 IDataReader
[^] 接口的一个实现,它将所有操作委托给一个 OracleDataReader
对象,同时使用 Log4Net 记录数据检索期间发生的任何异常。
DeptElementReader
是 DEPT 表的读取器。
namespace Harlinn.Oracle.DBTool.DB
{
public partial class DeptElementReader : Reader
{
再次使用 log4Net 来记录执行期间可能发生的任何异常。
private static readonly log4net.ILog sfLog =
log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private static void LogException(Exception exc, MethodBase method)
{
Harlinn.Oracle.DBTool.Common.Logger.LogException(sfLog, exc, method);
}
public const string DEFAULT_QUALIFIED_DBNAME = "SCOTT.DEPT";
public const string FULL_SELECT = "SELECT DEPTNO,DNAME,LOC FROM {0}";
public const string KEY_FIELDS = "DEPTNO";
public const int DEPTNO = 0;
public const int DNAME = 1;
public const int LOC = 2;
各种构造函数调用 CreateReader
工厂函数来为 DeptElementReader
创建 OracleDataReader
对象。
public DeptElementReader ( )
: base( CreateReader( DEFAULT_QUALIFIED_DBNAME ) )
{
}
public DeptElementReader ( string qualifiedDBName )
: base( CreateReader( qualifiedDBName ) )
{
}
public DeptElementReader ( OracleConnection oracleConnection )
: base( CreateReader( oracleConnection ) )
{
}
public DeptElementReader ( OracleConnection oracleConnection, string qualifiedDBName )
: base( CreateReader( oracleConnection, qualifiedDBName ) )
{
}
public DeptElementReader ( OracleDataReader reader )
: base( reader )
{
}
用于创建 OracleDataReader
对象的工厂函数。
private static OracleDataReader CreateReader( string qualifiedDBName )
{
try
{
OracleConnection oracleConnection = GetConnection();
OracleDataReader result = CreateReader(oracleConnection,qualifiedDBName);
return result;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
private static OracleDataReader CreateReader( OracleConnection oracleConnection )
{
try
{
OracleDataReader result =
CreateReader(oracleConnection,DEFAULT_QUALIFIED_DBNAME);
return result;
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
private static OracleDataReader CreateReader( OracleConnection oracleConnection,
string qualifiedDBName )
{
try
{
string sql = string.Format(FULL_SELECT, qualifiedDBName) +
" ORDER BY " + KEY_FIELDS;
OracleCommand oracleCommand = oracleConnection.CreateCommand();
using (oracleCommand)
{
oracleCommand.CommandText = sql;
OracleDataReader result =
oracleCommand.ExecuteReader(CommandBehavior.SingleResult);
return result;
}
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
用于创建 DeptElementReader
对象的工厂函数。
public static DeptElementReader CreateReaderByDeptno( short deptno)
{
DeptElementReader result =
CreateReaderByDeptno( GetConnection(), DEFAULT_QUALIFIED_DBNAME,deptno);
return result;
}
public static DeptElementReader CreateReaderByDeptno( OracleConnection oracleConnection ,
short deptno)
{
DeptElementReader result =
CreateReaderByDeptno( oracleConnection, DEFAULT_QUALIFIED_DBNAME,deptno);
return result;
}
public static DeptElementReader CreateReaderByDeptno( string qualifiedDBName, short deptno)
{
DeptElementReader result =
CreateReaderByDeptno( GetConnection(), qualifiedDBName,deptno);
return result;
}
public static DeptElementReader CreateReaderByDeptno( OracleConnection oracleConnection ,
string qualifiedDBName, short deptno)
{
try
{
string fullSelect = string.Format(FULL_SELECT, qualifiedDBName);
OracleCommand oracleCommand = oracleConnection.CreateCommand();
using (oracleCommand)
{
oracleCommand.BindByName = true;
string queryFilter = " WHERE DEPTNO = :deptno";
string selectStatement = fullSelect + queryFilter;
oracleCommand.CommandText = selectStatement;
OracleParameter deptnoParameter =
oracleCommand.Parameters.Add(new OracleParameter( ":deptno",
OracleDbType.Int16 ));
deptnoParameter.Value = deptno;
OracleDataReader result =
oracleCommand.ExecuteReader(CommandBehavior.SingleResult);
return new DeptElementReader( result );
}
}
catch (Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
以下属性允许我们以非常易读的方式访问当前行的字段。由于 Deptno 不是一个可为空的列,因此没有理由测试该列是否为空。
public short Deptno
{
get
{
try
{
short result = GetInt16(DEPTNO);
return result;
}
catch(Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
}
DNAME 和 LOC 都是 DEPT 表的可空列,因此生成的代码会检查这一点,并只尝试读取之前已设置为非空值的值。
public string Dname
{
get
{
try
{
if(IsDBNull(DNAME) == false)
{
string result = GetString(DNAME);
return result;
}
return null;
}
catch(Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
}
public string Loc
{
get
{
try
{
if(IsDBNull(LOC) == false)
{
string result = GetString(LOC);
return result;
}
return null;
}
catch(Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
}
最后是 Dept 属性,它从 DEPT 表的一行中读取所有信息,并将其转换为一个 DeptElementData
对象。
public DeptElementData Dept
{
get
{
try
{
DeptElementData result = new DeptElementData(Deptno,Dname,Loc );
return result;
}
catch(Exception exc)
{
LogException(exc, MethodBase.GetCurrentMethod());
throw;
}
}
}
}
}
通用类和接口
DeptElementData
是一个类,我们可以用它来传输 DEPT 表中单行所包含的信息。它可以使用 DataContractSerializer
[^]、BinaryFormatter
[^] 和 SoapFormatter
[^] 进行序列化。
namespace Harlinn.Oracle.DBTool.Types
{
[DataContract(Namespace = Constants.Namespace)]
[Serializable]
public class DeptElementData : ElementBase
{
private short deptno;
private string dname;
private string loc;
public DeptElementData( )
{
}
public DeptElementData( short deptno, string dname, string loc )
{
this.deptno = deptno;
this.dname = dname;
this.loc = loc;
}
public override ElementType ElementType
{
get
{
return ElementType.Dept;
}
}
AssignTo
从 DeptElementData
类的另一个实例复制数据。
public override void AssignTo(ElementBase destination)
{
DeptElementData destinationElement = (DeptElementData)destination;
destinationElement.deptno = this.deptno;
destinationElement.dname = this.dname;
destinationElement.loc = this.loc;
}
CompareTo
将一个 DeptElementData
的实例与该类的另一个实例进行比较。
public override int CompareTo(ElementBase other)
{
DeptElementData otherElement = (DeptElementData)other;
int result = CompareHelper.Compare( otherElement.deptno , this.deptno);
if( result != 0)
{
return result;
}
result = CompareHelper.Compare( otherElement.dname , this.dname);
if( result != 0)
{
return result;
}
result = CompareHelper.Compare( otherElement.loc , this.loc);
return result;
}
其余的是预期的属性,DEPT 表的每一列都有一个对应的属性。
[DataMember(EmitDefaultValue=false)]
public short Deptno
{
get
{
return deptno;
}
set
{
this.deptno = value;
}
}
[DataMember(EmitDefaultValue=false)]
public string Dname
{
get
{
return dname;
}
set
{
this.dname = value;
}
}
[DataMember(EmitDefaultValue=false)]
public string Loc
{
get
{
return loc;
}
set
{
this.loc = value;
}
}
IDataProvider
IDataProvider
声明了可以对这两个表执行的操作。
namespace Harlinn.Oracle.DBTool.Types
{
public interface IDataProvider
{
// =========================================================================
// Type : Emp
// Table : SCOTT.EMP
// =========================================================================
List<EmpElementData> GetAllEmps();
EmpElementData GetEmpByEmpno( short empno );
List<EmpElementData> GetEmpByDeptno( short? deptno );
EmpElementData InsertEmp( Guid clientId, EmpElementData element );
void InsertEmpList( Guid clientId, List<EmpElementData> elements );
EmpElementData UpdateEmp( Guid clientId, EmpElementData element );
int DeleteEmp( Guid clientId, short empno );
// =========================================================================
// Type : Dept
// Table : SCOTT.DEPT
// =========================================================================
List<DeptElementData> GetAllDepts();
DeptElementData GetDeptByDeptno( short deptno );
DeptElementData InsertDept( Guid clientId, DeptElementData element );
void InsertDeptList( Guid clientId, List<DeptElementData> elements );
DeptElementData UpdateDept( Guid clientId, DeptElementData element );
int DeleteDept( Guid clientId, short deptno );
}
}
IDataObserver
IDataObserver
声明了一个接口,允许实现该接口的类在系统中发生某些变化时得到通知。
namespace Harlinn.Oracle.DBTool.Types
{
public interface IDataObserver
{
void OnEmpInserted(object sender, OnEmpInsertedEventArgs eventArgs );
void OnEmpChanged(object sender, OnEmpChangedEventArgs eventArgs );
void OnEmpDeleted(object sender, OnEmpDeletedEventArgs eventArgs );
void OnDeptInserted(object sender, OnDeptInsertedEventArgs eventArgs );
void OnDeptChanged(object sender, OnDeptChangedEventArgs eventArgs );
void OnDeptDeleted(object sender, OnDeptDeletedEventArgs eventArgs );
}
}
IDataContextProvider
IDataContextProvider
扩展了 IDataProvider
接口,添加了事件,允许其他类在系统中发生变化时得到通知。
namespace Harlinn.Oracle.DBTool.Types
{
public interface IDataContextProvider : IDataProvider
{
event OnEmpInsertedDelegate OnEmpInsertedEvent;
event OnEmpChangedDelegate OnEmpChangedEvent;
event OnEmpDeletedDelegate OnEmpDeletedEvent;
event OnDeptInsertedDelegate OnDeptInsertedEvent;
event OnDeptChangedDelegate OnDeptChangedEvent;
event OnDeptDeletedDelegate OnDeptDeletedEvent;
}
}
服务器端类
这些是通常会放在服务器上的东西,但它们也可以用来实现直接与 Oracle 连接的胖客户端。
ServiceImplementation
ServiceImplementation
实现了 IDataContextProvider
接口,使用各自的访问器和读取器类来实现对 EMP 和 DEPT 表的 CRUD 功能。
ServiceImplementation
旨在用作单例,其中静态的 Implementation 属性返回公共实例。当以这种方式使用时,它可以作为系统的中心枢纽。
namespace Harlinn.Oracle.DBTool.Implementation
{
public partial class ServiceImplementation : IDataContextProvider
{
private static readonly log4net.ILog sfLog =
log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private static void LogException(Exception exc, MethodBase method)
{
Logger.LogException(sfLog, exc, method);
}
static void Entering( MethodBase method )
{
Logger.Entering(sfLog, method);
}
static void Leaving( MethodBase method )
{
Logger.Leaving(sfLog, method);
}
private static readonly object synchObject = new object();
private static ServiceImplementation implementation;
public static ServiceImplementation Implementation
{
get
{
if (implementation == null)
{
lock (synchObject)
{
if (implementation == null)
{
implementation = new ServiceImplementation();
}
}
}
return implementation;
}
}
// the rest of the code is omitted for brevity
}
}
DataObserver
DataObserver
实现了 IDataObserver
接口,并与一个实现了 IDataContextProvider
接口的类实例一起工作。
DataService
DataService
是 WCF 服务的实现。
namespace Harlinn.Oracle.DBTool.Implementation
{
DataService
使用 DataServiceDataContextProvider
来获取实现 IDataContextProvider
接口的对象。默认情况下,它将返回 ServiceImplementation
单例。
public partial class DataServiceDataContextProvider
{
private static IDataContextProvider dataContextProvider;
public static IDataContextProvider DataContextProvider
{
get
{
if( dataContextProvider == null )
{
dataContextProvider = ServiceImplementation.Implementation;
}
return dataContextProvider;
}
set
{
dataContextProvider = value;
}
}
}
DataService
是一个面向会话的 WCF 服务,它派生自 DataObserver
。
[ServiceContract(SessionMode = SessionMode.Required, Namespace = Constants.Namespace)]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public partial class DataService : DataObserver, IDisposable
{
private static readonly log4net.ILog sfLog =
log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private static void LogException(Exception exc, MethodBase method)
{
Logger.LogException(sfLog, exc, method);
}
// code omitted for brevity
public DataService()
{
clientId = Guid.NewGuid();
IDataContextProvider dataContextProvider =
DataServiceDataContextProvider.DataContextProvider;
Attach(dataContextProvider);
}
// code omitted for brevity
protected IDisposable CreateOperationContext(MethodBase method)
{
Harlinn.Oracle.DBTool.Common.OperationContext result =
new Harlinn.Oracle.DBTool.Common.OperationContext(method);
return result;
}
WCF 客户端必须通过调用 Connect()
来启动一个会话,然后才能调用 WCF 服务实现的其他方法。
[OperationContract(IsOneWay = false, IsInitiating = true, IsTerminating = false)]
[PrincipalPermission(SecurityAction.Demand, Role = DATASERVICE_USERS_GROUP)]
[OperationBehavior(Impersonation = ImpersonationOption.Required)]
public Guid Connect()
{
// code omitted for brevity
}
WCF 客户端调用 Disconnect()
来终止会话。
[OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = true)]
[PrincipalPermission(SecurityAction.Demand, Role = DATASERVICE_USERS_GROUP)]
[OperationBehavior(Impersonation = ImpersonationOption.Required)]
public void Disconnect()
{
// code omitted for brevity
}
// code omitted for brevity
GetAllEmps
是 WCF 服务公开的一个典型可调用方法的实现。operationContext
确保在调用期间我们与 Oracle 有一个有效的连接。该方法要求调用者是指定角色的成员——默认为“Administrators”,并模拟调用者。
[OperationContract(IsOneWay = false, IsInitiating = false, IsTerminating = false)]
[PrincipalPermission(SecurityAction.Demand, Role = DATASERVICE_USERS_GROUP)]
[OperationBehavior(Impersonation = ImpersonationOption.Required)]
public List<EmpElementData> GetAllEmps()
{
List<EmpElementData> result = null;
MethodBase currentMethod = MethodBase.GetCurrentMethod();
Entering(currentMethod);
try
{
try
{
IDisposable operationContext = CreateOperationContext(currentMethod);
using(operationContext)
{
result = DataContextProvider.GetAllEmps();
}
}
catch (Exception exc)
{
LogException(exc, currentMethod );
ConvertExceptionToFaultAndThrow( exc, currentMethod );
}
}
finally
{
Leaving(currentMethod);
}
return result;
}
结束语
这涵盖了与服务器端相关的一些基础知识,如果你读到这里,你可能是一个非常了不起的人。要写一些关于 CRUD 的激动人心的东西很难,即使这些东西非常有用。好吧,下一篇文章将介绍生成的代码的客户端部分。
构建说明
在构建此项目之前,您需要安装一些 NuGet 包。
由于我找不到 WPF Property Grid[^] 的 NuGet 包,我已将该项目包含在下载文件中。
您还需要 Oracle Developer Tools for Visual Studio[^],以及一个 Oracle 客户端或完整的数据库安装[^]。
您很可能还需要更新对 Oracle.DataAccess 程序集的引用。
您还需要更新 App.config 文件中的 OracleConnection
连接字符串,以提供与您的设置相匹配的正确用户ID、密码和其他设置。
深入阅读
-
John Hutchinson, Jon Whittle, Mark Rouncefield at School of Computing and Communications Lancaster University and Steinar Kristoffersen at Østfold University College and Møreforskning Molde AS: Empirical Assessment of MDE in Industry[^]
本文介绍了一项为期十二个月的实证研究结果,其长期目标是根据行业证据为模型驱动工程提供指导方针。
-
Martin Fowler, Rebecca Parsons Domain Specific Languages[^]
对领域特定语言的精彩介绍。
-
Juha-Pekka Tolvanen - 2014年代码生成大会主题演讲:The business case of modeling and generators[^]
Tolvanen 提出了一个有趣的观点:当公司开发自己的定制建模方法、语言和工具,而不是简单地应用现成的解决方案时,建模最有可能成功。
历史
- 2013年3月15日 - 首次发布
- 2013年3月16日 - 一些界面美化
- 2013年3月17日 - 许多新功能和一些错误修复
- 2013年3月19日 - 添加了示例项目
- Harlinn.Oracle.DBTool.Example - 包含几乎所有内容
- Harlinn.Oracle.DBTool.Example.Service - 包含 DataService 类
- Harlinn.Oracle.DBTool.Example.Client - 包含 DataClient 类
- Harlinn.Oracle.DBTool.Example.Service.Win - 托管生成的 WCF 服务
这些项目基于一个数据库模式,该模式可以通过执行位于 SQL 文件夹中的 CreateDatabase.sql 文件中的 SQL 命令来创建。
修复了与 Oracle 类型的自动魔术转换相关的多个错误。
- 2013年3月21日 - 性能改进和对大多数缺失的 Oracle 对象类型的初步支持。
如果没有特定类型的元素,树将不会显示该类型的节点。打开 SYS 用户需要一点点时间,因为 Java 类的数量太多。
增加了提取以下类型定义的功能:
- 集群
- 函数
- 目录
- 库
- 运算符
- 包
- Procedure
- 序列
- 同义词
- 表格
- 触发器
- 类型
- 视图
- 2013年3月28日 - 增加了关于 DBTool 如何从数据库中检索部分元数据的描述。
-
2013年3月29日 - 添加了查看对象之间关系的功能。这意味着 DBTool 将显示视图引用的表和视图。
-
2013年4月4日 - 添加了代码,以根据树视图中当前选定的元素来启用或禁用上下文菜单项。
-
2013年5月1日 - 代码进行重大重构。原始程序仍然包含在内,但我正在重构代码,以便能够支持多个数据库服务器,同时创建一组可重用的库,用于处理数据库服务器公开的元数据。
- 2013年7月16日 - 现在使用 AvalonDock.2.0.2000、AvalonEdit.4.3.1.9430 和 log4net.2.0.0。
-
2013年7月25日 - 向 Harlinn.Oracle 添加了新类,这是一个旨在提供对 Oracle.DataAccess 程序集提供的大多数功能的访问,同时仍然动态加载 Oracle.DataAccess 程序集的库。将来 DBTool 将不再引用 Oracle.DataAccess 程序集,它将使用连接字符串中的 providerName 进行加载。