当前位置: 首页 > 编程日记 > 正文

[原创] 如何追踪每一笔记录的来龙去脉:一个完整的Audit Logging解决方案—Part I...

一、提出问题

在开发一个企业级 应用的时候,尤其在一个涉及到敏感数据的应用,比如财务系统、物流系统,我们往往有这样的需求:对于数据库中每一笔数据的添加、修改和删除,都需要有一个明确的日志,以便我们可以追踪每一笔记录的来龙去脉——数据的更新是被谁、在什么时候执行的?该操作还涉及到哪些具体的Table?原来的数据是什么?新的数据又是什么?

本Blog的目的就是基于上面提出的要求设计一个Audit Logging的解决方案。

二、分析问题

基于上面提出的要求,我们进行具体的分析:

A.如何确定Log的粒度?

对于一个企业级 应用,数据的每一项操作应该被纳入一个Transaction中以保证数据的完整性。所以Transaction可以看作是数据操作的基本单元,我们的解决方案是 以Transaction为单位的Log。

B.如何确定记录的信息?

正如一开始我们提出的要求,我们记录的不仅仅包括Transaction本身的一些基本信息,比如执行该操作的User,执行的时间等。由于一个Transaction会涉及到对多个相关Table中的一个或者多个记录的增、删、改的操作,所以下面一些信息也需要纳入我们的Logging范畴:Transaction涉及的Table,每条记录的数据的变化:对于Insert操作,需要记录添加的新记录的数据,对于Update操作,需要记录原来的数据和更新后的数据,而对于Delete操作,需要记录Delete之前的数据。

C.如何设计记录的数据结构?

基于我们提取出的需要进行Log的信息,我们为决绝方案设计了下面的数据结构:两个具有主子关系的Table。主表T_AUDIT_LOG记录了一个Transaction的基本的信息:Transaction的标识,执行的用户帐号和操作的具体时间,子表T_AUDIT_LOG_DETAIL则记录了Transaction涉及的每条记录数据改变相关的信息:该记录对应的Table名称,操作的类型和具体的数据的变化。T_AUDIT_LOG和T_AUDIT_LOG_DETAIL通过Transaction的唯一标识TRANSACTION_NO关联在一起。 

主表T_AUDIT_LOG的结构:

  • TRANSACTION_NO[CHAR(36)]:一个GUID代表的字符串,唯一表示一个Transaction。
  • OPERATION_DATE[DATETIME]:Transaction真正执行的时间(Filed name应该改为OPERATION_TIME才对)
  • USER_ID [VARCHAR] :执行该Transaction的用户帐号。

子表T_AUDIT_LOG_DETAIL的结构:

  • AUDIT_DETAIL_ID(INT):一个自增长的Field,用作该表的主键。
  • TRANSACTION_NO [CHAR(36)] :同主表T_AUDIT_LOG的TRANSACTION_NO字段,Transaction的唯一标识,一个GUID。
  • TABLE_NAME [VARCHAR] :操作涉及的具体的Table的名称。
  • OPERATION_TYPE [VARCHAR] :操作的类型——Insert,Update,Delete。
  • DATA_CHANGE [XML] :该字段采用了SQL Server 2005新的数据类型——XML,用于存储操作引起的数据的改变。<before>Element封装了Update 操作之前的数据,其中每个XML attribute代表的是对用的Filed,<after>包含的则是Update执行之后的数据。这是Update操作对应的XML schema,如果操作对应的是向某个表中Insert一个记录,则只有封装了新添加记录数据的<after> element,同样的Delete操作对应的XML只有包含被Delete记录的<before> Element。

For Update

None.gif<dataChange>
None.gif  
<before order_id="30" order_date="Apr 21 2007 12:00AM" supplier="HP" />
None.gif  
<after order_id="30" order_date="Jan  1 2005 12:00AM" supplier="Dell Corporation" />
None.gif
</dataChange>
None.gif

For Insert

None.gif<dataChange>  
None.gif
<after order_id="30" order_date="Jan  1 2005 12:00AM" supplier="Dell Corporation" />
None.gif
</dataChange>
None.gif

For Delete

None.gif<dataChange>
None.gif  
<before order_id="30" order_date="Apr 21 2007 12:00AM" supplier="HP" />
None.gif
</dataChange>
None.gif

D.如何添加Log记录?

