65.9K
CodeProject 正在变化。 阅读更多。
Home

Reflection Studio - 第 4 部分 - 数据库:Schema、Provider 和 Plugin、控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (7投票s)

2010年9月22日

GPL3

10分钟阅读

viewsIcon

40183

downloadIcon

1

Reflection Studio 是一个用 C# 在 WPF 4.0 下编写的“开发人员”应用程序,用于处理程序集、数据库、性能和代码生成。

引言

这是我关于 Reflection Studio 文章的第四部分。在本章中,我将描述数据库模块:表示数据库实体的 Schema 对象,插件系统,以 SQL Server 200x 为例的 Provider,以及所有用户控件。

Reflection Studio 托管在 http://reflectionstudio.codeplex.com/。它完全使用 C# 在 NET/WPF 平台上编写。我最近升级到 Visual Studio 2010 和 NET 4。请查看 CodePlex 项目,因为它太庞大了,无法详细描述所有内容。下面是应用程序的屏幕截图。

ReflectionStudio

文章内容

如前所述,主题非常庞大,我将(尝试)分几个部分撰写本文

第 4 部分 - 数据库

此模块主要用于发现数据库内容。它提供以下功能

  • 探索数据库:从数据库到列和数据类型
  • 脚本:Provider 可以为 `create`/`drop`/`alter` + `select`、`update`、`insert` 和 `delete` 创建数据库脚本。
  • 查询:允许针对数据库运行查询
  • 检查:允许检查命名和性能规则

4.1 Schema 对象

存储在 `ReflectionStudio.Core.Database.Schema` 命名空间中的 Schema 对象是实体类,表示描述数据库所需的所有元素。下面是类图和依赖图。该命名空间包含

  • `SchemaObjectBase` 是通用的 `abstract` 类
  • `DataObjectBase` 是表示数据的通用 `abstract` 类
  • `DatabaseSchema` 包含连接字符串和 Provider 等信息。它表示一个单一的数据库。
  • `ITabularObjectBase` 是具有行结果的实体的抽象,例如 Table 和 View
  • `TableSchema` 和 `ViewSchema` 表示视图和表
  • `ColumnBaseSchema`、`TableColumnSchema` 和 `ViewColunmSchema` 表示列
  • `TableKeySchema`、`PrimaryKeySchema`、`IndexSchema` 表示附加的和众所周知的对象 FK、PK、IX
  • `CommandSchema`、`ParameterSchema`、`CommandResultSchema` 表示存储过程
  • `ExtendedProperty` 用于保存不同数据库不常见的属性;例如“Description”或“Comments”

如下所示,所有 Schema 对象之间的依赖关系很复杂,处理起来并不总是那么容易。我不会描述每个实体及其属性。请查看代码,它就像数据库对象一样。

Schema objects dependencies

4.2 插件系统和 Provider

命名空间 `ReflectionStudio.Core.Database` 包含 Provider 定义和一个简单的插件系统,该系统允许通过反射加载任何符合 Provider 类型的程序集。

4.2.1 - Provider 接口

Provider Schema 由以下接口描述。如果您想编写一个 Provider,则必须实现第一个接口才能使程序集可加载。

  • `IDbSchemaProvider`:插件系统使用的主接口。它为我们提供数据库 Schema。
  • `IDbSourcePanel`:在创建新的 `DataSource` 时提供对话框面板的接口。
  • `IDbExecuteQuery`:适合 SQL 编辑器查询需求的接口。
  • `IDbWriter`:用于创建数据库对象脚本的接口。
  • `IDbVerifier`:用于检查命名和性能质量合规性的接口(开发中)。

此外,我们还有 `DataSource` 类,它表示一个连接。它持有可在恢复时使用的 Provider 和主要的数据库 Schema 对象:`DatabaseSchema`。

请注意,您不需要使用 `IDbSchemaProvider`,因为它会自动被 Schema 对象用于自动填充其属性。例如,`DatabaseSchema` 通过 Provider 返回表(参见下文),依此类推直到树的末端。

