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

程序员创业指南与企业应用构建 - 第 4 篇

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2009 年 7 月 27 日

MPL

9分钟阅读

viewsIcon

85385

程序员创业指南与企业应用构建

引言

这是我关于创建 SplendidCRM Software, Inc. 系列文章的第四篇。我希望我的创业经历能激励您。在我看来,创建一个公司可以是一次美妙的冒险。

第四篇文章

仅仅因为你创建了它,并不意味着客户就会纷至沓来。几个月前,我听一位演讲者谈论如何营销产品。我忍不住笑了,因为我做了这位演讲者推荐的一切,但我仍然没有获得大量的客户。幸运的是,这位演讲者是在一个免费活动上发言的,所以他不太有帮助的建议没有花我任何钱。

以下是我学到的关于营销的知识:

  1. 注册域名和创建网站并不会让你的产品突然出现在所有搜索引擎的顶部。
  2. 将域名提交给 Google 并不会突然产生数千次点击。
  3. 创建博客并不会突然吸引大量忠实读者。
  4. 发送新闻稿并不会让你在所有行业期刊上被提及。
  5. 花钱请公关公司并不会立即让你在行业期刊上被报道。
  6. 花钱购买横幅广告并不会产生大量潜在客户。
  7. 在大量 Google AdWords 上竞价并不会产生大量流量。
  8. 创建 Facebook 页面并不会让你获得大量好友。
  9. 创建 Twitter 账户并发布你的一举一动并不会让你获得大量粉丝。

你必须有耐心、有恒心、脚踏实地、节俭。这些建议是古往今来流传下来的,它们是成功的久经考验的方法。即使在“互联网时代”,先行者的优势至关重要,一切都必须准备好明天发货,这些建议仍然是好的。营销需要时间、金钱和才能。如果你没有时间、金钱或才能,你可能需要找一个拥有所有这些优势的合作伙伴。

我不是在否定,你需要一个好的域名,你需要一个网站,你应该将网站提交给所有搜索引擎。但是,不要认为这些事情中的任何一件都会是你突然变富的灵丹妙药。有些人会说新闻稿是一种重要的营销工具。如果你是一个知名的组织,这可能是真的,但如果你是一个小公司,不要指望你的新闻稿会被除了自动化互联网服务之外的任何人注意到。很有可能,你希望看到你的新闻稿的行业网站和杂志不会注意到你。这些行业网站忙于发布来自 Microsoft、Cisco 和 Google 的大量新闻。

我写这一系列文章的目的是激励大家,虽然这篇文章可能看起来像一盆冷水,但我的目的恰恰相反。通过帮助你设定切合实际的期望,我希望你不会因为你的新闻稿没有产生你预期的反响而感到沮丧。我希望鼓励你去尝试不同的事物。我希望通过让你知道花一万美元请公关公司也不能保证成功来为你省钱。

另一方面,如果你在营销的任何方面都非常成功,请发表评论并告诉我们你做了什么。我们都想知道什么有效,什么无效。

审计

SplendidCRM 采取了一种老式的审计方法,它使用数据库中的触发器来跟踪特定表中所有记录的所有更改。这种审计方法的一个大好处是它非常快速且非常可靠。这种方法的缺点是它占用大量硬盘空间。随着硬盘空间每年都在变得越来越便宜,我们认为利大于弊。

相比之下,SugarCRM 通过在 PHP 中手动比较前一条记录和当前记录来确定审计信息。然后,SugarCRM Web 应用程序为已更改的每个字段存储一行。

我们不在 SplendidCRM 中采用这种方法有很多原因,但其中最主要的原因是 SugarCRM 的方法意味着所有数据都必须由 PHP 应用程序处理,以便正确添加审计信息,而 SplendidCRM 允许任何外部应用程序直接将数据插入或更新到数据库中,并且仍然正确记录其审计信息。审计数据存储在以 `_AUDIT` 结尾的审计表中。这些表应该拥有与基础表相同的所有字段,外加几个额外的字段来跟踪审计。例如,下面 `ACCOUNTS_AUDIT` 表的摘录显示了我们在所有审计表中使用的六个通用审计字段。