从T_AUDIT_LOG_DETAIL的结构上可以很清楚地看出,该表记录的是基于某个具体的Table的每个记录数据变化。所以我们会首先想到的是通过Trigger来添加这些Logging数据——当完成对相关Table的增、删、改操作后,通过出发我们为Audit Logging编写的Trigger来自动添加这些信息。所以我的这个Audit Logging的解决方案是一个基于Trigger的解决方案,我将在下面一节中讲述如何编写这个Trigger。由于我们的Logging数据表采用的是一个具有Parent-Child关系的两个Table,在通过Trigger为子表T_AUDIT_LOG_DETAIL添加Log记录之前,我们必须保证主表T_AUDIT_LOG中包含相应的记录,所以在进行与逻辑相关的数据操作之前,我们必须在把Log的总体信息插入T_AUDIT_LOG之中。

E.如何保证Logging操作和实际的操作纳入同一个Transaction中?

由于我们实际的商业逻辑的数据操作是一个基于Database的操作,而我们的Audit Logging也是一个基于Database的操作。而Audit Logging是基于这个具体商业逻辑的数据操作的。所以为了使用Logging的数据能够100%地反映真实执行了的数据操作,Logging操作和实际的数据操作应该纳入同一个Transaction中,避免造成Audit Logging记录一个执行失败的操作,或者数据操作执行成功而Logging操作执行失败。

F.权衡利弊

到现在为止,这个解决方案在功能上能够成功解决我们开篇提出的Logging要求,但是他在下面两个方面引起的不足必须引起足够的重视,不然会彻底毁掉你的应用。

  • 引起T_AUDIT_LOG_DETAIL表中的数据的急剧上升:由于对于需要进行Audit Logging的每个Table,它的每个记录的操作都会在T_AUDIT_LOG_DETAIL增加一条记录,如果这样Table,或者对这样的Table的操作过于频繁,将会造成该表中的记录急剧上升,近而影响整个应用的性能。
  • 性能问题:由于对需要进行Audit Logging的Table的每项操作都会出发Trigger,这会在一定程度影响数据操作的性能。

通过对上面的分析,我们大体知道整个解决方案的整体思路,现在我们来具体地在编程方面来进一步实现这个解决方案。

三、 解决方案

A.表的结构设计

对于一个涉及到敏感数据的企业级应用,对数据表的设计很重要,为了能够追踪每一笔数据的来龙去脉,能够确定每一笔记录被谁创建?什么时候创建?被谁最后一次修改?什么时候作的修改?如何处理并发操作?如何进行我们的Audit Logging?基于这些需求,我对每一个Table添加了下面7个Common 的字段:

  • CREATED_BY(VARCHAR):创建该记录的User ID。
  • CREATED_ON(DATETIME):纪录的创建时间。
  • LAST_UPDATED_BY(VARCHAR):记录最后一次被修改对应的User ID。
  • LAST_UPDATED_ON(DATETIME):记录最后一次修改的时间。
  • VERSION_NO(TIMESTAMP):表明该记录的版本号,用于并发操作。
  • TRANSACTION_NO(CHAR(36)):该记录最后一次修改的对用的Transaction的ID,也就是我们今天进行Audit Logging对应的那个Transaction的ID。
  • NEED_AUDIT(bit):这个将在后面的部分介绍它的用途。

当我们进行任何涉及到数据库的操作,为了保证数据的完整性,我们会把所有的操作纳入一个Transaction之中。为了有利于Auditing,我们在开始 这个Transaction之前,会生成一个基于GUID的Transaction No, 并把它更新到该Transaction涉及的每个记录的TRANSACTION_NO字段。如果某条记录是新添加的,那么我们会把CREATED_BY和LAST_UPDATED_BY赋值为当前的User,把CREATED_ON和LAST_UPDATED_ON赋值为当前的时间。如果我们要修改或者删除某条记录,我们通过获取记录的VERSION_NO和数据库中对应的数据进行比较来判断该记录时候在被当前Session取出后又被别的User修改了,从而有效地处理并发操作。

B. 整个数据处理流程

ADO.NET为我们在.NET平台下提供了简单而直接的数据操作机制。此外,通过Dataset、DataAdapter、DbCommand等一系列的Component,实现我们常用的离线的方式来操作数据库:我们通过DataAdapter获取数据填充到我们的Dataset对象,并断开Db Connection。我们通过Dataset来构建一个内存中的数据库来mapping真正Db中的数据结构,最终我们通过DataAdapter把对Dataset中的数据更新递交到Db中。我们的Audit Logging就以这样一种机制来介绍。我们通过这种离线操作模式来介绍我们的整个Log的操作流程,当然这个Audit Logging解决方案同样适合基于Connection的数据操作。


