2012年1月15日
#
这是一个非常普遍的需求,当我们向一张表插入记录的时候,如果该记录已经存在(通常是主键相同/其他条件匹配),我们就不应当重复插入记录,而是更新该记录。
于是我们最原始的想法一般都是有这样一个存储过程/方法/SQL,先判断是否该记录是否存在,然后决定究竟是更新还是插入。
首先要避免的是用程序来回读取,它们起码要被原子性地处理在一起,否则先不说先判断再执行带来增加一次数据库访问给应用带来性能上的开销外,也无法保证在高并发的前提下的正确性。
在参考资料6中,提供了如下一种SELECT XXX, IF XXX IS NULL INSERT, ELSE, UPDATE,当然这也没有什么错,但从SQL Server 2008开始提供MERGE操作专门处理诸如此类的操作,当然也可以含有删除,但这里只提到INSERTorUPDATE。
借用MongoDB的一个方法名Update + Insert = Upsert,写了下面的方法。其实在Microsoft提供的示例也清晰地描述了这个问题,请大家参看参考资料1。
USE [MyDB];
GO
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[UpsertUser]') AND type in (N'P', N'PC'))
DROP PROCEDURE [dbo].[UpsertUser]
GO
USE [MyDB];
GO
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[#AnalysisForUpsertUser]') AND type in (N'U'))
DROP TABLE [dbo].[#AnalysisForUpsertUser]
GO
CREATE TABLE #AnalysisForUpsertUser
(
ExistingUserID int,
ExistingUserName nvarchar(50),
ExistingUserFullName nvarchar(50),
ExistingPassword nvarchar(50),
ExistingState bit,
ExistingEmail nvarchar(50),
ExistingUpdateBy nvarchar(50),
ExistingUpdateTime datetime,
ExistingRemark nvarchar(50),
ActionTaken nvarchar(10),
NewUserID int,
NewUserName nvarchar(50),
NewUserFullName nvarchar(50),
NewPassword nvarchar(50),
NewState bit,
NewEmail nvarchar(50),
NewUpdateBy nvarchar(50),
NewUpdateTime datetime,
NewRemark nvarchar(50),
);
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE dbo.UpsertUser
@UserID int,
@UserName nvarchar(50),
@UserFullName nvarchar(50),
@Password nvarchar(50),
@State bit,
@Email nvarchar(50),
@UpdateBy nvarchar(50),
--@UpdateTime datetime,
@Remark nvarchar(50)
AS
BEGIN
SET NOCOUNT ON;
MERGE tbUser AS target
USING (SELECT @UserID,@UserName,@UserFullName,@Password,@State,@Email,@UpdateBy,@Remark) AS source
(UserID,UserName,UserFullName,Password,State,Email,UpdateBy,Remark)
ON (target.UserID = source.UserID)
WHEN MATCHED THEN
UPDATE SET
UserName = source.UserName
,UserFullName = source.UserFullName
,Password = source.Password
,State = source.State
,Email = source.Email
,UpdateBy = source.UpdateBy
,UpdateTime = GETDATE()
,Remark = source.Remark
WHEN NOT MATCHED THEN
INSERT (UserID,UserName,UserFullName,Password,State,Email,UpdateBy,UpdateTime,Remark)
VALUES (source.UserID,source.UserName,source.UserFullName,source.Password,source.State,source.Email,source.UpdateBy, GETDATE(),source.Remark)
OUTPUT deleted.*, $action, inserted.* INTO #AnalysisForUpsertUser;
END;
GO
EXEC UpsertUser @UserID = 6887, @UserName = N'test6887', @UserFullName = N'编辑所有测试', @Password = N'YCRrXZzNrNU=',
@State = 1, @Email = N'test3@microsoft.com', @UpdateBy = N'PROGRAM', @Remark = N'可以查看所有项目,并可以修改.';
EXEC UpsertUser @UserID = 6889, @UserName = N'test6889', @UserFullName = N'编辑所有测试', @Password = N'YCRrXZzNrNU=',
@State = 1, @Email = N'test3@microsoft.com', @UpdateBy = N'PROGRAM', @Remark = N'可以查看所有项目,并可以修改.';
EXEC UpsertUser @UserID = 6882, @UserName = N'test6882', @UserFullName = N'编辑所有测试', @Password = N'YCRrXZzNrNU=',
@State = 1, @Email = N'test3@microsoft.com', @UpdateBy = N'PROGRAM', @Remark = N'可以查看所有项目,并可以修改.';
SELECT * FROM [MyDB].[dbo].[tbUser]
SELECT * FROM #AnalysisForUpsertUser
GO;
DROP TABLE #AnalysisForUpsertUser;
GO
参考资料:
1、MERGE (Transact-SQL)
2、使用 MERGE 插入、更新和删除数据
3、OUTPUT 子句 (Transact-SQL)
4、判断SQL数据库中函数、存储过程等是否存在的方法
5、T-SQL Insert or update
6、源码:《WebForm :Nearforums论坛 v7.0源码》源文件浏览
2012年1月3日
#
假设有这么一个场景:
表1和表2,假设结构相同(这不是重点),两张表都以userId作为Key。目标是从表1的数据同步到表2。
实现:1、表1中有,表2中没有的数据,从表1同步到表2。
2、表1中有,表2中也有的数据,以表1的各个字段为准。
3、表1中没有,表2中有的数据,在表2中继续保留。
做法1:
两个MA,分别以userId作为Anchor,DistinguishedName,并且字段分别映射。
为MV编写Provisioning代码,并设置选项为启用MVExtension。
同步顺序:表1 FullImport -> 表2 FullImport -> 表2 FullSync -> 表1 FullSync -> 表2 Export
存在问题:
当表2中原本存在数据,且该数据已经在表1中存在的时候,该条记录将引发ObjectAlreadyExistsException异常,该条记录没有处理,但正确的行为是不是应该是Join?遇到这个情况(异常)正确的解法是什么?
做法2:(改进做法1)
考虑到有ObjectAlreadyExistsException异常,因此打算在Provisioning代码中将此异常抛弃,算作正常行为(因为此时对象确实存在),这样FIM应该按照自行逻辑进行Join操作?但是事实上,并非我所想(应该是理解有误),MV数据里面既有表1的数据,也有表2的数据,互不相干。
解决方案:
当表1中的数据在表2种已经存在的时候(第一次同步)会抛出ObjectAlreadyExistException,应先关闭MV扩展(Tools->Options->清空第一个复选框),执行表2的FullImport->FullSync,然后再打开MV扩展,执行正常同步(表1的FullSync)。
在实际操作过程中,其实通常并不需要先关闭MV扩展,只需要将表2先做一次FullSync,这样表1再做FullSync的时候,就可以避免这个问题了。
问题描述:
从A导入数据,同步到B,在A系统中删除数据,在B系统中删除数据。
前提:A、B已经完成一次FULL_IMPORT、FULL_SYNC,假设在A中的数据在B中的数据(不考虑过滤),都一一对应。
按照之前的思路,在A中删除一条记录后,通过FULL_IMPORT的时候,在CS_A中会删除记录A,通过FULL_SYNC,因为CS_B中的DATA_1仍然与MV保持Connected,因此包括MV在内一直都会存在该条记录。
解决方案:
1、 首先要让MV中把这条记录删除才可能导致B系统删除该数据。
之所以出现以上现象的原因是在于MV的默认配置所决定的,右键对应的MV,“Configuration Object Deletion Rule…”,这是改变MV对象删除规则的。该设置默认为第一项,也就是考虑到所有MA都有可能成为源,当某MV对象与所有MA都断开联系后,才删除对应的MV,类似C++的引用计数。我们这里的需求是,一旦A中删除,就将其他系统中的该记录删除,因此这里就需要配置成第二项,任何一个被勾选的对象被删除之后,都删除该MV对象。
2、 完成了第一步还不够的,因为它只提到了删除MV中的对象,但并不会删除CS_B中的对象,因此这时CS_B中的对象是孤立无援的,在FIM中是disconnected object,这样的对象会在下次同步的时候,继续参与规则讨论。因此我们要把这种没有户口的对象删掉。做法就是在对应的MA的配置中,配置“Configure Deprovisioning”,指示在一个CS对象与MV失去联系后的行为,选择第三项“Stage a delete on the object for the next export run”,该选项表明在失去联系后在接下来的export操作执行的时候删除数据。这里会删除connected data source中的数据也就是DB_A,但是不会删除CS_A中的数据,因为这一步操作在下一次FULL_IMPORT的时候是会被处理掉的。
3、 执行FULL_SYNC、EXPORT,数据依次从MV、DB_B中删除,检查DB_A,确实删掉了,执行B.FULL_IMPORT,CS_B中的数据也删除了,至此完成了这条记录的整个删除生命周期。
最近在做FIM的时候,发现Provisioning的项无法被添加了,然后发现自己在重构的时候,出现了一点儿画蛇添足。
首先说一下为什么要做Provisioning,原因是这样的,在FIM同步里面,假设有一个数据源,另一个目标数据源,目的是从一个数据源将数据导入到目标数据源中。默认情况下,FIM只处理两边都存在的数据,假设有这样两个数据:
表名:DB1.Table1
数据:
aUser, aUserName, aMobile
bUser, bUserName, bMobile
表名:DB2.Table2
bUser, bUserName1, bMobile1
cUser, cUserName1, cMobile1
默认情况下,两张表中,只有INNER JOIN的关系,也就是说bUser被关联了。这样在同步的时候,bUserName将会覆盖bUserName1,bMobile将会覆盖bMobile1,而aUser不会加入到DB2.Table2中。
而在实际的情况通常是DB1.Table1表代表一个类似HR的系统,新增一个员工的时候,我们希望新来的员工会出现在所有的第三方系统中,因此这个aUser就需要被强加到DB2.Table2中。
这需要我们为FIM启用Provisoion代码:(Tools->Options->Enable metaverse rules extension + Enable Provisioning Rules Extension)

然后我们在MVExtension的代码中实现接口void IMVSynchronization.Provision(MVEntry mventry)。
在我们进行同步的时候,我们需要先将两个数据源的数据都导入(Import)各自的CS中,为了区分不同的数据源的数据,我们为他们分别指定了dataSource的属性,这里我们用一个常量来表示。
当我们执行哪个MA同步的时候,这个dataSource的值(mventry[“dataSource”])就会是对应MA设置的那个常量,我们也就因此可以在MVExtensions代码中区分不同数据的来源了。
运行各个MA的时候,各个MA所对应的CS数据会被逐条通过mventry传入MVExtension中进行Provision。也就是说当我们执行DB1.Table1_MA的FULL_SYNC的时候,aUser和bUser都会被导入。当bUser导入的时候,我们可以想象,这个时候,如果我们不做任何事情,就离开这个Provision,按照默认的配置,Join-Rules将被激活,DB1.Table1_MA中的bUser将和DB2.Table2_MA中的bUser建立关联。于是会有一条Join-Rules:

那么aUser呢?如果不做Provisioning,它将被忽略。这个时候,aUser的ConnectedMAs中只有DB1.Table1_MA,而没有DB2.Table2_MA,因为它还不曾与之关联。
下面是一段标准的将DB1.Table1_MA中的aUser添加到DB2.Table2_MA的方法,当遇到aUser的mventry的时候,我们取得ConnectedMAs,然后将它添加到DB2.Table2_MA的CS中,并创建Connector。
private void ProvisioningFIMSys1User1(MVEntry mventry)
{
try
{
Logger.Get().Write("-----ProvisioningFIMSys1User1 start-----");
ConnectedMA MA = null;
if (TryGetMA(mventry, MAs.FIMSys1_User1_MA.NAME, out MA))
{
string dn = string.Empty;
Logger.Get().Write(string.Format("MA.Connectors.Count == {0}", MA.Connectors.Count));
if (MA.Connectors.Count == 0) // create new one
{
CSEntry csEntry = null;
Logger.Get().Write(string.Format("mventry[\"{0}\"].IsPresent = {1}", MVs.Person.Attributes.accountName, mventry[MVs.Person.Attributes.accountName].IsPresent));
if (mventry[MVs.Person.Attributes.accountName].IsPresent)
{
dn = mventry[MVs.Person.Attributes.accountName].StringValue;
Logger.Get().Write(string.Format("dn.0 = {0}", dn));
csEntry = MA.Connectors.StartNewConnector(MVs.Person.NAME);
// create DN for csEntry.
csEntry.DN = MA.CreateDN(dn);
csEntry.CommitNewConnector();
csEntry[MAs.FIMSys1_User1_MA.Attributes.UniqueID].StringValue = mventry[MVs.Person.Attributes.accountName].StringValue;
csEntry[MAs.FIMSys1_User1_MA.Attributes.UserName].StringValue = mventry[MVs.Person.Attributes.displayName].StringValue;
csEntry[MAs.FIMSys1_User1_MA.Attributes.Mobile].StringValue = mventry[MVs.Person.Attributes.mobile].StringValue;
Logger.Get().Write("-----csEntry.CommitNewConnector(); -----");
}
}
}
}
catch (Exception ex)
{
Logger.Get().Write("Exception:" + ex.ToString());
throw ex;
}
finally
{
Logger.Get().Write("-----ProvisioningFIMSys1User1 end-----");
}
}
这个TryGetMA是一个用来获取MA的方法:
public bool TryGetMA(MVEntry mventry, string MAname, out ConnectedMA ma)
{
ma = null;
bool result = false;
if (mventry == null)
result = false;
else
{
ma = mventry.ConnectedMAs[MAname];
Logger.Get().Write("GET MA: " + MAname);
if (ma == null)
result = false;
else
result = true;
}
return result;
}
下面看看我做了什么蠢事,在之前它是这样写的(不正确):
public bool TryGetMA(MVEntry mventry, string MAname, out ConnectedMA ma)
{
ma = null;
bool result = false;
if(mventry == null)
result = false;
else
{
if (ExistsMA(mventry, MAname))
{
ma = mventry.ConnectedMAs[MAname];
Logger.Get().Write("GET MA: " + MAname);
}
if(ma == null)
result = false;
else
result = true;
}
return result;
}
仔细看一下这个ExistsMA:
private bool ExistsMA(MVEntry mventry, string MAname)
{
if (mventry != null && mventry.ConnectedMAs.Count > 0)
{
foreach (ConnectedMA ma in mventry.ConnectedMAs)
{
if (ma.Name == MAname) return true;
}
}
return false;
}
倒不是这个方法抛出了异常。而是因为当这个mventry在还没有与该MA建立关联的时候,我就试图去寻找它,当然返回的结果一定是false,于是我永远都无法获得这个MA。
2011年12月1日
#
遇到读取权限的问题,TFS会对文件做只读处理,这样在ASP.NET Development Server使用FileMode.Open,的时候,会出现以下异常。
“/”应用程序中的服务器错误。
对路径“E:\MyCSharpProject\Projects\tfs.10.0.6.41\Configs\MenuItems.xml”的访问被拒绝。
说明: 执行当前 Web 请求期间,出现未经处理的异常。请检查堆栈跟踪信息,以了解有关该错误以及代码中导致错误的出处的详细信息。
异常详细信息: System.UnauthorizedAccessException: 对路径“E:\MyCSharpProject\Projects\tfs.10.0.6.41\Configs\MenuItems.xml”的访问被拒绝。
ASP.NET 无权访问所请求的资源。请考虑对 ASP.NET 请求标识授予访问此资源的权限。ASP.NET 有一个在应用程序没有模拟时使用的基进程标识(通常,在 IIS 5 上为 {MACHINE}\ASPNET,在 IIS 6 和 IIS 7 上为网络服务,在 IIS 7.5 上为配置的应用程序池标识)。如果应用程序正在通过 <identity impersonate="true"/> 模拟,则标识将为匿名用户(通常为 IUSR_MACHINENAME)或经过身份验证的请求用户。
要将 ASP.NET 访问权限授予某个文件,请在资源管理器中右击该文件,选择“属性”,然后选择“安全”选项卡。单击“添加”添加适当的用户或组。突出显示 ASP.NET 帐户,选中所需访问权限对应的框。
源错误: 行 15: XmlSerializer serializer = new XmlSerializer(typeof(T));
行 16: // using (FileStream fileStream = new FileStream(xmlFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
行 17: using (FileStream fileStream = new FileStream(xmlFilePath, FileMode.Open))
行 18: {
行 19: return serializer.Deserialize(fileStream) as T;
源文件: E:\MyCSharpProject\Projects\tfs.10.0.6.41\Utility\XmlHelper.cs 行: 17
解决方案:
将代码修改成:
using (FileStream fileStream = new FileStream(xmlFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
即可。
2011年10月16日
#
本文所提及的三种方式,不是原创,方法都来自互联网,出处不详。
谈到这个开源日志工具,应该大家都耳熟能详,从http://logging.apache.org/log4net/可以下载到它。
在官方提供的关于数据库插入日志的文档中,我们能看到,我们可以在Message中写入各种各样的文本,对于logger.Info(message, exception),如何才能加入自定义的内容呢?如果我们都只是把他们写入Message,将不便于日后查找。举个例子,比如我们对登录有个日志,对退出也有个日志,如何才能区分二者呢,这时候我们希望对我们的Action有个记录,可能就需要一个Action字段。样子可能是:
logger.Info(actionName, message, exception)。
log4net是开源项目,但是文档并不详细,其实网上对这个的研究已经有很多了,也可以很方便搜到,本文就把它们做一个总结放在一起。
其实对于以上类型的日志,只是Log4net项目的一个日志类型,叫做PatternLayout,就是根据配置文件中提到的Pattern进行输出,让我们瞅一眼配置文件(该配置文件来自官方文档http://logging.apache.org/log4net/release/config-examples.html),被指定为PatternLayout的方法,在PatternParser类中被识别,比如有个字符串为“%message%newline --- hello world %date”,PatternParser类的目的就是识别出其中的%message, %newline, %date,具体怎么做到的,请查看PatternParser类。除了%message之外,还有选项(option)的概念,%property{XXX}就是用到了这个特性,也是在PatternParser.ParseInternal中进行演算的。
<appender name="AdoNetAppender" type="log4net.Appender.AdoNetAppender">
<bufferSize value="100" />
<connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<connectionString value="data source=[database server];initial catalog=[database name];integrated security=false;persist security info=True;User ID=[user];Password=[password]" />
<commandText value="INSERT INTO Log ([Date],[Thread],[Level],[Logger],[Message],[Exception]) VALUES (@log_date, @thread, @log_level, @logger, @message, @exception)" />
<parameter>
<parameterName value="@log_date" />
<dbType value="DateTime" />
<layout type="log4net.Layout.RawTimeStampLayout" />
</parameter>
<parameter>
<parameterName value="@thread" />
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%thread" />
</layout>
</parameter>
<parameter>
<parameterName value="@log_level" />
<dbType value="String" />
<size value="50" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%level" />
</layout>
</parameter>
<parameter>
<parameterName value="@logger" />
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%logger" />
</layout>
</parameter>
<parameter>
<parameterName value="@message" />
<dbType value="String" />
<size value="4000" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%message" />
</layout>
</parameter>
<parameter>
<parameterName value="@exception" />
<dbType value="String" />
<size value="2000" />
<layout type="log4net.Layout.ExceptionLayout" />
</parameter>
</appender>
其中layout部分如果是指定PatternLayout的,则是本文所要涉及的重点,比如%message。
方法1,通过log4net提供的ThreadContext,设置属性的值,详见Info1部分。
方法2,通过PatternLayout的AddConvert,增加用于处理扩展部分(系统预制了一些包括%date、%message),此处做了一个通用的XPatternCustomLayout,用来截取%property{XXX}款式的匹配转换,详见Info2部分。
方法3,通过做一个类似PatternLayout的机制来实现,缺点也是比较明显的,需要针对每一个%XXXX做预制,这样的话,如果需要临时再增加,就比较困难了。但该方法有助于理解log4net PatternLayout的机制。
具体的内容就不在这里详述了,代码这玩意儿需要跑跑才清楚。
要运行代码,本文使用了本地数据库(一个随着压缩包提供的数据库文件,sdf,用到System.Data.SqlServerCe4.0,随Microsoft Visual Studio 2010提供),可以更换成SQLServer等标准数据库来使用,注意各配置文件内的描述。
针对方法1、2、3分别提供了配置文件log4net1.xml、log4net2.xml、log4net3.xml,只需要在Web.config文件中对应的字眼修改即可。
另外本地数据库需要指定路径,共4处需要修改,Web.config、log4net1.xml、log4net2.xml、log4net3.xml。
代码下载:
本地下载 SVN(svn checkout http://v-labs.googlecode.com/svn/trunk/log4net v-labs-read-only )
2011年8月22日
#
安装MySQL的时候,无法删除服务,在Start Service的时候,出错,Could not start the service MySQL. Error:0
到控制面板里查看服务,存在MySQL服务(名字可能是别的比如MySQL4,具体看安装时候的配置)。
右键启动,出现“在 本地计算机 无法启动MySQL服务”,“错误1067:进程意外终止。”
这个问题通常是因为当前实例已经存在,并且MySQL不是全新安装(有垃圾文件未清理,卸载也一样)。
步骤1:卸载MySQL。
步骤2:删除服务:运行命令行,cmd,sc delete mysql,其中mysql为服务的名称。
步骤3:删除垃圾文件:Windows7下路径是C:\ProgramData\MySQL,直接删除即可。
步骤4:重新安装MySQL。
2011年8月4日
#
摘要: 通过流的下载的方式下载文件的需求很普遍,也基本不太会错,但最近用VPN(这套系统用的是网页反代的方式来实现),下载的Excel却无法打开,仔细检查后通过增加ContentType,修正了这个问题。代码分享如下:byte[] bytes = System.IO.File.ReadAllBytes(path); ms.Write(bytes, 0, bytes.Length); Response.A...
阅读全文
2011年5月24日
#
摘要: 如何对已经有数据的表增加非空字段? 在新增字段的时候,所谓的新增字段没有默认值,所以会被系统提示无法添加,这时候,需要先增加一列可空字段,然后修改可空字段的所有值,直到没有空字段为止,然后打开设计器或者编写脚本,将该列修改为非空。 1、ALTER TABLE [Sechema1].[Sechema2].[TableName] ADD <NewColumnName> <TYPE> NULL ; 2、...
阅读全文
2011年5月22日
#
摘要: script标签“<script src”都是按顺序下载的,如果某个文件下载失败,则会触发onerror事件:<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www....
阅读全文