Create Table dbo.ACCOUNTS_AUDIT
	( AUDIT_ID             uniqueidentifier   not null
	, AUDIT_ACTION         int                not null
	, AUDIT_DATE           datetime           not null
	, AUDIT_VERSION        rowversion         not null
	, AUDIT_COLUMNS        varbinary(128)     null
	, AUDIT_TOKEN          varchar(255)       null

我们的所有审计表都是使用数据库提供的架构信息动态生成的。如果你是数据库专家,那么你可能已经知道如何获取所有可用表以及这些表内所有列的列表。对于那些不是专家的人,我将向你展示如何做到这一点。作为存储过程和视图的忠实粉丝,你将在实现中看到过程和视图。

vwSqlTables

首先,让我们从系统上所有可用表的列表开始。有几种方法可以获取这样的列表。我们使用 `INFORMATION_SCHEMA`,因为它是一种跨多个数据库平台共享的技术。在我们的情况下,由于我们处理的是 SQL Server,我们需要排除列表中通常会找到的几个伪系统表。

Create View dbo.vwSqlTables
as
select TABLE_NAME
  from INFORMATION_SCHEMA.TABLES
 where TABLE_TYPE = N'BASE TABLE'
   and TABLE_NAME not in (N'dtproperties', N'sysdiagrams')
GO

vwSqlTablesAudited

第二步是将表列表减少到仅包含已审计的表。在 SplendidCRM 中,我们审计一切,所以我们的已审计表视图从所有表开始,然后我们排除不需要审计的 SplendidCRM 系统表。SplendidCRM 中有很多系统表,但我在这里将它们缩写。

Create View dbo.vwSqlTablesAudited
as
select TABLE_NAME
  from vwSqlTables
 where TABLE_NAME not like N'%_AUDIT'
   and TABLE_NAME not like N'TEMP_%'
   and TABLE_NAME not in
( N'ACL_ROLES_CSTM'
, N'CAMPAIGN_TRKRS'
, N'CAMPAIGN_TRKRS_CSTM'
, N'CONTRACT_TYPES_DOCUMENTS'
, N'CURRENCIES_CSTM'
. . .
)
GO

从 `vwSqlTablesAudited` 中可以看到,我们排除了审计表、临时表以及任何其他系统表。这意味着我们的用户创建的任何客户表都将自动完全审计。

spSqlBuildAllAuditTables

现在我们有了要审计的表列表,我们可以使用游标循环遍历列表并构建每个审计表。代码是非常直接的 SQL,我们定义游标、打开游标、循环遍历游标并关闭游标。真正繁重的工作是在另一个以表名作为参数的存储过程中完成的。任何时候我们构建审计表,我们还需要构建审计触发器,所以我们在过程的底部进行。

Create Procedure dbo.spSqlBuildAllAuditTables
as
  begin
	declare @TABLE_NAME varchar(80);
	declare TABLES_CURSOR cursor for
	select TABLE_NAME
	  from vwSqlTablesAudited
	order by TABLE_NAME;
	
	open TABLES_CURSOR;
	fetch next from TABLES_CURSOR into @TABLE_NAME;
	while @@FETCH_STATUS = 0 begin -- do
		exec dbo.spSqlBuildAuditTable @TABLE_NAME;
		fetch next from TABLES_CURSOR into @TABLE_NAME;
	end -- while;
	close TABLES_CURSOR;
	deallocate TABLES_CURSOR;

	exec dbo.spSqlBuildAllAuditTriggers ;
  end
GO

spSqlBuildAllAuditTriggers

创建审计触发器的主要过程几乎相同,并且围绕已审计表进行了类似的循环。这次,我们只需确保每个审计表都存在,然后再尝试创建触发器。

Create Procedure dbo.spSqlBuildAllAuditTriggers
as
  begin
	declare @TABLE_NAME varchar(80);
	declare TABLES_CURSOR cursor for
	select vwSqlTablesAudited.TABLE_NAME
	  from      vwSqlTablesAudited
	 inner join vwSqlTables
	         on vwSqlTables.TABLE_NAME = vwSqlTablesAudited.TABLE_NAME + '_AUDIT'
	order by vwSqlTablesAudited.TABLE_NAME;
	
	open TABLES_CURSOR;
	fetch next from TABLES_CURSOR into @TABLE_NAME;
	while @@FETCH_STATUS = 0 begin -- do
		exec dbo.spSqlBuildAuditTrigger @TABLE_NAME;
		fetch next from TABLES_CURSOR into @TABLE_NAME;
	end -- while;
	close TABLES_CURSOR;
	deallocate TABLES_CURSOR;
  end
GO

vwSqlColumns

在构建与基础表具有相同字段和数据类型的所有审计表时,我们需要一个视图来返回字段及其表。我们在 SplendidCRM 中大量使用 `vwSqlColumns`,所以我们实际的视图版本做了更多的事情,但为了这个目的,我们已经简化了视图,只包含构建审计表所需的信息。此外,我们不使用 `INFORMATION_SCHEMA`,而是直接访问 SQL Server 系统表。这些系统表提供了关于用户表(U)、存储过程(P)、视图(V)和函数(FN)的信息。重要的是要注意 `vwSqlColumns` 只为 SplendidCRM 支持的数据类型定义了 `ColumnType`。SQL Server 支持更多数据类型,尝试在不支持的类型上使用我们的审计系统将产生错误。解决方法是为你的应用程序中使用的所有数据类型添加对 `vwSqlColumns` 视图的支持。

Create View dbo.vwSqlColumns
as
select sysobjects.name as ObjectName
     , syscolumns.name as ColumnName
     , syscolumns.colid
     , (case when syscolumns.xtype =  36 then N'uniqueidentifier'
             when syscolumns.xtype =  48 then N'tinyint'
             when syscolumns.xtype =  56 then N'int'
             when syscolumns.xtype = 127 then N'bigint'
             when syscolumns.xtype =  59 then N'real'
             when syscolumns.xtype =  62 _
		then N'float('    + cast(syscolumns.prec     as varchar) + N')'
             when syscolumns.xtype =  60 then N'money'
             when syscolumns.xtype = 104 then N'bit'
             when syscolumns.xtype = 175 _
		then N'char('     + cast(syscolumns.length   as varchar) + N')'
             when syscolumns.xtype = 167 _
		then N'varchar('  + cast(syscolumns.length   as varchar) + N')'
             when syscolumns.xtype = 231 _
		then N'nvarchar(' + cast(syscolumns.length/2 as varchar) + N')'
             when syscolumns.xtype = 239 _
		then N'nchar('    + cast(syscolumns.length/2 as varchar) + N')'
             when syscolumns.xtype =  99 then N'ntext'
             when syscolumns.xtype =  61 then N'datetime'
             when syscolumns.xtype =  34 then N'image'
             when syscolumns.xtype = 106 _
		then N'decimal('  + cast(syscolumns.prec   as varchar) + N', ' + _
		cast(syscolumns.scale as varchar) + N')'
             when syscolumns.xtype = 165 _
		then N'varbinary('+ cast(syscolumns.length as varchar) + N')'
             when syscolumns.xtype = 173 _
		then N'binary('   + cast(syscolumns.length as varchar) + N')'
             when syscolumns.xtype = 189 then N'timestamp'
        end
       ) as ColumnType
  from sysobjects
 inner join syscolumns
         on syscolumns.id = sysobjects.id
 where sysobjects.type in ('U', 'P', 'V', 'FN')
GO

spSqlBuildAuditTable

`spSqlBuildAuditTable` 执行两项操作;它要么

  1. 如果审计表不存在,则构建一个全新的审计表,或者
  2. 如果审计表已存在,则添加缺失的字段。

`vwSqlColumns` 视图在 SplendidCRM 中得到广泛使用,并且是为了使构建 Create Table 语句变得非常容易而编写的,因为它在 `ColumnType` 字段中返回格式正确的 `ColumnType`。然后,构建 Create Table 语句变成了一个简单的游标,该游标连接 `ColumnName` 和 `ColumnType`。我们喜欢我们的语句易于阅读,所以我们总是在每一行添加 `@CRLF`,即使 SQL Server 不关心我们是否用换行符分隔字段。当我们调试此存储过程时,我们经常将 SQL 的 "exec" 语句替换为 print 语句。

如果审计表已存在,则我们使用外部联接在基础表的字段和审计表的字段之间创建游标,以找出审计表中缺少的字段。在这种情况下,我们为需要添加到审计表中的每个字段执行 Alter Table 语句。

Create Procedure dbo.spSqlBuildAuditTable(@TABLE_NAME varchar(80))
as
  begin
	declare @Command           varchar(8000);
	declare @AUDIT_TABLE       varchar(90);
	declare @AUDIT_PK          varchar(90);
	declare @COLUMN_NAME       varchar(80);
	declare @COLUMN_TYPE       varchar(20);
	declare @CRLF              char(2);
	
	set @CRLF = char(13) + char(10);
	set @AUDIT_TABLE = @TABLE_NAME + '_AUDIT';
	set @AUDIT_PK    = 'PKA_' + @TABLE_NAME;
	if not exists (select * from dbo.sysobjects where id = _
		object_id(@AUDIT_TABLE) and OBJECTPROPERTY_
			(id, N'IsUserTable') = 1) begin -- then
		declare COLUMNS_CURSOR cursor for
		select ColumnName
		     , ColumnType
		  from vwSqlColumns
		 where ObjectName = @TABLE_NAME
		 order by colid;
	
		set @Command = '';
		set @Command = @Command + 'Create Table dbo.' + @AUDIT_TABLE + @CRLF;
		set @Command = @Command + _
		'	( AUDIT_ID          uniqueidentifier   not null constraint ' _
		+ @AUDIT_PK + ' primary key' + @CRLF;
		set @Command = @Command + _
		'	, AUDIT_ACTION      int                not null' + @CRLF;
		set @Command = @Command + _
		'	, AUDIT_DATE        datetime           not null' + @CRLF;
		set @Command = @Command + _
		'	, AUDIT_VERSION     rowversion         not null' + @CRLF;
		set @Command = @Command + _
		'	, AUDIT_COLUMNS     varbinary(128)     null' + @CRLF;
		set @Command = @Command + _
		'	, AUDIT_TOKEN       varchar(255)       null' + @CRLF;
		open COLUMNS_CURSOR;
		fetch next from COLUMNS_CURSOR into @COLUMN_NAME, @COLUMN_TYPE;
		while @@FETCH_STATUS = 0 begin -- while
			set @Command = @Command + '	, ' + _
			@COLUMN_NAME + ' ' + @COLUMN_TYPE + ' null' + @CRLF;
			fetch next from COLUMNS_CURSOR into _
				@COLUMN_NAME, @COLUMN_TYPE;
		end -- while;
		close COLUMNS_CURSOR;
		deallocate COLUMNS_CURSOR;
		set @Command = @Command + '	)' + @CRLF;
		exec(@Command);

		if right(@TABLE_NAME, 5) = '_CSTM' begin -- then
			set @Command = 'create index IDX_' + _
			@AUDIT_TABLE + ' on dbo.' + @AUDIT_TABLE + _
			'(ID_C, AUDIT_TOKEN, AUDIT_ACTION)';
		end else begin
			set @Command = 'create index IDX_' + _
			@AUDIT_TABLE + ' on dbo.' + @AUDIT_TABLE + _
			'(ID, AUDIT_VERSION, AUDIT_TOKEN)';
		end -- if;
		exec(@Command);
	end else begin
		print 'Alter Table dbo.' + @AUDIT_TABLE + ';';
		declare COLUMNS_CURSOR cursor for
		select vwSqlColumns.ColumnName
		     , vwSqlColumns.ColumnType
		  from            vwSqlColumns
		  left outer join vwSqlColumns                   vwSqlColumnsAudit
		               on vwSqlColumnsAudit.ObjectName = _
					vwSqlColumns.ObjectName + '_AUDIT'
		              and vwSqlColumnsAudit.ColumnName = _
						vwSqlColumns.ColumnName
		 where vwSqlColumnsAudit.ObjectName is null
		   and vwSqlColumns.ObjectName = @TABLE_NAME
		 order by vwSqlColumns.colid;

		open COLUMNS_CURSOR;
		fetch next from COLUMNS_CURSOR into @COLUMN_NAME, @COLUMN_TYPE;
		while @@FETCH_STATUS = 0 begin -- while
			set @Command = 'alter table ' + @AUDIT_TABLE + _
			' add ' + @COLUMN_NAME + ' ' + _
				@COLUMN_TYPE + ' null' + @CRLF;
			print @Command;
			exec(@Command);
			fetch next from COLUMNS_CURSOR into _
					@COLUMN_NAME, @COLUMN_TYPE;
		end -- while;
		close COLUMNS_CURSOR;
		deallocate COLUMNS_CURSOR;
	end -- if;
  end
GO

spSqlBuildAuditTrigger

`spSqlBuildAuditTrigger` 过程与 `spSqlBuildAuditTable` 过程非常相似,因为我们从字段列表开始并构建 SQL 语句。然而,在审计触发器的情况下,我们必须删除已存在的触发器。我们创建单独的 `insert` 和 `update` 触发器,以便我们能够指定正确的 `AUDIT_ACTION`(`0` 表示插入,`1` 表示更新,`-1` 表示删除)。在 SplendidCRM 中,我们实际上不会删除记录,所以你不会找到 `delete` 触发器,但如果 `DELETED` 字段更改为 `1`,我们需要正确设置审计操作标志。

`AUDIT_TOKEN` 很有趣,因为我们使用此字段来存储 SQL Server `@BIND_TOKEN`。此值对于每个事务都是唯一的,因此我们使用它来跟踪单个事务中所有已更改的记录。`AUDIT_TOKEN` 成为我们工作流引擎的关键字段。

Create Procedure dbo.spSqlBuildAuditTrigger(@TABLE_NAME varchar(80))
as
  begin
	declare @Command      varchar(8000);
	declare @CRLF         char(2);
	declare @AUDIT_TABLE  varchar(90);
	declare @TRIGGER_NAME varchar(90);
	declare @COLUMN_NAME  varchar(80);
	declare @COLUMN_TYPE  varchar(20);
	declare @PRIMARY_KEY  varchar(10);

	set @PRIMARY_KEY = 'ID';
	if right(@TABLE_NAME, 5) = '_CSTM' begin -- then
		set @PRIMARY_KEY = 'ID_C';
	end -- if;
	set @AUDIT_TABLE = @TABLE_NAME + '_AUDIT';
	if exists (select * from vwSqlTables where TABLE_NAME = _
				@AUDIT_TABLE) begin -- then
		set @CRLF = char(13) + char(10);
		declare COLUMNS_CURSOR cursor for
		select vwSqlColumns.ColumnName
		     , vwSqlColumns.ColumnType
		  from       vwSqlColumns
		  inner join vwSqlColumns                   vwSqlColumnsAudit
		          on vwSqlColumnsAudit.ObjectName = _
					vwSqlColumns.ObjectName + '_AUDIT'
		         and vwSqlColumnsAudit.ColumnName = vwSqlColumns.ColumnName
		 where vwSqlColumns.ObjectName = @TABLE_NAME
		 order by vwSqlColumns.colid;

		set @TRIGGER_NAME = 'tr' + @TABLE_NAME + '_Ins_AUDIT';
		if exists (select * from dbo.sysobjects where id = _
			object_id(@TRIGGER_NAME) and OBJECTPROPERTY_
			(id, N'IsTrigger') = 1) begin -- then
			set @Command = 'Drop   Trigger dbo.' + @TRIGGER_NAME;
			exec(@Command);
		end -- if;

		if right(@TABLE_NAME, 5) <> '_CSTM' begin -- then
			if not exists (select * from dbo.sysobjects _
			where id = object_id(@TRIGGER_NAME) and _
			OBJECTPROPERTY(id, N'IsTrigger') = 1) begin -- then
				set @Command = '';
				set @Command = @Command + _
					'Create Trigger dbo.' + @TRIGGER_NAME _
					+ ' on dbo.' + @TABLE_NAME + @CRLF;
				set @Command = @Command + 'for insert' + @CRLF;
				set @Command = @Command + 'as' + @CRLF;
				set @Command = @Command + '  begin' + @CRLF;
				set @Command = @Command + _
				'	declare @BIND_TOKEN varchar(255);' + @CRLF;
				set @Command = @Command + _
				'	exec spSqlGetTransactionToken _
				@BIND_TOKEN out;' + @CRLF;
				set @Command = @Command + _
				'	insert into dbo.' + @AUDIT_TABLE + @CRLF;
				set @Command = @Command + _
				'	     ( AUDIT_ID'      + @CRLF;
				set @Command = @Command + _
				'	     , AUDIT_ACTION'  + @CRLF;
				set @Command = @Command + _
				'	     , AUDIT_DATE'    + @CRLF;
				set @Command = @Command + _
				'	     , AUDIT_COLUMNS' + @CRLF;
				set @Command = @Command + _
				'	     , AUDIT_TOKEN'   + @CRLF;
				open COLUMNS_CURSOR;
				fetch next from COLUMNS_CURSOR into @COLUMN_NAME, _
						@COLUMN_TYPE;
				while @@FETCH_STATUS = 0 begin -- while
					set @Command = @Command + _
					'	     , ' + @COLUMN_NAME + @CRLF;
					fetch next from COLUMNS_CURSOR _
					into @COLUMN_NAME, @COLUMN_TYPE;
				end -- while;
				close COLUMNS_CURSOR
				set @Command = @Command + '	     )' + @CRLF;
				set @Command = @Command + _
				'	select newid()'           + @CRLF;
				set @Command = @Command + _
				'	     , 0  -- insert'      + @CRLF;
				set @Command = @Command + _
				'	     , getdate()'         + @CRLF;
				set @Command = @Command + _
				'	     , columns_updated()' + @CRLF;
				set @Command = @Command + _
				'	     , @BIND_TOKEN'       + @CRLF;
				
				open COLUMNS_CURSOR;
				fetch next from COLUMNS_CURSOR into _
						@COLUMN_NAME, @COLUMN_TYPE;
				while @@FETCH_STATUS = 0 begin -- while
					set @Command = @Command + _
					'	     , ' + @TABLE_NAME + _
					'.' + @COLUMN_NAME + @CRLF;
					fetch next from COLUMNS_CURSOR _
						into @COLUMN_NAME, @COLUMN_TYPE;
				end -- while;
				close COLUMNS_CURSOR;
				set @Command = @Command + _
				'	  from       inserted' + @CRLF;
				set @Command = @Command + _
				'	  inner join ' + @TABLE_NAME + @CRLF;
				set @Command = @Command + _
				'	          on ' + @TABLE_NAME + _
				'.' + @PRIMARY_KEY + ' = inserted.' + _
				@PRIMARY_KEY + ';' + @CRLF;
				set @Command = @Command + '  end' + @CRLF;
				exec(@Command);
			end -- if;
		end -- if;

		set @TRIGGER_NAME = 'tr' + @TABLE_NAME + '_Upd_AUDIT';
		if exists (select * from dbo.sysobjects where id = _
			object_id(@TRIGGER_NAME) and OBJECTPROPERTY_
			(id, N'IsTrigger') = 1) begin -- then
			set @Command = 'Drop   Trigger dbo.' + @TRIGGER_NAME;
			exec(@Command);
		end -- if;

		if not exists (select * from dbo.sysobjects where id = _
			object_id(@TRIGGER_NAME) and OBJECTPROPERTY_
			(id, N'IsTrigger') = 1) begin -- then
			set @Command = '';
			set @Command = @Command + 'Create Trigger dbo.' _
				+ @TRIGGER_NAME + ' on dbo.' + @TABLE_NAME + @CRLF;
			set @Command = @Command + 'for update' + @CRLF;
			set @Command = @Command + 'as' + @CRLF;
			set @Command = @Command + '  begin' + @CRLF;
			set @Command = @Command + _
			'	declare @BIND_TOKEN varchar(255);' + @CRLF;
			-- 01/09/2009 Paul.  spSqlGetTransactionToken should be 
                            -- used instead of sp_getbindtoken. 
			set @Command = @Command + _
			'	exec spSqlGetTransactionToken @BIND_TOKEN out;_
			' + @CRLF;
			set @Command = @Command + '	insert into dbo.' + _
							@AUDIT_TABLE + @CRLF;
			set @Command = @Command + '	     ( AUDIT_ID'      + @CRLF;
			set @Command = @Command + '	     , AUDIT_ACTION'  + @CRLF;
			set @Command = @Command + '	     , AUDIT_DATE'    + @CRLF;
			set @Command = @Command + '	     , AUDIT_COLUMNS' + @CRLF;
			set @Command = @Command + '	     , AUDIT_TOKEN'   + @CRLF;
			open COLUMNS_CURSOR;
			fetch next from COLUMNS_CURSOR into _
					@COLUMN_NAME, @COLUMN_TYPE;
			while @@FETCH_STATUS = 0 begin -- while
				set @Command = @Command + '	     , ' + _
						@COLUMN_NAME + @CRLF;
				fetch next from COLUMNS_CURSOR into _
						@COLUMN_NAME, @COLUMN_TYPE;
			end -- while;
			close COLUMNS_CURSOR;
			set @Command = @Command + '	     )' + @CRLF;
			set @Command = @Command + '	select newid()'           + @CRLF;
			if right(@TABLE_NAME, 5) <> '_CSTM' begin -- then
				set @Command = @Command + '	     , _
				(case inserted.DELETED when 1 _
				then -1 else 1 end) -- updated'      + @CRLF;
			end else begin
				set @Command = @Command + '	     , _
						1 -- updated'      + @CRLF;
			end -- if;
			set @Command = @Command + '	     , getdate()'         + @CRLF;
			set @Command = @Command + '	     , columns_updated()' + @CRLF;
			set @Command = @Command + '	     , @BIND_TOKEN'       + @CRLF;
			
			open COLUMNS_CURSOR;
			fetch next from COLUMNS_CURSOR into @COLUMN_NAME, _
								@COLUMN_TYPE;
			while @@FETCH_STATUS = 0 begin -- while
				set @Command = @Command + _
			'	     , ' + @TABLE_NAME + '.' + @COLUMN_NAME + @CRLF;
				fetch next from COLUMNS_CURSOR into _
						@COLUMN_NAME, @COLUMN_TYPE;
			end -- while;
			close COLUMNS_CURSOR;
			set @Command = @Command + '	  from       inserted' + @CRLF;
			set @Command = @Command + '	  inner join ' + _
						@TABLE_NAME + @CRLF;
			set @Command = @Command + '	          on ' + _
				@TABLE_NAME + '.' + @PRIMARY_KEY + _
				' = inserted.' + @PRIMARY_KEY + ';' + @CRLF;
			set @Command = @Command + '  end' + @CRLF;
			exec(@Command);
		end -- if;
		deallocate COLUMNS_CURSOR;
	end -- if;
  end
GO

除非你真的非常喜欢阅读 SQL,否则你可能想看看这些过程的输出。我将以 `PROJECT` 表为例,因为它字段不多。`spSqlBuildAuditTable` 存储过程将创建 `PROJECT_AUDIT` 表以及 `trPROJECT_Ins_AUDIT` 和 `trPROJECT_Upd_AUDIT` 触发器。

PROJECT_AUDIT

Create Table dbo.PROJECT_AUDIT
	( AUDIT_ID             uniqueidentifier   _
				not null constraint PKA_PROJECT primary key
	, AUDIT_ACTION         int                not null
	, AUDIT_DATE           datetime           not null
	, AUDIT_VERSION        rowversion         not null
	, AUDIT_COLUMNS        varbinary(128)     null
	, AUDIT_TOKEN          varchar(255)       null
	, ID                   uniqueidentifier   null
	, DELETED              bit                null
	, CREATED_BY           uniqueidentifier   null
	, DATE_ENTERED         datetime           null
	, MODIFIED_USER_ID     uniqueidentifier   null
	, DATE_MODIFIED        datetime           null
	, ASSIGNED_USER_ID     uniqueidentifier   null
	, NAME                 nvarchar(50)       null
	, DESCRIPTION          ntext              null
	, TEAM_ID              uniqueidentifier   null
	);

create index IDX_PROJECT_AUDIT on dbo.PROJECT_AUDIT(ID, AUDIT_VERSION, AUDIT_TOKEN);

trPROJECT_Ins_AUDIT

Create Trigger dbo.trPROJECT_Ins_AUDIT on dbo.PROJECT
for insert
as
  begin
	declare @BIND_TOKEN varchar(255);
	exec spSqlGetTransactionToken @BIND_TOKEN out;
	insert into dbo.PROJECT_AUDIT
	     ( AUDIT_ID
	     , AUDIT_ACTION
	     , AUDIT_DATE
	     , AUDIT_COLUMNS
	     , AUDIT_TOKEN
	     , ID
	     , DELETED
	     , CREATED_BY
	     , DATE_ENTERED
	     , MODIFIED_USER_ID
	     , DATE_MODIFIED
	     , ASSIGNED_USER_ID
	     , NAME
	     , DESCRIPTION
	     , TEAM_ID
	     )
	select newid()
	     , 0  -- insert
	     , getdate()
	     , columns_updated()
	     , @BIND_TOKEN
	     , PROJECT.ID
	     , PROJECT.DELETED
	     , PROJECT.CREATED_BY
	     , PROJECT.DATE_ENTERED
	     , PROJECT.MODIFIED_USER_ID
	     , PROJECT.DATE_MODIFIED
	     , PROJECT.ASSIGNED_USER_ID
	     , PROJECT.NAME
	     , PROJECT.DESCRIPTION
	     , PROJECT.TEAM_ID
	  from       inserted
	  inner join PROJECT
	          on PROJECT.ID = inserted.ID;
  end

trPROJECT_Upd_AUDIT

Create Trigger dbo.trPROJECT_Upd_AUDIT on dbo.PROJECT
for update
as
  begin
	declare @BIND_TOKEN varchar(255);
	exec spSqlGetTransactionToken @BIND_TOKEN out;
	insert into dbo.PROJECT_AUDIT
	     ( AUDIT_ID
	     , AUDIT_ACTION
	     , AUDIT_DATE
	     , AUDIT_COLUMNS
	     , AUDIT_TOKEN
	     , ID
	     , DELETED
	     , CREATED_BY
	     , DATE_ENTERED
	     , MODIFIED_USER_ID
	     , DATE_MODIFIED
	     , ASSIGNED_USER_ID
	     , NAME
	     , DESCRIPTION
	     , TEAM_ID
	     )
	select newid()
	     , (case inserted.DELETED when 1 then -1 else 1 end) -- updated
	     , getdate()
	     , columns_updated()
	     , @BIND_TOKEN
	     , PROJECT.ID
	     , PROJECT.DELETED
	     , PROJECT.CREATED_BY
	     , PROJECT.DATE_ENTERED
	     , PROJECT.MODIFIED_USER_ID
	     , PROJECT.DATE_MODIFIED
	     , PROJECT.ASSIGNED_USER_ID
	     , PROJECT.NAME
	     , PROJECT.DESCRIPTION
	     , PROJECT.TEAM_ID
	  from       inserted
	  inner join PROJECT
	          on PROJECT.ID = inserted.ID;
  end

工作流

现在你已经看到了我们在 SplendidCRM 中如何创建详细的审计跟踪,你可能会对我们整个工作流引擎都构建在这个审计系统之上感到有趣。我们的工作流引擎需要在任何特定字段发生更改时运行,通过比较已审计记录和当前记录,我们可以检测到这些更改。我们审计系统的这种设计使我们能够创建一个高性能的工作流引擎。

希望你喜欢这个系列的第四篇文章。请在接下来的几周内关注第五篇文章。

历史

  • 2009 年 7 月 27 日:首次发布
© . All rights reserved.