正如上图所描述的,我们首先从Db中获取数据并填充到Dataset中,然后我们把 Audit Log的基本的数据添加到一个Audit Log Dataset中,并生成(对应T_AUDIT_LOG表),一个Transaction的一个ID,我们称之为Transaction No,然后我们根据我们具体的业务逻辑来对我们用来承载获取数据的Dataset作相应的修改,并把我们生成的Transaction更新到该Dataset每个需要更新的Data Row中。然后我们把基于商业逻辑的更新和添加的Log数据向Db提交,所有的这些操作被纳入到一个单独Transaction中。当这些更新通过最终调用SQL或者Stored procedure更新到Db中后,对应的Trigger被触发,基于某个Table的数据改变的信息被添加到T_AUDIT_LOG_DETAIL中。

C. Programming

上面我们通过文字介绍了Audit logging 的整个流程,我们现在已我们最擅长的编程的角度来进一步了解这个过程。

首先我们定义了一个AuditLoggingDataSet的强类型的Dataset,该Dataset包含一个Table:T_AUDIT_LOG,映射DB中的同名T_AUDIT_LOG表。

然后我们定义了一个专门用于Audit Logging操作的Helper类:AuditLoggingHelper
该Helper包含连个Public成员,一个Property:AuditLoggingData,返回对应的Log数据。一个方法AuditLog,添加Log信息并以GUID的形式返回一个Transaction No。

None.gifusing System;
None.gif
using System.Collections.Generic;
None.gif
using System.Text;
None.gif
None.gif
namespace Artech.AuditLogging.ConsoleApp
ExpandedBlockStart.gifContractedBlock.gif
dot.gif{
InBlock.gif    
public class AuditLoggingHelper
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
InBlock.gif        
private AuditLoggingDataSet _auditLoggingData;
InBlock.gif
ExpandedSubBlockStart.gifContractedSubBlock.gif        
/**//// <summary>
InBlock.gif        
/// A strongly typed dataset to used to store the general auditoing inforamtion. 
ExpandedSubBlockEnd.gif        
/// </summary>

InBlock.gif        public AuditLoggingDataSet AuditLoggingData
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif            
get dot.gifreturn _auditLoggingData; }
ExpandedSubBlockStart.gifContractedSubBlock.gif            
set dot.gif{ _auditLoggingData = value; }
ExpandedSubBlockEnd.gif        }

InBlock.gif
ExpandedSubBlockStart.gifContractedSubBlock.gif        
/**//// <summary>
InBlock.gif        
/// Log the general auditoing information according with the current transaction.
InBlock.gif        
/// </summary>
ExpandedSubBlockEnd.gif        
/// <returns>A guid which identifies uniquely a transaction</returns>

InBlock.gif        public Guid AuditLog()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
if (this._auditLoggingData == null)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
this._auditLoggingData = new AuditLoggingDataSet();
ExpandedSubBlockEnd.gif            }

InBlock.gif
InBlock.gif            Guid transactionNo 
= Guid.NewGuid();
InBlock.gif            AuditLoggingDataSet.T_AUDIT_LOGRow auditRow
= this._auditLoggingData.T_AUDIT_LOG.NewT_AUDIT_LOGRow();
InBlock.gif
InBlock.gif            auditRow.BeginEdit();
InBlock.gif            auditRow.TRANSACTION_NO 
= transactionNo.ToString();
InBlock.gif            
//TODO: The user id is generally the account of the current login user.
InBlock.gif
            auditRow.USER_ID = "testUser";
InBlock.gif            auditRow.OPERATION_DATE 
= DateTime.Now;
InBlock.gif            auditRow.EndEdit();
InBlock.gif
InBlock.gif            
this._auditLoggingData.T_AUDIT_LOG.AddT_AUDIT_LOGRow(auditRow);
InBlock.gif
InBlock.gif            
return transactionNo;
ExpandedSubBlockEnd.gif        }

ExpandedSubBlockEnd.gif    }

ExpandedBlockEnd.gif}

None.gif

我还定义了一个专门定义了用于Data Access操作的DataAccessHelper的另一个Helper类。这个Helper类帮助我以一种简单的方式向Db获取、提交数据。我将现在下面一节中简单介绍这个DataAccessHelper。