private List<TableSchema> _Tables;
[Browsable(false)]
public ReadOnlyCollection<TableSchema> Tables
{
	get
	{
		if (this._Tables == null)
		{
			this.Check();
			this._Tables = this.Provider.GetTables
				(this.ConnectionString, this);
		}
		return this._Tables.AsReadOnly();
	}
}

4.2.2 - 插件系统

插件系统由 `DatabaseService` 类实现。它特意保持简单,因为我(现在)不想实现一个复杂的插件系统,例如 Microsoft 提供的新系统,如此处所述。

该类有一个属性,如果为空,它将扫描文件夹以获取所有兼容的程序集,加载它们并为每个程序集创建一个 `IDbSchemaProvider` 对象。请注意,要加载程序集,其名称必须为 *xxx.Provider.dll*。

List<IDbSchemaProvider> _Providers;
public ReadOnlyCollection<IDbSchemaProvider> Providers
{
	get
	{
		if (_Providers == null)
			_Providers = LoadProviders();

		return _Providers.AsReadOnly();
	}
}
...

private List<IDbSchemaProvider> LoadProviders()
{
	List<IDbSchemaProvider> list = new List<IDbSchemaProvider>();

	List<String> fileProvider = new List<String>();
	foreach (string folder in this.ProviderSearchDirectories)
	{
		fileProvider.AddRange(Directory.GetFiles(folder, "*Provider.dll"));
	}

	foreach (string str2 in fileProvider)
	{
		try
		{
			foreach (Type type in Assembly.LoadFrom(str2).GetTypes())
			{
				if (type.GetInterface
				(typeof(IDbSchemaProvider).FullName) != null)
				{
					IDbSchemaProvider provider =
					(IDbSchemaProvider)Activator.CreateInstance
						(type);
					if (list.Where(p => p.Name ==
						provider.Name).Count() == 0)
					{
						list.Add(provider);
					}
				}
			}.....

当我们获取所有扫描目录中符合第一个要求(文件名)的所有文件后,我们加载每个程序集以检查是否有类型实现了 `IDbSchemaProvider` 接口。如果是这种情况,我们就会创建它并将其添加到 Provider 列表中。

4.2.3 - 助手

`QueryResourceManager` 是一个非常有用的类,它允许从 XML 嵌入式资源文件中检索查询。它可以存储在您的专用 Provider 中发现 Schema 所需的所有查询。XML 如下所示。每个查询都有 `Key` 和 `Version` 属性,因为您可以根据您正在使用的数据库服务器版本查询特定版本。

<?xml version="1.0" encoding="utf-8" ?>
<Queries>
	<ListOfQuery>
		<Query Key="Dependencies" Version="2008">
			<Content>
CREATE TABLE #tempdep (objid int NOT NULL, objtype smallint NOT NULL)

BEGIN TRANSACTION

INSERT INTO #tempdep
SELECT

在您的 Provider 中实例化一个 `QueryManager` 并按如下方式使用它

QueryManager _Queries = new QueryManager
	("ReflectionStudio.Core.Database.SQL2KProvider", "Queries.xml");

[...}

	string str = string.Format(_Queries.GetQuery("CommandText"),
			command.Database.Name, command.Name);

	using (SQLQueryHelper query = new SQLQueryHelper(connectionString))
	{
		SqlDataReader reader = query.ExecuteReader(str);

4.2.4 - SQL 2K Provider

此 Provider(用于兼容 Microsoft SQL Server 2000 到 2008 的所有版本)实现了前面描述的所有接口。它实际上处于草稿状态 - 我需要改进代码 - 但它满足了基本需求。进一步的描述将在下一次文章更新中提供。

Schema objects dependencies

4.2.5 - 其他 Provider

一个 ORACLE Provider 实际上正在由一个团队成员开发中,文档更新将很快发布。欢迎为项目做出贡献,但请通过订阅 CodePlex 并使用 TFS 来进行,因为管道中有很多改进。

4.3 - 相关用户界面

本章介绍您在 Reflection Studio 中可以找到的与数据库模块相关的用户界面元素。列表如下

  1. 数据库浏览器(左侧)
    • 新建、刷新和删除命令,用于操作 `DataSource`
    • `DataSource` 选择器,用于在不同数据库之间切换
    • 用于浏览 Schema 的 `treeview`
    • 用于访问命令的上下文菜单
  2. `QueryDocument`(中间)
    • 具有语法着色和可缩放功能的编辑器部分
    • 显示执行查询结果的结果窗格
    • 显示特定执行信息的状态栏
  3. 功能区选项卡(顶部)
    • SQL 查询命令
    • 当查询文档处于活动状态时显示的文本命令
  4. 属性浏览器(右侧)
    • 显示活动业务对象属性
    • `propertyGrid` 将重新开发
  5. Provider 选择和新建数据源对话框
  6. 依赖对话框
  7. `ProjectExplorer`(稍后)

4.3.1 - 树视图和数据库浏览器

允许通过 Microsoft 跟踪或调试系统中常见的函数(如错误、信息、详细信息)跟踪所有开发步骤。我通常使用如下模板,这在编写代码和测试时非常有用,因为您可以直接在 UI 中看到跟踪。

public bool Open( string fileName )
{
Tracer.Verbose("ProjectService:Open", "START");

try
{
Current = new ProjectEntity(fileName);
return LoadProject();
}
catch (Exception err)
{
Tracer.Error("ProjectService.Open", err);
return false;
}
finally
{
Tracer.Verbose("ProjectService:Open", "END");
}
}

4.3.2 - Provider 选择和数据源创建

在应用程序中,当我们响应数据源创建命令(由 DB 浏览器持有)时,我们首先检查列表中是否有 Provider...如果存在,我们显示 `ProviderChoiceDialog`,其 `DataContext` 设置为 Provider 列表(其余只是绑定和模板)。如果此对话框经过验证,我们使用其属性 `SelectedProvider` 来检索 `IDbSourcePanel` 控件,以便我们可以继续使用 `NewDatasourceDialog`。当我们设置其属性 `SourcePanel` 时,此对话框会将其内容更改为我们的新面板。如果成功,我们检索连接字符串属性,要求 `DatabaseService` 创建 `DataSource`,并且我们还更新工作区集合。

public void AddDataSourceCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
	e.Handled = true;

	//load the providers?
	if (Providers.Count > 0)
	{
		//display the dialog
		ProviderChoiceDialog providerDlg = new ProviderChoiceDialog();
		providerDlg.Owner = Application.Current.MainWindow;
		providerDlg.DataContext = Providers;

		//if ok, display the dialog from selected provider
		if (providerDlg.ShowDialog() == true)
		{
			//if ok, create the source
			IDbSourcePanel providerNewSourcePanel =
			providerDlg.SelectedProvider.GetSourcePanelInterface()
				as IDbSourcePanel;

			//display the dialog
			NewDataSourceDialog sourceDlg = new NewDataSourceDialog();
			sourceDlg.Owner = Application.Current.MainWindow;
			sourceDlg.SourcePanel = providerNewSourcePanel;

			if (sourceDlg.ShowDialog() == true)
			{
				// will add a new source to the workspace
				AddSource(providerNewSourcePanel.SourceName,
				providerDlg.SelectedProvider,
				providerNewSourcePanel.ConnectionString);
			}
		}
	}
	else [...]
}

下面是新的数据源对话框,它托管了来自 `SQL2KProvider` 的 Provider 面板。

4.3.3 - 功能区选项卡

SQL 查询功能区选项卡始终可见,并包含以下命令

  • 新建、刷新、删除和检查(`Datasource`)- 来自资源管理器的命令
  • 新建、输出、检查、执行、停止(`Query`)- 来自查询文档的命令
  • 稍后...

我们还有一个上下文选项卡,它与任何富文本文档(如查询和模板)一起出现。它允许格式化文本、剪贴板和 AvalonEdit 控件中包含的撤销/重做功能。

4.3.4 - 其他

应用程序提供一个与数据库模块相关的对话框。依赖对话框显示依赖于或被选定对象依赖的对象。Provider 只提供一个关系表,对话框必须以正确的方式处理该表以显示 ON 或 FROM 值。

4.4 - 文档

应用程序目前包含一个查询文档 - 但将来可能还会有其他文档,例如用于创建或逆向分析的设计器。

4.4.1 - 查询文档

查询文档基于 `ZoomDocument`,它派生自 AvalonDock 中的 `DocumentContent`。此用户控件包含一个 `SyntaxedTextEditor`,这是一个派生自 `ICSharpCode.AvalonEdit` 中包含的 `TextEditor` 的个性化类,用于支持模板和 SQL 语言语法着色。

除此之外,我们还有一个选项卡式窗格,用于以网格或文本形式显示结果,以及一个消息部分。在此之下,我有一个状态部分,用于显示当前数据源、查询返回的行数、执行时间和消息。这与应用程序状态栏有点冗余...

我们可以通过 `DocumentFactory` 类请求创建 `QueryDocument`(参见第 2 部分

internal QueryDocument CreateQueryEditor()
{
	DataSource ds = Data;
	if (ds != null)
	{
		return (QueryDocument)DocumentFactory.Instance.CreateDocument(
				DocumentFactory.Instance.SupportedDocuments.Find
				(p => p.DocumentContentType == typeof(QueryDocument)),
				new DocumentDataContext() { Entity = ds });
	}
	else
		return null;
}

`QueryDocument` 类提供 4 个命令

  1. 检查以控制 SQL 代码
  2. 执行以在当前窗口中启动 SQL 查询
  3. 停止(执行)以取消当前查询
  4. 输出模式以在网格和文本之间切换

检查 SQL 查询命令非常简单,它只是用 `SET PARSEONLY ON/OFF` 标记包围当前编辑器文本。它要求 Provider 执行它,当结果没有错误时,命令就成功了。

为了实现 SQL 查询执行,我希望它是异步的,但只有原生 SQL Server 类才提供 `BeginExecuteReader` 方法。所有 ADO 和其他驱动程序都是同步的。但这不符合我的 SQL Provider Schema 和需要适用于每个数据库引擎的插件系统。

所以我选择了一个折衷方案:执行可以是同步的——这意味着我们必须等待数据库查询完成执行,但我们需要能够取消数据检索。

从这个角度来看,我决定实现一个 `QueryContext` 来保存执行所需的信息,以及一个基于 `ReflectionStudio.Core.Helpers` 命名空间中的 `WorkerBase` 的 `QueryWorker`,它是一个管理后台工作线程的助手类。

如下所示,在文档的执行命令中,我们创建一个 `QueryContext` 并启动一个新线程,该线程运行 `QueryWorker` 的 `Execute` 方法。

public void ExecuteQueryCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
	e.Handled = true;

	//execute the thread that run the query on the database
         //as ExecuteReader is blocking
	try
	{
		//display the context options
		ShowResultPane();
		this.dataGridResult.Columns.Clear();

		//display the progress,
		EventDispatcher.Instance.RaiseStatus
			("Executing query !", StatusEventType.StartProgress);

		QueryContext context = new QueryContext()
		{
			UIDispatcher = this.Dispatcher,
			Command = SyntaxEditor.Text,
			Source = this.DataSource,
			StartTime = DateTime.Now
		};

		if (OutputMode == QueryOutputMode.Grid)
		{
			context.AddColumn = new QueryContext.AddColumnDelegate
					(GridAddColumnHandler);
			context.AddData = new QueryContext.AddRowDelegate
					(GridAddDataHandler);
		}
		else
		{
			context.AddColumn = new QueryContext.AddColumnDelegate
					(TextAddColumnHandler);
			context.AddData = new QueryContext.AddRowDelegate
					(TextAddDataHandler);
		}

		// Start the asynchronous operation.
		_Worker = new BackgroundWorker();
		_Worker.WorkerSupportsCancellation = true;
		_Worker.DoWork += new DoWorkEventHandler(bw_RequestWork);
		_Worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler
					(bw_RequestCompleted);
		_Worker.ProgressChanged += new ProgressChangedEventHandler
					(bw_RequestProgressChanged);

		_Worker.RunWorkerAsync(context);
	}
	catch (Exception all)
	{
	}
}

然后我们用这段代码启动线程。请注意结果值的覆盖,因为 `WorkerBase` 类默认使用布尔值作为线程结果值。

protected void bw_RequestWork(object sender, DoWorkEventArgs e)
{
	QueryWorker qw = new QueryWorker((BackgroundWorker)sender, e);
	qw.ExecuteQuery();
	e.Result = qw.Context;
}

一个有趣的观点是,`QueryContext` 类使用了两个委托,它们可以根据我们需要的输出类型(网格或文本)进行更改。为了解决列和行方面可变结果的问题,我使用了一个新的 Framework 4 对象:`ExpandoObject`。

下面使用的算法可以概括为

  1. 从上下文和 Provider 检索 `IDataReader`
  2. 循环遍历字段以添加列(根据委托,可以是 `datagrid` 列或简单文本)
  3. 然后循环遍历行以调用 `AddDataHandler`
public void ExecuteQuery()
{
	Tracer.Verbose("QueryWorker.ExecuteQuery", "START");
	IDataReader reader = null;

	try
	{
		reader = Context.Source.Database.Provider.GetSQLQueryInterface().
		ExecuteReader(Context.Source.ConnectionString, Context.Command);

		for (int i = 0; i < reader.FieldCount; i++)
			Context.UIDispatcher.Invoke
			(Context.AddColumn, reader.GetName(i));

		int counter = 0;
		while (!CancelPending() && reader.Read() )
		{
			dynamic data = new ExpandoObject();

			for (int i = 0; i < reader.FieldCount; i++)
				((IDictionary<String, Object>)data).Add
				(reader.GetName(i), reader.GetValue(i));

			Context.UIDispatcher.Invoke(Context.AddData, data);
			counter++;
		}

		Context.LineCount = counter;
		Context.Message = "Query ok";
	}
	[...]
}

这就是奇迹!`ExpandoObject` 暴露了您可以动态创建的动态属性!因此,网格的两个处理程序非常简单……因为我们只是将对象添加到集合(在网格 `DataContext` 中),因此网格将通过绑定发现属性和值。

protected void GridAddColumnHandler(string headerText)
{
	if (this.dataGridResult.ItemsSource == null)
		this.dataGridResult.ItemsSource = new ObservableCollection<dynamic>();

	DataGridTextColumn dt = new DataGridTextColumn();
	dt.Header = headerText;
	dt.Binding = new Binding(headerText);
	this.dataGridResult.Columns.Add(dt);
}

protected void GridAddDataHandler(dynamic data)
{
	(this.dataGridResult.ItemsSource as ObservableCollection<dynamic>).Add(data);
}

请注意,性能需要改进。我注意到当网格更新其界面时速度非常慢,而之后只需要调整滚动条时速度要快得多。

您已经注意到,我们循环读取器并始终检查 `CancelPending` 方法。它来自 `WorkerBase` 并且可以中断线程。

结论/反馈

请关注本系列的下一篇文章。请随时在 Codeplex 或 CodeProject 上给我反馈。随着团队的壮大,我希望我们能更快地发展,欢迎加入我们!

文章/软件历史

  • 初始发布 - 版本 BETA 0.2
    • 包含“几乎”所有内容的初始版本,可在第 1 部分CodePlex上作为发布版本下载。
  • 版本 BETA 0.3
    • 更新了皮肤/颜色管理和数据库模块。
    • 第四部分 - 数据库模块 - 已发布。
© . All rights reserved.