现在我们简单地来模拟这样一个场景:我们有一个简单的处理Order的应用, 从Db中获取某个Order ID的Order信息,对获取的数据进行相应修改后被最终被提交到Db中。

我们简化了Order数据的复杂度,假设DB中对应的Table如下,通过这些是我们Dataset的结构,我将在下面一节已Sample的形式来一步一步来介绍这个场景,现在我们这些简单地通过程序来了解整个处理的流程。


我们现在来看我们的code:

None.gifusing System;
None.gif
using System.Collections.Generic;
None.gif
using System.Text;
None.gif
using System.Data;
None.gif
None.gif
namespace Artech.AuditLogging.ConsoleApp
ExpandedBlockStart.gifContractedBlock.gif
dot.gif{
InBlock.gif    
class Program
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
InBlock.gif        
static string USER_ID = "testUser";
InBlock.gif
InBlock.gif        
static void Main(string[] args)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            UpdateOrderData();
ExpandedSubBlockEnd.gif        }

InBlock.gif
InBlock.gif        
static void UpdateCommonField(DataRow row)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            row[
"LAST_UPDATED_BY"= USER_ID;
InBlock.gif            row[
"LAST_UPDATED_ON"= DateTime.Now;
InBlock.gif            
if (row.RowState == DataRowState.Detached || row.RowState == DataRowState.Added)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                row[
"CREATED_BY"= USER_ID;
InBlock.gif                row[
"CREATED_ON"= DateTime.Now;
ExpandedSubBlockEnd.gif            }

ExpandedSubBlockEnd.gif        }
  
InBlock.gif
InBlock.gif        
static OrderDataSet GetAllOrderData()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            OrderDataSet orderData 
= new OrderDataSet();
InBlock.gif            
using (DataAccessHelper dataAccessHelper = new DataAccessHelper())
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                orderData.EnforceConstraints 
= false;
InBlock.gif                dataAccessHelper.FillData(orderData.T_ORDER, CommandType.Text, 
"SELECT * FROM dbo.T_ORDER"new Dictionary<stringobject>());
InBlock.gif                dataAccessHelper.FillData(orderData.T_ORDER_DETAIL, CommandType.Text, 
"SELECT * FROM dbo.T_ORDER_DETAIL"new Dictionary<stringobject>());
InBlock.gif                orderData.EnforceConstraints 
= true;                
ExpandedSubBlockEnd.gif            }

InBlock.gif
InBlock.gif            
return orderData;
ExpandedSubBlockEnd.gif        }

InBlock.gif
InBlock.gif        
static void UpdateOrderData()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            OrderDataSet orderData 
= GetAllOrderData();
InBlock.gif            AuditLoggingHelper auditLoggingHelper 
= new AuditLoggingHelper();
InBlock.gif            Guid transactionNo 
= auditLoggingHelper.AuditLog();
InBlock.gif
InBlock.gif     OrderDataSet.T_ORDERRow orderRow 
= orderData.T_ORDER[0];
InBlock.gif            orderRow.ORDER_DATE 
= new DateTime(200511);
InBlock.gif            orderRow.SUPPLIER 
= "Dell Corporation";
InBlock.gif            orderRow.TRANSACTION_NO  
= transactionNo.ToString();
InBlock.gif            UpdateCommonField(orderRow);
InBlock.gif    
InBlock.gif
InBlock.gif            
using (DataAccessHelper dataAccessHelper = new DataAccessHelper())
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{                
InBlock.gif                dataAccessHelper.BeginTransaction();
InBlock.gif                
try
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    dataAccessHelper.UpdateData(auditLoggingHelper.AuditLoggingData.T_AUDIT_LOG);
InBlock.gif                    dataAccessHelper.UpdateData(orderData.T_ORDER);
InBlock.gif                    dataAccessHelper.UpdateData(orderData.T_ORDER_DETAIL);
InBlock.gif                    dataAccessHelper.Commit();
ExpandedSubBlockEnd.gif                }

InBlock.gif                
catch (Exception ex)
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    dataAccessHelper.Rollback();
InBlock.gif                    Console.WriteLine(ex.Message);
ExpandedSubBlockEnd.gif                }

ExpandedSubBlockEnd.gif            }
            
ExpandedSubBlockEnd.gif        }

ExpandedSubBlockEnd.gif    }

ExpandedBlockEnd.gif}

None.gif

这个程序执行的流程很简单,这里应该不需要再作进一步的说明。通过向Db提交auditLoggingHelper.AuditLoggingData.T_AUDIT_LOG,整个Audit Logging实际上只做了一半。通过前面对Logging数据的介绍,我们知道需要Log 是基于两张表:T_AUDIT_LOG和T_AUDIT_LOG_DETAIL.我现在仅仅添加了T_AUDIT_LOG这个主表的数据,具体的Log信息实际上存储在T_AUDIT_LOG_DETAIL这个子表中,而这个表中的数据是通过Trigger写入的。我们现在就来看看,这个Trigger如何写。

D.定义Trigger

我们已表T_Order为例,由于对它的添加、修改和删除都需要把 对应的数据的改变记录到T_AUDIT_LOG_DETAIL中,我们需要为这3种操作类型定义Trigger。

For Insert:'tr_order_i'

None.gifIF EXISTS (SELECT * FROM sysobjects WHERE type = 'TR' AND name = 'tr_order_i')
None.gif    
BEGIN
None.gif        
DROP  Trigger tr_order_i
None.gif    
END
None.gif
GO
None.gif
None.gif
CREATE Trigger tr_order_i ON dbo.T_ORDER 
None.gifAFTER 
INSERT
None.gif
AS
None.gif
IF UPDATE(VERSION_NO)
None.gif
BEGIN
None.gif        
INSERT [dbo].[T_AUDIT_LOG_DETAIL]
None.gif                (
[TRANSACTION_NO]
None.gif                ,
[TABLE_NAME]
None.gif                ,
[OPERATION_TYPE]
None.gif                ,
[DATA_CHANGE])                
None.gif        
SELECT INSERTED.TRANSACTION_NO
None.gif                , 
'T_ORDER'
None.gif                ,
'Insert'
None.gif                ,
'<dataChange> <after order_id ="'+CONVERT(VARCHAR,INSERTED.ORDER_ID)+'"' +
None.gif                
' order_date="' +CONVERT(VARCHAR,INSERTED.ORDER_DATE) + '"' +
None.gif                
' supplier="'+INSERTED.SUPPLIER +'"/></dataChange>'
None.gif        
FROM  INSERTED 
None.gif        
None.gif
END  
None.gif
None.gif
GO
None.gif

For Update:'tr_order_u'

None.gifIF EXISTS (SELECT * FROM sysobjects WHERE type = 'TR' AND name = 'tr_order_u')
None.gif    
BEGIN
None.gif        
DROP  Trigger tr_order_u
None.gif    
END
None.gif
GO
None.gif
None.gif
CREATE Trigger tr_order_u ON dbo.T_ORDER 
None.gifAFTER 
UPDATE
None.gif
AS
None.gif
IF UPDATE(VERSION_NO)
None.gif
BEGIN
None.gif        
INSERT [dbo].[T_AUDIT_LOG_DETAIL]
None.gif                (
[TRANSACTION_NO]
None.gif                ,
[TABLE_NAME]
None.gif                ,
[OPERATION_TYPE]
None.gif                ,
[DATA_CHANGE])
None.gif        
SELECT INSERTED.TRANSACTION_NO
None.gif                , 
'T_ORDER'
None.gif                ,
'Update'
None.gif                ,
'<dataChange> <before order_id ="'+CONVERT(VARCHAR,DELETED.ORDER_ID)+'"' +
None.gif                
' order_date="' +CONVERT(VARCHAR,DELETED.ORDER_DATE) + '"' +
None.gif                
' supplier="'+DELETED.SUPPLIER +'"/>' +
None.gif                
'<after order_id ="'+CONVERT(VARCHAR,INSERTED.ORDER_ID)+'"' +
None.gif                
' order_date="' +CONVERT(VARCHAR,INSERTED.ORDER_DATE) + '"' +
None.gif                
' supplier="'+INSERTED.SUPPLIER +'"/></dataChange>'
None.gif        
FROM DELETED INNER JOIN INSERTED ON
None.gif        DELETED.ORDER_ID 
= INSERTED.ORDER_ID           
None.gif        
WHERE INSERTED.NEED_AUDIT  = 1    
None.gif        
None.gif
END    
None.gif
None.gif
GO
None.gif

我知道对于一个Trigger来说,我们可以通过两个表INSERTED和DELETED获取原来的数据和当前的数据。所以我们可以通过INSERTED.TRANSACTION_NO获取对应的Transaction No。这个对于Insert和Update操作没有任何问题,但是对于Delete操作,INSERTED表中没有数据,我们如何获取这个必须的Transaction No呢?我们的做法的是,在数据被真正被Delete之前,先对它进行Update操作,把Transaction No赋值给它的TRANSACTION_NO字段。那么在真正触发Delete Trigger的时候,就可以通过 DELETED. TRANSACTION_NO来获得这个Transaction No。但是这又带来了一个新的问题,我们通过为一个即将被Delete的记录修改Transaction No的时候,他会触发我们上面定义的Update Trigger,那么一些错误的信息会添加到T_AUDIT_LOG_DETAIL之中,这显然是不允许的。如何来解决这个问题呢?这就要借助要的NEED_AUDIT 这个字段了。这个字段的默认值为1(true),在Delete之前我们不但修改TRANSACTION_NO,我们还将NEED_AUDIT 字段赋为0。那么Update trigger就会根据这个字段判断该Update操作是否是真正意义上的Update。这也是我们在上面的Trigger中加入了一个条件WHERE INSERTED.NEED_AUDIT = 1的原因。

下面我们来看Delete Trigger:tr_order_d

None.gifIF EXISTS (SELECT * FROM sysobjects WHERE type = 'TR' AND name = 'tr_order_d')
None.gif    
BEGIN
None.gif        
DROP  Trigger tr_order_d
None.gif    
END
None.gif
GO
None.gif
None.gif
CREATE Trigger tr_order_d ON dbo.T_ORDER 
None.gifAFTER 
DELETE
None.gif
AS
None.gif
None.gif
BEGIN        
None.gif        
None.gif        
INSERT [dbo].[T_AUDIT_LOG_DETAIL]
None.gif                (
[TRANSACTION_NO]
None.gif                ,
[TABLE_NAME]
None.gif                ,
[OPERATION_TYPE]
None.gif                ,
[DATA_CHANGE])
None.gif                
None.gif        
SELECT TRANSACTION_NO
None.gif                , 
'T_ORDER'
None.gif                ,
'Delete'
None.gif                ,
'<dataChange> <before order_id ="'+CONVERT(VARCHAR,DELETED.ORDER_ID)+'"' +
None.gif                
' order_date="' +CONVERT(VARCHAR,DELETED.ORDER_DATE) + '"' +
None.gif                
' supplier="'+DELETED.SUPPLIER +'"/></dataChange>' 
None.gif        
FROM DELETED
None.gif
END
None.gif
GO

转载于:https://www.cnblogs.com/BoKeRen/archive/2007/04/23/723998.html

相关文章:

进程间通信:同步双工管道

因为工作需要&#xff0c;需要设计出一个双工的IPC。&#xff08;转载请指明出处&#xff09;在一番比较后&#xff0c;我发现管道是比较符合我们的需求的。但是我们需求要求管道的对方是可信任的&#xff0c;而在vista以下系统是没有GetNamedPipeClientProcessId、GetNamedPip…

就因为一个笔记本,运营和产品吵得不可开交......

上班最讨厌的一件事情&#xff0c;莫过于开会&#xff0c;因为每次开会感觉就要吵架&#xff0c;这个今天开会又吵架了&#xff0c;吵架竟然是因为产品小姐姐的笔记本。产品小姐姐用了一本可擦笔记本记录会议内容&#xff0c;运营小姐姐竟然说这个本子有什么用&#xff0c;不就…

Ka的递归编程练习 Part4|Hanoi汉诺塔,双色汉诺塔的也有

1 #include <stdio.h>2 void hanoi(int s,char a,char b,char c) //a是出发盘&#xff0c;b是中途盘&#xff0c;c是结束盘 3 {4 if(s0) return;5 hanoi(s-1,a,c,b); //把最底下的从a借助c移动到b6 printf("%d from %c move to %c\n",s,a,c);7 …

一种精确从文本中提取URL的思路及实现

在今年三四月份&#xff0c;我接受了一个需求&#xff1a;从文本中提取URL。这样的需求&#xff0c;可能算是非常小众的需求了。大概只有QQ、飞信、阿里旺旺等之类的即时通讯软件存在这样的需求。在研究这个之前&#xff0c;我测试了这些软件这块功能&#xff0c;发现它们这块的…

解读 | 2019年10篇计算机视觉精选论文(上)

作者 | 神经小兮来源 | HyperAI超神经&#xff08;ID:HyperAI&#xff09;2019 年转眼已经接近尾声&#xff0c;我们看到&#xff0c;这一年计算机视觉&#xff08;CV&#xff09;领域又诞生了大量出色的论文&#xff0c;提出了许多新颖的架构和方法&#xff0c;进一步提高了视…

不错的工具:Reflector for .NET

下载地址: http://www.aisto.com/roeder/dotnet/ 注意&#xff1a;下载时要输一些注册信息&#xff0c;输入用户名时&#xff0c;中间要加一个空格。

Possible MySQL server UUID duplication for server

&#xfeff;&#xfeff;在mysql enterprise monitor监控过程中出现这样的event事件&#xff0c;Topic: Possible MySQL server UUID duplication for server 事件&#xff0c;从该提示的描述来看貌似是存在重复的uuid&#xff0c;而实际上主从关系并不存在重复的uuid。主从关…

使用VC实现一个“智能”自增减线程池

工作中接手了一款产品的改造。因为该产品可能使用很多线程&#xff0c;所以产品中使用了线程池。&#xff08;转载请指明来自BreakSoftware的CSDN博客&#xff09; 线程池的一个优点是降低线程创建和销毁的频率&#xff1b;缺点是可能在比较闲的时候还存在一定数量的空闲线程。…

国内外财务软件科目结构的比较

科目结构是整个会计核算的基础。国内外财务软件都是任意定义科目的分段及科目编码长度&#xff0c;一般都能支持六段到九段。但科目结构在不同的国家有不同的规范&#xff0c;因而在不同的财务软件中也就有不同的控制。在科目分类上&#xff0c;国内外有明显的区别。国外财务软…

朋友圈装死,微博蹦迪,Python教你如何掌握女神情绪变化 | CSDN博文精选

作者 | A字头来源 | 数据札记倌很多人都是在朋友圈装死&#xff0c;微博上蹦迪。微信朋友圈已经不是一个可以随意发表心情的地方了&#xff0c;微博才是&#xff01;所以你不要傻傻盯着女神的朋友圈发呆啦&#xff01;本文教你如何用Python自动通知女神微博情绪变化&#xff0c…

java异常笔记

Throwable是所有Java程序中错误处理的父类&#xff0c;有两种资类&#xff1a;Error和Exception。Error&#xff1a;表示由JVM所侦测到的无法预期的错误&#xff0c;由于这是属于JVM层次的严重错误&#xff0c;导致JVM无法继续执行&#xff0c;因此&#xff0c;这是不可捕捉到的…

2019最新进展 | Transformer在深度推荐系统中的应用

作者 | Alex-zhai来源 | 深度传送门&#xff08;ID:deep_deliver&#xff09;【导读】最近基于Transformer的一些NLP模型很火&#xff08;比如BERT&#xff0c;GPT-2等&#xff09;&#xff0c;因此将Transformer模型引入到推荐算法中是近期的一个潮流。Transformer比起传统的L…

自己架设windows升级服务器

大部分对计算机比较熟悉的朋友都知道&#xff0c;通常安装好Windows 操作系统后要做的第一件事就是上Windows Update网站去给Windows 安装补丁程序&#xff0c;否则各种漏洞对系统就是一个很大的威胁。不过遗憾的是很多人还没有这样的意识&#xff0c;疏忽了给系统打补丁。这也…

内嵌IE网页窗口中消除IE默认脚本设置影响的方法

随着人们对客户端软件界面要求的不断提高,软件开发商面临着一个问题:如何快速廉价开发出各种丰富效果的UI界面。设计出一套丰富控件的界面库是不容易的,且产品经理丰富的想法和UED对效果的追求,往往会使程序员疲于编写这些“效果控件”。目前市面上使用的很多界面库是基于X…

win7 64位操作系统中 Oracle 11g 安装教程(图解)

1.下载Oracle 11g R2 for Windows版本&#xff0c;下载地址如下  官方网站&#xff1a;  http://download.oracle.com/otn/nt/oracle11g/112010/win32_11gR2_database_1of2.zip http://download.oracle.com/otn/nt/oracle11g/112010/win32_11gR2_database_2of2.zip 2.解压两…

使用APIHOOK实现进程隐藏

今天翻出一些今年前写的代码。其中一个是09年&#xff0c;我帮一个读研的同学写的一个“无公害恶意”程序。大致要求就是要实现自启动和自我隐藏。我使用的都是些简单的技术&#xff0c;只是实现自我隐藏稍微让我花费了点时间写算法。其实这个算法也很简单&#xff0c;就是大学…

程序员创业前要做哪些准备?

作者 | hsm_computer出品 | CSDN博客在互联网时代&#xff0c;不少干IT的人白手起家&#xff0c;在短短的几年里通过努力干出了一番事业&#xff0c;有房有车有公司&#xff0c;在人前也很光鲜。这就吸引了更多的程序员想要通过自主创业来实现财务自由。殊不知&#xff0c;创业…

Flex编码过程

Flex编码过程当我们开发一个Flex程序&#xff0c;我们重复其他类型网络程序的过程&#xff0c;例如HTML,JSP,ASP和CFML。创建一个有用的Flex程序是很容易的&#xff1a;打开我们最喜欢的文本编辑器&#xff0c;例如Flex Builder&#xff0c;输入XML标签&#xff0c;编译成为SWF…

BufferedWriter

package JBJADV003;import java.io.*;public class BufferedWriterTest { public static void main(String[] args) { try { //创建一个FileWriter 对象 FileWriter fwnew FileWriter("c:\\myDoc\\hello.txt"); //创建一个BufferedWriter 对象 BufferedWriter bwnew…

使用VC内嵌Python实现的一个代码检测工具

最近组内准备整顿代码&#xff0c;领导让我写个简单的python脚本分析代码中注释的行数和无效注释。因为这个需求不是很急&#xff0c;所以我想把简单的事情做复杂点。于是就写了一个用VC内嵌Python&#xff0c;并通过模拟按键和发消息去控制其他软件的工具。&#xff08;转载请…

Python如何实现24个微信大群万人同步转发直播?

作者 | 猪哥66来源 | CSDN博客今天我们来学习微信机器人多群转发做同步图文直播&#xff01;一、背景介绍猪哥一年前在建Python学习群的时候就说过&#xff0c;要邀请企业大佬来学习群做直播。其实文章早就写好了&#xff0c;但是一直没有找到好的转发软件&#xff0c;所以耽搁…

ITSM实施三招[案例]

当前国外成熟的ITSM解决方案的实施成本相对比较高&#xff0c;使一些对成本较敏感的的IT部门&#xff0c;成为ITSM实施的一个真空区。对于国内起步阶段的ITSM&#xff08;IT服务管理&#xff09;实施来说&#xff0c;南航的ITSM实施之路是一个借鉴。 南航it环境 在各大航空公司…

lr手工添加关联函数的步骤:

点击“确定”后&#xff1a; 如何修改已经创建好的关联规则&#xff1a;

新闻内容实现分页

/**//// <summary> /// 新闻内容分页 /// </summary> /// <param name"content">新闻内容</param> /// <param name"extension">扩展名(aspx,html..)</param> /// <returns></returns>pub…

使用自己的数据集训练MobileNet、ResNet实现图像分类(TensorFlow)| CSDN博文精选

作者 | pan_jinquan来源 | CSDN博文精选之前写了一篇博客《使用自己的数据集训练GoogLenet InceptionNet V1 V2 V3模型&#xff08;TensorFlow&#xff09;》https://panjinquan.blog.csdn.net/article/details/81560537&#xff0c;本博客就是此博客的框架基础上&#xff0c;完…

VC下提前注入进程的一些方法1——远线程不带参数

前些天一直在研究Ring3层的提前注入问题。所谓提前注入&#xff0c;就是在程序代码逻辑还没执行前就注入&#xff0c;这样做一般用于Hook API。&#xff08;转载请指明出处&#xff09;自己写了个demo&#xff0c;在此记下。 我的demo使用了两种注入方式&#xff1a;1 远线程&a…

【转】用示例说明索引数据块中出现热块的场景,并给出解决方案

文章转自&#xff1a;http://www.luocs.com/archives/582.html

VC下提前注入进程的一些方法2——远线程带参数

在前一节中介绍了通过远线程不带参数的方式提前注入进程&#xff0c;现在介绍种远线程携带参数的方法。&#xff08;转载请指明出处&#xff09; 1.2 执行注入的进程需要传信息给被注入进程 因为同样采用的是远线程注入&#xff0c;所以大致的思路是一样的&#xff0c;只是在细…

芬兰开放“线上AI速成班”课程,全球网民均可免费观看

出品 | AI科技大本营&#xff08;ID:rgznai100&#xff09;去年&#xff0c;芬兰推出了一个免费的“人工智能线上速成班”项目&#xff0c;目的是向该国民众教授与新技术有关的知识。现在&#xff0c;作为送给全世界的圣诞节礼物&#xff0c;这个项目已面向全球网民开放访问&am…