2019年7月20日 By mikel 分类: ASP.NET MVC

来源: 【半小时大话.net依赖注入】(下)详解AutoFac+实战Mvc、Api以及.NET Core的依赖注入 – 在7楼’s Blogs

系列目录#

  1. 第一章|理论基础+实战控制台程序实现AutoFac注入
  2. 第二章|AutoFac的使用技巧
  3. 第三章|实战Asp.Net Framework Web程序实现AutoFac注入
  4. 第四章|实战Asp.Net Core自带DI实现依赖注入
  5. 第五章|实战Asp.Net Core引入AutoFac的两种方式

前言#

本来计划是五篇文章的,每章发个半小时随便翻翻就能懂,但是第一篇发了之后,我发现.NET环境下很多人对IoC和DI都很排斥,搞得评论区异常热闹。
同一个东西,在Java下和在.NET下能有这么大的差异,也是挺有意思的一件事情。

所以我就把剩下四篇内容精简再精简,合成一篇了,权当是写给自己的一个备忘记录了。
GitHub源码地址:https://github.com/WangRui321/Ray.EssayNotes.AutoFac

源码是一个虚构的项目框架,类似于样例性质的代码或者测试程序,里面很多注释,对理解DI,或怎么在MVC、WebApi和Core Api分别实现依赖注入有很好的帮助效果。
所以,以下内容,配合源码食用效果更佳~

第一部分:详解AutoFac用法#

名词解释#

老规矩,理论先行。

组件(Components)#

一串声明了它所提供服务和它所消费依赖的代码。

可以理解为容器内的基本单元,一个容器内会被注册很多个组件,每个组件都有自己的信息:比如暴露的服务类型、生命周期域、绑定的具象对象等。

服务(Services)#

一个在提供和消费组件之间明确定义的行为约定。

和项目中的xxxService不同,AutoFac的服务是对容器而言的,可以简单的理解为上一章讲的组件的暴露类型(即对外开放的服务类型),也就是As方法里的东西:

Copy
builder.RegisterType<CallLogger>()
       .As<ILogger>()
       .As<ICallInterceptor>();

这里,针对同一个注册对象(CallLogger),容器就对外暴露了两个服务(service),ILogger服务和ICallInterceptor服务。

生命周期作用域(LifeTimeScope)#

  • 生命周期

指服务实例在你的应用中存在的时长:从开始实例化到最后释放结束。

  • 作用域

指它在应用中能共享给其他组件并被消费的作用域。例如, 应用中有个全局的静态单例,那么该全局对象实例的 “作用域” 将会是整个应用。

  • 生命周期作用域

其实是把这两个概念组合在了一起, 可以理解为应用中的一个工作单元。后面详细讲。

怎么理解它们的关系#

容器是一个自动售货机,组件是放在里面的在售商品,服务是商品的出售名称
把商品(项目里的具象对象)放入自动售货机(容器)上架的过程叫注册
注册的时候会给商品贴上标签,标注该商品的名称,这个名称就叫服务
我们还可以标注这个商品的适用人群和过期时间等(生命周期作用域);
把这个包装后的商品放入自动售货机后,它就变成了在售商品(组件)。
当有顾客需要某个商品时,他只要对着售货机报一个商品名(服务名),自动售货机找到对应商品,抛出给客户,这个抛给你的过程,就叫做注入你;
而且这个售货机比较智能,抛出前还可以先判断商品是不是过期了,该不该抛给你。

注册组件#

即在容器初始化时,向容器内添加对象的操作。AutoFac封装了以下几种便捷的注册方法:

反射注册#

直接指定注入对象与暴露类型,使用RegisterType<T>()或者RegisterType(typeof(T))方法:

Copy
builder.RegisterType<StudentRepository>()
    .As<IStudentRepository>();
builder.RegisterType(typeof(StudentService))
    .As(typeof(IStudentService));

实例注册#

将实例注册到容器,使用RegisterInstance()方法,通常有两种:

  • new出一个对象注册:
Copy
var output = new StringWriter();
builder.RegisterInstance(output).As<TextWriter>();
  • 注册项目已存在单例:
Copy
builder.RegisterInstance(MySingleton.Instance).ExternallyOwned();

Lambda表达式注册#

Copy
builder.Register(x => new StudentRepository())
    .As<IStudentRepository>();
builder.Register(x => new StudentService(x.Resolve<IStudentRepository>()))
    .As<IStudentService>();

利用拉姆达注册可以实现一些常规反射无法实现的操作,比如一些复杂参数注册。

泛型注册#

最常见的就是泛型仓储的注册:

Copy
builder.RegisterGeneric(typeof(BaseRepository<>))
    .As(typeof(IBaseRepository<>))
    .InstancePerLifetimeScope();

条件注册#

通过加上判断条件,来决定是否执行该条注册语句。

  • IfNotRegistered

表示:如果没注册过xxx,就执行语句:

Copy
builder.RegisterType<TeacherRepository>()
    .AsSelf()
    .IfNotRegistered(typeof(ITeacherRepository));

只有当ITeacherRepository服务类型没有被注册过,才会执行该条注册语句。

  • OnlyIf

表示:只有…,才会执行语句:

Copy
builder.RegisterType<TeacherService>()
    .AsSelf()
    .As<ITeacherService>()
    .OnlyIf(x => 
            x.IsRegistered(new TypedService(typeof(ITeacherRepository)))||
            x.IsRegistered(new TypedService(typeof(TeacherRepository))));

只有当ITeacherRepository服务类型或者TeacherRepository服务类型被注册过,才会执行该条注册语句。

程序集批量注册#

最常用,也最实用的一个注册方法,使用该方法最好要懂点反射的知识。

Copy
        /// <summary>
        /// 通过反射程序集批量注册
        /// </summary>
        /// <param name="builder"></param>
        public static void BuildContainerFunc8(ContainerBuilder builder)
        {
            Assembly[] assemblies = Helpers.ReflectionHelper.GetAllAssemblies();

            builder.RegisterAssemblyTypes(assemblies)//程序集内所有具象类(concrete classes)
                .Where(cc =>cc.Name.EndsWith("Repository")|//筛选
                            cc.Name.EndsWith("Service"))
                .PublicOnly()//只要public访问权限的
                .Where(cc=>cc.IsClass)//只要class型(主要为了排除值和interface类型)
                //.Except<TeacherRepository>()//排除某类型
                //.As(x=>x.GetInterfaces()[0])//反射出其实现的接口,默认以第一个接口类型暴露
                .AsImplementedInterfaces();//自动以其实现的所有接口类型暴露(包括IDisposable接口)

            builder.RegisterGeneric(typeof(BaseRepository<>))
                .As(typeof(IBaseRepository<>));
        }

如上会批量注册项目中所有的Repository和Service。

属性注入#

讲属性注入之前,要先看下构造注入。

  • 构造注入
    即解析的时候,利用构造函数注入,形式如下:
Copy
    /// <summary>
    /// 学生逻辑处理
    /// </summary>
    public class StudentService : IStudentService
    {
        private readonly IStudentRepository _studentRepository;
        /// <summary>
        /// 构造注入
        /// </summary>
        /// <param name="studentRepository"></param>
        public StudentService(IStudentRepository studentRepository)
        {
            _studentRepository = studentRepository;
        }
    }

在构造函数的参数中直接写入服务类型,AutoFac解析该类时,就会去容器内部已存在的组件中查找,然后将匹配的对象注入到构造函数中去。

  • 属性注入
    属性注入与构造注入不同,是将容器内对应的组件直接注入到类内的属性中去,形式如下:
Copy
    /// <summary>
    /// 教师逻辑处理
    /// </summary>
    public class TeacherService : ITeacherService
    {
        /// <summary>
        /// 用于属性注入
        /// </summary>
        public ITeacherRepository TeacherRepository { get; set; }

        public string GetTeacherName(long id)
        {
            return TeacherRepository?.Get(111).Name;
        }
    }

要使用这种属性注入,在注册该属性所属类的时候,需要使用PropertiesAutowired()方法额外标注,如下:

Copy
builder.RegisterType<TeacherService>().PropertiesAutowired();

这样,容器在解析并实例化TeacherService类时,便会将容器内的组件与类内的属性做映射,如果相同则自动将组件注入到类内属性种。

  • 注意

属性注入争议性很大,很多人称这是一种_反模式_,事实也确实如此。
使用属性注入会让代码可读性变得极其复杂(而复杂难懂的代码一定不是好的代码,不管用的技术有多高大上)。
但是属性注入也不是一无是处,因为属性注入有一个特性:
在构造注入的时候,如果构造函数的参数中有一个对象在容器不存在,那么解析就会报错。
但是属性注入就不一样了,当容器内没有与该属性类型对应的组件时,这时解析不会报异常,只会让这个属性保持为空类型(null)。
利用这个特性,可以实现一些特殊的操作。

暴露服务#

即上面提到的As<xxx>()函数,AutoFac提供了以下三种标注暴露服务类型的方法:

以其自身类型暴露服务#

使用AsSelf()方法标识,表示以其自身类型暴露,也是当没有标注暴露服务的时候的默认选项。
如下四种写法是等效的:

Copy
builder.RegisterType<StudentService>();//不标注,默认以自身类型暴露服务
builder.RegisterType<StudentService>().AsSelf();
builder.RegisterType<StudentService>().As<StudentService>();
builder.RegisterType<StudentService>().As(typeof(StudentService));

以其实现的接口(interface)暴露服务#

使用As()方法标识,暴露的类型可以是多个,比如CallLogger类实现了ILogger接口和ICallInterceptor接口,那么可以这么写:

Copy
builder.RegisterType<CallLogger>()
       .As<ILogger>()
       .As<ICallInterceptor>()
       .AsSelf();

程序集批量注册时指定暴露类型#

  • 方法1:自己指定
Copy
        public static void BuildContainerFunc8(ContainerBuilder builder)
        {
            Assembly[] assemblies = Helpers.ReflectionHelper.GetAllAssemblies();

            builder.RegisterAssemblyTypes(assemblies)//程序集内所有具象类(concrete classes)
                .Where(cc =>cc.Name.EndsWith("Repository")|//筛选
                            cc.Name.EndsWith("Service"))
                .As(x=>x.GetInterfaces()[0])//反射出其实现的接口,并指定以其实现的第一个接口类型暴露
        }
  • 方法2:以其实现的所有接口类型暴露

使用AsImplementedInterfaces()函数实现,相当于一个类实现了几个接口(interface)就会暴露出几个服务,等价于上面连写多个As()的作用。

Copy
        public static void BuildContainerFunc8(ContainerBuilder builder)
        {
            Assembly[] assemblies = Helpers.ReflectionHelper.GetAllAssemblies();

            builder.RegisterAssemblyTypes(assemblies)//程序集内所有具象类(concrete classes)
                .Where(cc =>cc.Name.EndsWith("Repository")|//筛选
                            cc.Name.EndsWith("Service"))
                .AsImplementedInterfaces();//自动以其实现的所有接口类型暴露(包括IDisposable接口)
        }

生命周期作用域#

相当于UnitWork(工作单元)的概念。下面罗列出了AutoFac与.NET Core的生命周期作用域,并作了简要的对比。

AutoFac的生命周期作用域#

下面讲下AutoFac定义的几种生命周期作用域,上一篇评论里也有人提了,关于生命周期作用域这块确实不是很好理解,所以下面每中类型我都写了一个例子程序,这些例子程序对理解很有帮助,只要能读懂这些例子程序,就一定能弄懂这些生命周期作用域。(例子项目源码里都有,可以去试着实际运行下,更易理解)

瞬时实例(Instance Per Dependency)

也叫每个依赖一个实例。
即每次从容器里拿出来的都是全新对象,相当于每次都new出一个。
在其他容器中也被标识为 ‘Transient'(瞬时) 或 ‘Factory’(工厂)。

  • 注册

使用InstancePerDependency()方法标注,如果不标注,这也是默认的选项。以下两种注册方法是等效的:

Copy
//不指定,默认就是瞬时的
builder.RegisterType<Model.StudentEntity>();

//指定其生命周期域为瞬时
builder.RegisterType<Model.StudentEntity>().InstancePerDependency();
  • 解析:
Copy
using (var scope = Container.Instance.BeginLifetimeScope())
{
    var stu1 = scope.Resolve<Model.StudentEntity>();
    Console.WriteLine($"第1次打印:{stu1.Name}");
    stu1.Name = "张三";
    Console.WriteLine($"第2次打印:{stu1.Name}");

    var stu2 = scope.Resolve<Model.StudentEntity>();
    Console.WriteLine($"第2次打印:{stu2.Name}");
}

上面解析了2次,有两个实例,stu1和stu2指向不同的两块内存,彼此之间没有关系。
打印结果:

单例(Single Instance)

即全局只有一个实例,在根容器和所有嵌套作用域内,每次解析返回的都是同一个实例。

  • 注册

使用SingleInstance()方法标识:

Copy
builder.RegisterType<Model.StudentEntity>().SingleInstance();
  • 解析:
Copy
//直接从根域内解析(单例下可以使用,其他不建议这样直接从根域内解析)
var stu1 = Container.Instance.Resolve<Model.StudentEntity>();
stu1.Name = "张三";
Console.WriteLine($"第1次打印:{stu1.Name}");

using (var scope1 = Container.Instance.BeginLifetimeScope())
{
    var stu2 = scope1.Resolve<Model.StudentEntity>();
    Console.WriteLine($"第2次打印:{stu2.Name}");

    stu1.Name = "李四";
}
using (var scope2 = Container.Instance.BeginLifetimeScope())
{
    var stu3 = scope2.Resolve<Model.StudentEntity>();
    Console.WriteLine($"第3次打印:{stu3.Name}");
}

上面的stu1、stu2、stu3都是同一个实例,在内存上它们指向同一个内存块。
打印结果:

域内单例(Instance Per Lifetime Scope)

即在每个生命周期域内是单例的。

  • 注册
    使用InstancePerLifetimeScope()方法标识:
Copy
x.RegisterType<Model.StudentEntity>().InstancePerLifetimeScope();
  • 解析
Copy
//子域一
using (var scope1 = Container.Instance.BeginLifetimeScope())
{
    var stu1 = scope1.Resolve<Model.StudentEntity>();
    Console.WriteLine($"第1次打印:{stu1.Name}");
    
    stu1.Name = "张三";

    var stu2 = scope1.Resolve<Model.StudentEntity>();
    Console.WriteLine($"第2次打印:{stu2.Name}");
}
//子域二
using (var scope2 = Container.Instance.BeginLifetimeScope())
{
    var stuA = scope2.Resolve<Model.StudentEntity>();
    Console.WriteLine($"第3次打印:{stuA.Name}");
    
    stuA.Name = "李四";

    var stuB = scope2.Resolve<Model.StudentEntity>();
    Console.WriteLine($"第4次打印:{stuB.Name}");
}

如上,在子域一中,虽然解析了2次,但是2次解析出的都是同一个实例,即stu1和stu2指向同一个内存块Ⅰ。
子域二也一样,stuA和stuB指向同一个内存块Ⅱ,但是内存块Ⅰ和内存块Ⅱ却不是同一块。
打印结果如下,第1次和第3次为null:

指定域内单例(Instance Per Matching Lifetime Scope)

即每个匹配的生命周期作用域一个实例。
该域类型其实是上面的“域内单例”的其中一种,不一样的是它允许我们给域“打标签”,只要在这个特定的标签域内就是单例的。

  • 注册
    使用InstancePerMatchingLifetimeScope(string tagName)方法注册:
Copy
builder.RegisterType<Worker>().InstancePerMatchingLifetimeScope("myTag");
  • 解析
Copy
//myScope标签子域一
using (var myScope1 = Container.Instance.BeginLifetimeScope("myTag"))
{
    var stu1 = myScope1.Resolve<Model.StudentEntity>();
    stu1.Name = "张三";
    Console.WriteLine($"第1次打印:{stu1.Name}");

    var stu2 = myScope1.Resolve<Model.StudentEntity>();
    Console.WriteLine($"第2次打印:{stu2.Name}");
    //解析了2次,但2次都是同一个实例(stu1和stu2指向同一个内存块Ⅰ)
}
//myScope标签子域二
using (var myScope2 = Container.Instance.BeginLifetimeScope("myTag"))
{
    var stuA = myScope2.Resolve<Model.StudentEntity>();
    Console.WriteLine($"第3次打印:{stuA.Name}");
    //因为标签域内已注册过,所以可以解析成功
    //但是因为和上面不是同一个子域,所以解析出的实例stuA与之前的并不是同一个实例,指向另一个内存块Ⅱ
}
//无标签子域三
using (var noTagScope = Container.Instance.BeginLifetimeScope())
{
    try
    {
        var stuOne = noTagScope.Resolve<Model.StudentEntity>();//会报异常
        Console.WriteLine($"第4次正常打印:{stuOne.Name}");
    }
    catch (Exception e)
    {
        Console.WriteLine($"第4次异常打印:{e.Message}");
    }
    //因为StudentEntity只被注册到带有myScope标签域内,所以这里解析不到,报异常
}

打印结果:

需要注意:

  • 第3次打印为null,不同子域即使标签相同,但也是不同子域,所以域之间不是同一个实例
  • 在其他标签的域内(包括无标签域)解析,会报异常

每次请求内单例(Instance Per Request)

该种类型适用于“request”类型的应用,比如MVC和WebApi。
其实质其实又是上一种的“指定域内单例”的一种特殊情况:AutoFac内有一个静态字符串叫Autofac.Core.Lifetime.MatchingScopeLifetimeTags.RequestLifetimeScopeTag,其值为"AutofacWebRequest",当“指定域内单例”打的标签是这个字符串时,那它就是“每次请求内单例”了。

  • 注册
    使用InstancePerRequest()方法标注:
Copy
builder.RegisterType<Model.StudentEntity>().InstancePerRequest();

也可以使用上面的域内单例的注册法(但是不建议):

Copy
//使用静态字符串标记
builder.RegisterType<Model.StudentEntity>().InstancePerMatchingLifetimeScope(Autofac.Core.Lifetime.MatchingScopeLifetimeTags.RequestLifetimeScopeTag);
//或者直接写明字符串
builder.RegisterType<Model.StudentEntity>().InstancePerMatchingLifetimeScope("AutofacWebRequest");

这里用控制台程序不好举例子就不写解析代码了,要理解“每次请求内单例”的作用,最好的例子就是EF中的DBContext,我们在一次request请求内,即使是用到了多个Service和多个Repository,也只需要一个数据库实例,这样即能减少数据库实例初始化的消耗,还能实现事务的功能。

.NET Core的生命周期作用域(Service lifetimes)#

相比于AutoFac的丰富复杂,.NET Core就比较简单粗暴了,只要3种类型:

瞬时实例(Transient)

与AutoFac的瞬时实例(Instance Per Dependency)相同,每次都是全新的实例。
使用AddTransient()注册:

Copy
services.AddTransient<IStudentService, StudentService>();

请求内单例(Scoped)

其意义与AutoFac的请求内单例(Instance Per Request)相同,但实际如果真正在.NET Core中使用使用AutoFac的话,应该使用AutoFac的域内单例(Instance Per LifetimeScope)来代替。
原因是.NET Core框架自带的DI(Microsoft.Extensions.DependencyInjection)全权接管了请求和生命周期作用域的创建,所以AutoFac无法控制,但是使用域内单例(Instance Per LifetimeScope)可以实现相同的效果。
使用AddScoped()注册:

Copy
services.AddScoped<IStudentService, StudentService>();

单例(Singleton)

与AutoFac的单例(Single Instance)相同。
使用AddSingleton();注册:

Copy
services.AddSingleton<StudentEntity>();

第二部分:.NET Framework Web程序AutoFac注入#

MVC项目#

思路很简单,三步走:

  1. 新建AutoFac容器
  2. 初始化容器,向容器注册所有需要的依赖对象
  3. 将AutoFac解析器设置为系统的依赖解析器(Dependency Resolver)

MVC容器#

除了AutoFac主包之外,还需要Nuget导入AutoFac.Mvc5包:

容器代码:

Copy
using System;
using System.Linq;
using System.Reflection;
//
using Autofac;
using Autofac.Integration.Mvc;
//
using Ray.EssayNotes.AutoFac.Repository.IRepository;
using Ray.EssayNotes.AutoFac.Repository.Repository;


namespace Ray.EssayNotes.AutoFac.Infrastructure.Ioc
{
    /// <summary>
    /// .net framework MVC程序容器
    /// </summary>
    public static class MvcContainer
    {
        public static IContainer Instance;

        /// <summary>
        /// 初始化MVC容器
        /// </summary>
        /// <param name="func"></param>
        /// <returns></returns>
        public static System.Web.Mvc.IDependencyResolver Init(Func<ContainerBuilder, ContainerBuilder> func = null)
        {
            //新建容器构建器,用于注册组件和服务
            var builder = new ContainerBuilder();
            //注册组件
            MyBuild(builder); 
            func?.Invoke(builder);
            //利用构建器创建容器
            Instance = builder.Build();

            //返回针对MVC的AutoFac解析器
            return new AutofacDependencyResolver(Instance);
        }

        public static void MyBuild(ContainerBuilder builder)
        {
            Assembly[] assemblies = Helpers.ReflectionHelper.GetAllAssembliesWeb();

            //注册仓储 && Service
            builder.RegisterAssemblyTypes(assemblies)//程序集内所有具象类(concrete classes)
                .Where(cc => cc.Name.EndsWith("Repository") |//筛选
                             cc.Name.EndsWith("Service"))
                .PublicOnly()//只要public访问权限的
                .Where(cc => cc.IsClass)//只要class型(主要为了排除值和interface类型)
                .AsImplementedInterfaces();//自动以其实现的所有接口类型暴露(包括IDisposable接口)

            //注册泛型仓储
            builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>));

            //注册Controller
            //方法1:自己根据反射注册
            //builder.RegisterAssemblyTypes(assemblies)
            //    .Where(cc => cc.Name.EndsWith("Controller"))
            //    .AsSelf();
            //方法2:用AutoFac提供的专门用于注册MvcController的扩展方法
            Assembly mvcAssembly = assemblies.FirstOrDefault(x => x.FullName.Contains(".NetFrameworkMvc"));
            builder.RegisterControllers(mvcAssembly);
        }
    }
}

这里Init()初始化函数返回类型变成了System.Web.Mvc.IDependencyResolver接口,即MVC的系统依赖解析器。
AutoFac自己封装了一个AutofacDependencyResolver类(AutoFac依赖解析器类)实现了这个接口,所以直接new一个AutofacDependencyResolver类返回,等下把这个AutoFac依赖解析器类设置为MVC的系统依赖解析器就可以了。

Copy
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Web.Mvc;

namespace Autofac.Integration.Mvc
{
  /// <summary>
  /// Autofac implementation of the <see cref="T:System.Web.Mvc.IDependencyResolver" /> interface.
  /// </summary>
  public class AutofacDependencyResolver : IDependencyResolver
  {
        //内部实现
        //......
  }

项目主程序:#

  • Global.asax启动项

启动时初始化容器,并把AutoFac生成的解析器设置为系统依赖解析器:

Copy
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
//
using Ray.EssayNotes.AutoFac.Infrastructure.Ioc;


namespace Ray.EssayNotes.AutoFac.NetFrameworkMvc
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            //初始化容器,并返回适用于MVC的AutoFac解析器
            System.Web.Mvc.IDependencyResolver autoFacResolver = MvcContainer.Init();
            //将AutoFac解析器设置为系统DI解析器
            DependencyResolver.SetResolver(autoFacResolver);
        }
    }
}

其中DependencyResolver.SetResolver()为MVC封装的一个静态方法,用于设置MVC的依赖解析器。
其参数只要是实现了System.Web.Mvc.IDependencyResolver接口的对象都可以,AutoFac自己封装的解析器AutofacDependencyResolver类实现了这个接口,所以可以传进来,从而实现了让AutoFac接管MVC的依赖注入。

  • 学生控制器:

直接利用构造注入就可以了:

Copy
using System.Web.Mvc;
//
using Ray.EssayNotes.AutoFac.Service.IService;


namespace Ray.EssayNotes.AutoFac.NetFrameworkMvc.Controllers
{
    /// <summary>
    /// 学生Api
    /// </summary>
    public class StudentController : Controller
    {
        private readonly IStudentService _studentService;

        /// <summary>
        /// 构造注入
        /// </summary>
        /// <param name="studentService"></param>
        public StudentController(IStudentService studentService)
        {
            _studentService = studentService;
        }

        /// <summary>
        /// 获取学生姓名
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public string GetStuNameById(long id)
        {
            return _studentService.GetStuName(id);
        }
    }
}

运行调用Api#

WebApi项目#

和MVC一样,思路很简单,三步走:

  1. 新建AutoFac容器
  2. 初始化容器,向容器注册所有需要的依赖对象
  3. 将AutoFac解析器设置为系统的依赖解析器(Dependency Resolver)

Api容器#

除了AutoFac主包之外,还需要Nuget导入AutoFac.WebApi2包:

容器代码:

Copy
using System;
using System.Linq;
using System.Reflection;
//
using Autofac;
using Autofac.Integration.WebApi;
//
using Ray.EssayNotes.AutoFac.Repository.Repository;
using Ray.EssayNotes.AutoFac.Repository.IRepository;


namespace Ray.EssayNotes.AutoFac.Infrastructure.Ioc
{
    /// <summary>
    /// .NET Framework WebApi容器
    /// </summary>
    public static class ApiContainer
    {
        public static IContainer Instance;

        /// <summary>
        /// 初始化Api容器
        /// </summary>
        /// <param name="func"></param>
        public static System.Web.Http.Dependencies.IDependencyResolver Init(Func<ContainerBuilder, ContainerBuilder> func = null)
        {
            //新建容器构建器,用于注册组件和服务
            var builder = new ContainerBuilder();
            //注册组件
            MyBuild(builder);
            func?.Invoke(builder);
            //利用构建器创建容器
            Instance = builder.Build();

            //返回针对WebApi的AutoFac解析器
            return new AutofacWebApiDependencyResolver(Instance);
        }

        public static void MyBuild(ContainerBuilder builder)
        {
            var assemblies = Helpers.ReflectionHelper.GetAllAssembliesWeb();

            //注册仓储 && Service
            builder.RegisterAssemblyTypes(assemblies)//程序集内所有具象类(concrete classes)
                .Where(cc => cc.Name.EndsWith("Repository") |//筛选
                             cc.Name.EndsWith("Service"))
                .PublicOnly()//只要public访问权限的
                .Where(cc => cc.IsClass)//只要class型(主要为了排除值和interface类型)
                .AsImplementedInterfaces();//自动以其实现的所有接口类型暴露(包括IDisposable接口)

            //注册泛型仓储
            builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>));

            //注册ApiController
            //方法1:自己根据反射注册
            //Assembly[] controllerAssemblies = assemblies.Where(x => x.FullName.Contains(".NetFrameworkApi")).ToArray();
            //builder.RegisterAssemblyTypes(controllerAssemblies)
            //    .Where(cc => cc.Name.EndsWith("Controller"))
            //    .AsSelf();
            //方法2:用AutoFac提供的专门用于注册ApiController的扩展方法
            Assembly mvcAssembly = assemblies.FirstOrDefault(x => x.FullName.Contains(".NetFrameworkApi"));
            builder.RegisterApiControllers(mvcAssembly);
        }
    }
}

这里Init()初始化函数返回类型变成了System.Web.Http.Dependencies.IDependencyResolver接口,即WebApi的系统依赖解析器。
AutoFac自己封装了一个AutofacWebApiDependencyResolver类(AutoFac针对WebApi的依赖解析器类)实现了这个接口,所以直接new一个AutofacWebApiDependencyResolver类返回,等下把这个AutoFac依赖解析器类设置为WebApi的系统依赖解析器就可以了。

WebApi主程序#

  • Global.asax启动项

在项目启动时初始化容器:

Copy
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
//
using Ray.EssayNotes.AutoFac.Infrastructure.Ioc;


namespace Ray.EssayNotes.AutoFac.NetFrameworkApi
{
    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            //初始化容器,并返回适用于WebApi的AutoFac解析器
            System.Web.Http.Dependencies.IDependencyResolver autoFacResolver = ApiContainer.Init();
            //获取HttpConfiguration
            HttpConfiguration config = GlobalConfiguration.Configuration;
            //将AutoFac解析器设置为系统DI解析器
            config.DependencyResolver = autoFacResolver;
        }
    }
}

这里跟上面的MVC项目不太一样,是通过HttpConfiguration对象来设置依赖解析器的,但是原理相同,不赘述了。

  • 学生控制器:

直接利用构造函数注入即可:

Copy
using System.Web.Http;
//
using Ray.EssayNotes.AutoFac.Service.IService;


namespace Ray.EssayNotes.AutoFac.NetFrameworkApi.Controllers
{
    /// <summary>
    /// 学生Api
    /// </summary>
    public class StudentController : ApiController
    {
        private readonly IStudentService _studentService;

        /// <summary>
        /// 构造注入
        /// </summary>
        /// <param name="studentService"></param>
        public StudentController(IStudentService studentService)
        {
            _studentService = studentService;
        }

        /// <summary>
        /// 获取学生姓名
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpGet]
        [Route("Student/GetStuNameById")]
        public string GetStuNameById(long id)
        {
            return _studentService.GetStuName(123);
        }
    }
}

运行调用接口#

第三部分:.NET Core的DI#

自带的DI#

与.NET Framework不同,.NET Core把DI提到了非常重要的位置,其框架本身就集成了一套DI容器。
针对其自带DI,主要理解两个对象,IServiceCollection和 IServiceProvider。

  • IServiceCollection

用于向容器注册服务,可以和AutoFac的ContainerBuilder(容器构建器)类比。

  • IServiceProvider

负责从容器中向外部提供实例,可以和AutoFac的解析的概念类比。

注册的地方就在主程序下的startup类中。

但是其本身的注册语法并没有AutoFac那么丰富,泛型注册、批量注册这些全都没有,只有下面这种最基础的一个一个注册的形式:

Copy
using System.Linq;
using System.Reflection;
//
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
//
using Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc.Extensions;
using Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc.Helpers;


namespace Ray.EssayNotes.AutoFac.CoreApi
{
    public class Startup
    {
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
            //注册
            //自定义注册
            //注册仓储
            services.AddScoped<ITeacherRepository, TeacherRepository>();
            services.AddScoped<IStudentRepository, StudentRepository>();
            services.AddScoped<IBaseRepository<StudentEntity>, BaseRepository<StudentEntity>>();
            services.AddScoped<IBaseRepository<TeacherEntity>, BaseRepository<TeacherEntity>>();
            services.AddScoped<IBaseRepository<BookEntity>, BaseRepository<BookEntity>>();
            //注册Service
            services.AddScoped<IStudentService, StudentService>();
            services.AddScoped<ITeacherService, TeacherService>();
            services.AddScoped<IBookService, BookService>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}

所以,大家通常都会自己去扩展这些注册方法,以实现一些和AutoFac一样的便捷的注册操作,下面我根据反射写了一个小扩展,写的比较简单潦草,可以参考下:

扩展#

扩展代码:

Copy
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
//
using Microsoft.Extensions.DependencyInjection;
//
using Ray.EssayNotes.AutoFac.Model;
using Ray.EssayNotes.AutoFac.Repository.IRepository;
using Ray.EssayNotes.AutoFac.Repository.Repository;
using Ray.EssayNotes.AutoFac.Service.IService;
using Ray.EssayNotes.AutoFac.Service.Service;


namespace Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc.Extensions
{
    /// <summary>
    /// asp.net core注册扩展
    /// </summary>
    public static class RegisterExtension
    {
        /// <summary>
        /// 反射批量注册
        /// </summary>
        /// <param name="services"></param>
        /// <param name="assembly"></param>
        /// <param name="serviceLifetime"></param>
        /// <returns></returns>
        public static IServiceCollection AddAssemblyServices(this IServiceCollection services, Assembly assembly, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)
        {
            var typeList = new List<Type>();//所有符合注册条件的类集合

            //筛选当前程序集下符合条件的类
            List<Type> types = assembly.GetTypes().
                Where(t => t.IsClass && !t.IsGenericType)//排除了泛型类
                .ToList();

            typeList.AddRange(types);
            if (!typeList.Any()) return services;

            var typeDic = new Dictionary<Type, Type[]>(); //待注册集合<class,interface>
            foreach (var type in typeList)
            {
                var interfaces = type.GetInterfaces();   //获取接口
                typeDic.Add(type, interfaces);
            }

            //循环实现类
            foreach (var instanceType in typeDic.Keys)
            {
                Type[] interfaceTypeList = typeDic[instanceType];
                if (interfaceTypeList == null)//如果该类没有实现接口,则以其本身类型注册
                {
                    services.AddServiceWithLifeScoped(null, instanceType, serviceLifetime);
                }
                else//如果该类有实现接口,则循环其实现的接口,一一配对注册
                {
                    foreach (var interfaceType in interfaceTypeList)
                    {
                        services.AddServiceWithLifeScoped(interfaceType, instanceType, serviceLifetime);
                    }
                }
            }
            return services;
        }

        /// <summary>
        /// 暴露类型可空注册
        /// (如果暴露类型为null,则自动以其本身类型注册)
        /// </summary>
        /// <param name="services"></param>
        /// <param name="interfaceType"></param>
        /// <param name="instanceType"></param>
        /// <param name="serviceLifetime"></param>
        private static void AddServiceWithLifeScoped(this IServiceCollection services, Type interfaceType, Type instanceType, ServiceLifetime serviceLifetime)
        {
            switch (serviceLifetime)
            {
                case ServiceLifetime.Scoped:
                    if (interfaceType == null) services.AddScoped(instanceType);
                    else services.AddScoped(interfaceType, instanceType);
                    break;
                case ServiceLifetime.Singleton:
                    if (interfaceType == null) services.AddSingleton(instanceType);
                    else services.AddSingleton(interfaceType, instanceType);
                    break;
                case ServiceLifetime.Transient:
                    if (interfaceType == null) services.AddTransient(instanceType);
                    else services.AddTransient(interfaceType, instanceType);
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(serviceLifetime), serviceLifetime, null);
            }
        }
    }
}

利用这个扩展,我们在startup里就可以用类似AutoFac的语法来注册了。

主程序#

注册代码:

Copy
using System.Linq;
using System.Reflection;
//
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
//
using Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc.Extensions;
using Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc.Helpers;


namespace Ray.EssayNotes.AutoFac.CoreApi
{
    public class Startup
    {
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
            //注册
            
            //自定义批量注册
            Assembly[] assemblies = ReflectionHelper.GetAllAssembliesCoreWeb();
            //注册repository
            Assembly repositoryAssemblies = assemblies.FirstOrDefault(x => x.FullName.Contains(".Repository"));
            services.AddAssemblyServices(repositoryAssemblies);
            //注册service  
            Assembly serviceAssemblies = assemblies.FirstOrDefault(x => x.FullName.Contains(".Service"));
            services.AddAssemblyServices(serviceAssemblies);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}

其实AutoFac针对.NET Core已经帮我们集成了一套注册的扩展,我们可以通过两种方式把AutoFac引入.NET Core:一种是将AutoFac容器作为辅助容器,与.NET Core的DI共存,我们可以同时向两个容器里注册组件;一种是让AutoFac容器接管.NET Core的DI,注册时只选择往Autofac容器中注册。
下面就分别实现下这两种引入AutoFac的方式。

AutoFac作为辅助注册#

Core容器#

先按照之前写AutoFac容器的方法,新建一个针对Core的AutoFac容器:

Copy
using System;
//
using Microsoft.Extensions.DependencyInjection;
//
using Autofac;
using Autofac.Extensions.DependencyInjection;
//
using Ray.EssayNotes.AutoFac.Repository.IRepository;
using Ray.EssayNotes.AutoFac.Repository.Repository;


namespace Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc
{
    /// <summary>
    /// Core的AutoFac容器
    /// </summary>
    public static class CoreContainer
    {
        /// <summary>
        /// 容器实例
        /// </summary>
        public static IContainer Instance;

        /// <summary>
        /// 初始化容器
        /// </summary>
        /// <param name="services"></param>
        /// <param name="func"></param>
        /// <returns></returns>
        public static IServiceProvider Init(IServiceCollection services, Func<ContainerBuilder, ContainerBuilder> func = null)
        {
            //新建容器构建器,用于注册组件和服务
            var builder = new ContainerBuilder();
            //将Core自带DI容器内的服务迁移到AutoFac容器
            builder.Populate(services);
            //自定义注册组件
            MyBuild(builder);
            func?.Invoke(builder);
            //利用构建器创建容器
            Instance = builder.Build();

            return new AutofacServiceProvider(Instance);
        }

        /// <summary>
        /// 自定义注册
        /// </summary>
        /// <param name="builder"></param>
        public static void MyBuild(this ContainerBuilder builder)
        {
            var assemblies = Helpers.ReflectionHelper.GetAllAssembliesCoreWeb();

            //注册仓储 && Service
            builder.RegisterAssemblyTypes(assemblies)
                .Where(cc => cc.Name.EndsWith("Repository") |//筛选
                             cc.Name.EndsWith("Service"))
                .PublicOnly()//只要public访问权限的
                .Where(cc => cc.IsClass)//只要class型(主要为了排除值和interface类型)
                .AsImplementedInterfaces();//自动以其实现的所有接口类型暴露(包括IDisposable接口)

            //注册泛型仓储
            builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>));

            /*
            //注册Controller
            Assembly[] controllerAssemblies = assemblies.Where(x => x.FullName.Contains(".CoreApi")).ToArray();
            builder.RegisterAssemblyTypes(controllerAssemblies)
                .Where(cc => cc.Name.EndsWith("Controller"))
                .AsSelf();
                */
        }
    }
}

主程序#

在主程序中新建一个StartupWithAutoFac类,用于注册。
StartupWithAutoFac代码:

Copy
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
//
using Autofac;
//
using Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc;


namespace Ray.EssayNotes.AutoFac.CoreApi
{
    public class StartupWithAutoFac
    {
        public IConfiguration Configuration { get; }

        public StartupWithAutoFac(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        /// <summary>
        /// 利用该方法可以使用AutoFac辅助注册,该方法在ConfigureServices()之后执行,所以当发生覆盖注册时,以后者为准。
        /// 不要再利用构建器去创建AutoFac容器了,系统已经接管了。
        /// </summary>
        /// <param name="builder"></param>
        public void ConfigureContainer(ContainerBuilder builder)
        {
            builder.MyBuild();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}

这里其他地方与原startup都相同,只是多了一个ConfigureContainer()方法,在该方法内可以按照AutoFac的语法进行自由注册。

然后修改program类,将AutoFac hook进管道,并将StartupWithAutoFac类指定为注册入口:

Copy
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;


namespace Ray.EssayNotes.AutoFac.CoreApi
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                //第一种:使用自带DI
                //.UseStartup<Startup>();

                //第二种:添加AutoFac作为辅助容器
                .HookAutoFacIntoPipeline()
                .UseStartup<StartupWithAutoFac>();

                //第三种:添加AutoFac接管依赖注入
                //.UseStartup<StartupOnlyAutoFac>();
    }
}

AutoFac接管注册#

容器#

还是上面的CoreContainer容器。

主程序#

主程序新建一个StartupOnlyAutoFac类,
代码如下:

Copy
using System;
//
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
//
using Ray.EssayNotes.AutoFac.Infrastructure.CoreIoc;


namespace Ray.EssayNotes.AutoFac.CoreApi
{
    public class StartupOnlyAutoFac
    {
        public IConfiguration Configuration { get; }

        public StartupOnlyAutoFac(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            return CoreContainer.Init(services);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}

这里直接改了ConfigureServices()方法的返回类型,然后在该方法内直接利用AutoFac注册。

最后当然也要更改下program类,指定StartupOnlyAutoFac类为注册入口。
代码:

Copy
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;


namespace Ray.EssayNotes.AutoFac.CoreApi
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                //第一种:使用自带DI
                //.UseStartup<Startup>();

                //第二种:添加AutoFac作为辅助容器
                //.HookAutoFacIntoPipeline()
                //.UseStartup<StartupWithAutoFac>();

                //第三种:添加AutoFac接管依赖注入
                .UseStartup<StartupOnlyAutoFac>();
    }
}

运行调用#

  • StudentController
Copy
using Microsoft.AspNetCore.Mvc;
//
using Ray.EssayNotes.AutoFac.Service.IService;


namespace Ray.EssayNotes.AutoFac.CoreApi.Controllers
{
    [ApiController]
    public class StudentController : ControllerBase
    {
        private readonly IStudentService _studentService;
        public StudentController(IStudentService studentService)
        {
            _studentService = studentService;
        }

        [Route("Student/GetStuNameById")]
        public string GetStuNameById(long id)
        {
            return _studentService.GetStuName(id);
        }
    }
}
  • 调用

【半小时大话.net依赖注入】(下)详解AutoFac+实战Mvc、Api以及.NET Core的依赖注入 – 在7楼’s Blogs已关闭评论
2019年7月20日 By mikel 分类: C#, Debug, 项目管理

来源: Code Review最佳实践 – 宝玉 – 博客园

我一直认为Code Review(代码审查)是软件开发中的最佳实践之一,可以有效提高整体代码质量,及时发现代码中可能存在的问题。包括像Google、微软这些公司,Code Review都是基本要求,代码合并之前必须要有人审查通过才行。

然而对于我观察到的大部分软件开发团队来说,认真做Code Review的很少,有的流于形式,有的可能根本就没有Code Review的环节,代码质量只依赖于事后的测试。也有些团队想做好代码审查,但不知道怎么做比较好。

 网上关于如何做Code Review的文章已经有很多了,这里我结合自己的一些经验,也总结整理了一下Code Review的最佳实践,希望能对大家做好Code Review有所帮助。

Code Review有什么好处?

很多团队或个人不做Code Review,根源还是不觉得这是一件有意义的事情,不觉得有什么好处。这个问题要从几个角度来看。

  • 首先是团队知识共享的角度 

一个开发团队中,水平有高有低,每个人侧重的领域也有不同。怎么让高水平的帮助新人成长?怎么让大家都对自己侧重领域之外的知识保持了解?怎么能有人离职后其他人能快速接手?这些都是团队管理者关心的问题。

而代码审查,就是一个很好的知识共享的方式。通过代码审查,高手可以直接指出新手代码中的问题,新手可以马上从高手的反馈中学习到好的实践,得到更快的成长;通过代码审查,前端也可以去学习后端的代码,做功能模块A的可以去了解功能模块B的。

可能有些高手觉得给新手代码审查浪费时间,自己也没收获。其实不然,新人成长了,就可以更多的帮高手分担繁重的任务;代码审查中花时间,就少一些帮新人填坑擦屁股的时间;良好的沟通能力、发现问题的能力、帮助其他人成长,都是技术转管理或技术上更上一层楼必不可少的能力,而通过代码审查可以有效的去练习这些方面的能力。

  • 然后是代码质量的角度

现实中的项目总是人手缺进度紧,所以被压缩的往往就是自动化测试和代码审查,结果影响代码质量,欠下技术债务,最后还是要加倍偿还。

也有人寄希望于开发后的人工测试,然而对于代码质量来说,很多问题通过测试是测试不出来的,只能通过代码审查。比如说代码的可读性可维护性,比如代码的结构,比如一些特定条件才触发的死循环、逻辑算法错误,还有一些安全上的漏洞也更容易通过代码审查发现和预防。

也有人觉得自己水平高就不需要代码审查了。对于高手来说,让别人审查自己的代码,可以让其他人学习到好的实践;在让其他人审查的同时,在给别人说明自己代码的时候,也等于自己对自己的代码进行了一次审查。这其实就跟我们上学时做数学题一样,真正能拿高分的往往是那些做完后还会认真检查的。

  • 还有团队规范的角度

每个团队都有自己的代码规范,有自己的基于架构设计的开发规范,然而时间一长,就会发现代码中出现很多不遵守代码规范的情况,有很多绕过架构设计的代码。比如难以理解和不规范的命名,比如三层架构里面UI层绕过业务逻辑层直接调用数据访问层代码。

如果这些违反规范的代码被纠正的晚了,后面再要修改就成本很高了,而且团队的规范也会慢慢的形同虚设。

通过代码审查,就可以及时的去发现和纠正这些问题,保证团队规范的执行。

关于代码审查的好处,还有很多,也不一一列举。还是希望能认识到Code Review和写自动化测试一样,都是属于磨刀不误砍柴工的工作,在上面投入一点点时间,未来会收获代码质量,会节约整体的开发时间。

该怎么做?

现在很多人都已经有意识到Code Review的重要性了,只是苦于不知道如何去实践,不知道怎么样算是好的Code Review实践。

把Code Review作为开发流程的必选项而不是可选项

在很早以前,我就尝试过将代码审查作为代码流程的一部分,但只是一个可选项,没有Code Review也可以把代码合并到master。这样的结果就是想起来才会去做Code Review,去检查的时候已经有了太多的代码变更,审查起来非常困难,另外就算审查出问题,也很难得以修改。

我们现在对代码的审查则是作为开发流程的一个必选项,每次开发新功能或者修复Bug,开一个新的分支,分支要合并到master有两个必要条件:

  • 所有的自动化测试通过
  • 有至少一个人Code Review通过,如果是新手的PR,还必须有资深程序员Code Review通过

图片来源:How to Do Code Reviews Like a Human

这样把Code Review作为开发流程的一个必选项后,就很好的保证了代码在合并之前有过Code Review。而且这样合并前要求代码审查的流程,好处也很明显:

  • 由于每一次合并前都要做代码审查,这样一般一次审查的代码量也不会太大,对于审查者来说压力也不会太大
  • 如果在Code Review时发现问题,被审查者希望代码能尽快合并,也会积极的对审查出来的问题进行修改,不至于对审查结果太过抵触

如果你觉得Code Review难以推行,不妨先尝试着把Code Review变成你开发流程的一个必选项。

把Code Review变成一种开发文化而不仅仅是一种制度

把Code Review 作为开发流程的必选项后,不代表Code Review这件事就可以执行的很好,因为Code Review 的执行,很大部分程度上依赖于审查者的认真审查,以及被审查者的积极配合,两者缺一不可!

如果仅仅只是当作一个流程制度,那么就可能会流于形式。最终结果就是看起来有Code Review,但没有人认真审查,随便看下就通过了,或者发现问题也不愿意修改。

真要把Code Review这件事做好,必须让Code Review变成团队的一种文化,开发人员从心底接受这件事,并认真执行这件事。

要形成这样的文化,不那么容易,也没有想象的那么难,比如这些方面可以参考:

  • 首先,得让开发人员认识到Code Review这件事为自己、为团队带来的好处
  • 然后,得要有几个人做好表率作用,榜样的力量很重要
  • 还有,对于管理者来说,你激励什么,往往就会得到什么
  • 最后,像写自动化测试一样,把Code Review要作为开发任务的一部分,给审查者和被审查者都留出专门的时间去做这件事,不能光想着马儿跑得快又舍不得给马儿吃草

如何形成这样的文化,有心的话,还有很多方法可以尝试。只有真正让大家都认同和践行,才可能去做好Code Review这件事。

一些Code Review的经验技巧

在做好Code Review这件事上,还有一些经验技巧可以参考。

选什么工具辅助做CODE REVIEW?

现在很多源代码管理工具都自带Code Review工具,典型的像Github、Gitlab、微软的Azure DevOps,尤其是像Gitlab,还可以自己在本地搭建环境,根据自己的需要灵活配置。

配合什么样的开发流程比较好?

Github Flow这样基于分支开发的流程是特别适合搭配Code Review的。其实不管什么样的开发流程,关键点在于代码合并到master(主干)之前,要先做Code Review。

真遇到紧急情况,来不及代码审查怎么办?

虽然原则上,必须要Code Review才能合并,但有时候确实会存在一些紧急情况,比如说线上故障补丁,而又没有其他人在线,那么这种情况下,最好是在任务管理系统中,创建一个Ticket,用来后续跟踪,确保后续补上Code Review,并对Code Review结果有后续的代码更新。

先设计再编码

有些新人发现自己的代码提交PR(Pull Request)后,会收到一堆的Code Review意见,必须要做大量的改动。这多半是因为在开始做之前,没有做好设计,做出来后才发现问题很多。

建议在做一个新功能之前,写一个简单的设计文档,表达清楚自己的设计思路,找资深的先帮你做一下设计的审查,发现设计上的问题。设计上没问题了,再着手开发,那么到Review的时候,相对问题就会少很多。

代码在提交CODE REVIEW之前,作者要自己先REVIEW和测试一遍

我在做代码审查的时候,有时候会发现一些非常明显的问题,有些甚至自己都没有测试过,就等着别人Code Review和测试帮助发现问题。这种依赖心理无论是对自己还是对团队都是很不负责任的。

一个好的开发人员,代码在提交Code Review之前,肯定是要自己先Review一遍,把该写的自动化测试代码写上,自己把基本的测试用例跑一遍的。

我对于团队提交的PR,有个要求就是要在PR的描述中增加截图或者录屏,就是为了通过截图或者录屏,确保提交PR的人自己是先测试过的。这也是一个有效的辅助手段。

PR要小

在做Code Review的时候,如果有大量的文件修改,那么Review起来是很困难的,但如果PR比较小,相对就比较容易Review,也容易发现代码中可能存在的问题。

所以在提交PR时,PR要小,如果是比较大的改动,那么最好分批提交,以减轻审查者的压力。

对评论进行分级

在做Code Review时,需要针对审查出有问题的代码行添加评论,如果只是评论,有时候对于被审查者比较难甄别评论所代表的含义,是不是必须要修改。

建议可以对Review的评论进行分级,不同级别的结果可以打上不同的Tag,比如说:

  • [blocker]: 在评论前面加上一个[blocker]标记,表示这个代码行的问题必须要修改
  • [optional]:在评论前面加上一个[optional]标记,表示这个代码行的问题可改可不改
  • [question]:在评论前面加上一个[question]标记,表示对这个代码行不理解,有问题需要问,被审查者需要针对问题进行回复澄清

类似这样的分级可以帮助被审查者直观了解Review结果,提高Review效率。

评论要友好,避免负面词汇;有说不清楚的问题当面沟通

虽然评论是主要的Code Review沟通方式,但也不要过于依赖,有时候面对面的沟通效率更高,也容易消除误解。

另外文明用语,不要用一些负面的词汇。

总结

Code Review是一种非常好的开发实践,如果你还没开始,不妨逐步实践起来;如果已经做了效果不好,不妨对照一下,看有没有把Code Review作为开发流程的必选项而不是可选项?有没有把Code Review变成一种开发文化而不仅仅是一种制度?

Code Review最佳实践 – 宝玉 – 博客园已关闭评论
2019年7月16日 By mikel 分类: 数据库

来源: Introduction to Change Data Capture (CDC) in SQL Server 2008[转] – Artech – 博客园

Change Data Capture records INSERTs, UPDATEs, and DELETEs applied to SQL Server tables, and makes a record available of what changed, where, and when, in simple relational ‘change tables’ rather than in an esoteric chopped salad of XML. These change tables contain columns that reflect the column structure of the source table you have chosen to track, along with the metadata needed to understand the changes that have been made. Pinal Dave explains all, with plenty of examples in a simple introduction. [原文地址:http://www.simple-talk.com/sql/learn-sql-server/introduction-to-change-data-capture-(cdc)-in-sql-server-2008/;作者:Pinal Dave]

1. Introduction
2. Enabling Change Data Capture on a Database
3. Enabling Change Data Capture on one or more Database Tables
4. Example of Change Data Capture
5. Capture Selected Column
Summary

1. Introduction

Often, you’ll be told that the specification of an application requires that  the value of  data in the database of an application must be recorded before it is changed. In other words, we are required to save all the history of the changes to the data. This feature is usually implemented for data security purposes. To implement this, I have seen a variety of solutions from triggers, timestamps and complicated queries (stored procedures) to audit data.

SQL Server 2005 introduced the new features of ‘after update’, ‘after insert’ and ‘after delete’ triggers that  almost solved the problem of tracking changes in data.  A better solution was introduced in SQL Server 2008 and is called Change Data Capture (CDC). CDC has allowed SQL Server developers to deliver SQL Server data archiving and capturing without any additional programming.

CDC is one of the new data tracking and capturing features of SQL Server 2008. It only tracks changes in user-created tables. Because captured data is then stored in relational tables, it can be easily accessed and retrieved subsequently, using regular T-SQL.

When you apply Change Data Capture features on a database table, a mirror of the tracked table is created with the same column structure of the original table, but with additional columns that include the metadata used to summarize the nature of the change in the database table row.  The SQL Server DBA can then easily monitor the activity for the logged table using these new audit tables .

2. Enabling Change Data Capture on a Database

CDC first has to be enabled for the database. Because CDC is a table-level feature, it then has to be enabled for each table to be tracked. You can run following query and check whether it is enabled for any database.

   1: USE master
   2: GO
   3: SELECT [name], database_id, is_cdc_enabled
   4: FROM sys.databases
   5: GO

This query will return the entire database name along with a column that shows whether  CDC is enabled.

clip_image001

You can run this stored procedure in the context of each database to enable CDC at database level. (The following script will enable CDC in AdventureWorks database. )

   1: USE AdventureWorks
   2: GO
   3: EXEC sys.sp_cdc_enable_db
   4: GO

As soon as CDC is enabled, it will show this result in SSMS.

clip_image002

Additionally, in the database AdventureWorks, you will see that a schema with the name ‘cdc’ has now been  created.

clip_image003

Some System Tables will have been created within the  AdventureWorks database as part of the cdc schema.

clip_image004

The table which have been created are listed here.

  • cdc.captured_columns – This table returns result for list of captured column.
  • cdc.change_tables – This table returns list of all the tables which are enabled for capture.
  • cdc.ddl_history – This table contains history of all the DDL changes since capture data enabled.
  • cdc.index_columns – This table contains indexes associated with change table.
  • cdc.lsn_time_mapping – This table maps LSN number (for which we will learn later) and time.

3. Enabling Change Data Capture on one or more Database Tables

The CDC feature can be applied at the table-level  to any database for which CDC is enabled.  It has to be enabled for any table which needs to be tracked. First run following query to show which tables of database have already been enabled for CDC.

   1: USE AdventureWorks
   2: GO
   3: SELECT [name], is_tracked_by_cdc
   4: FROM sys.tables
   5: GO

The above query will return a result that includes a column with the  table name, along with a column which displays if CDC is enabled or not.

clip_image005

You can run the following stored procedure to enable each table. Before enabling CDC at the table level, make sure that you have  enabled SQL Server Agent. When CDC is enabled on a table, it creates two CDC-related jobs that are specific to the database,  and executed using SQL Server Agent. Without SQL Server Agent enabled, these jobs will not execute.

Additionally, it is very important to understand the role of the required parameter @role_name. If there is any restriction of how data should be extracted from database, this option is used to specify any role which is following restrictions and gating access to data to this option if there is one.  If you do not specify any role and, instead, pass a NULL value, data access to this changed table will not be tracked and will be available to access by everybody.

Following script will enable CDC on HumanResources.Shift table.

   1: USE AdventureWorks
   2: GO
   3: EXEC sys.sp_cdc_enable_table
   4: @source_schema = N'HumanResources',
   5: @source_name   = N'Shift',
   6: @role_name     = NULL
   7: GO

clip_image006

As we are using AdventureWorks database, it creates the jobs with following names.

1. cdc.AdventureWorks_capture – When this job is executed it runs the system stored procedure sys.sp_MScdc_capture_job.  The procedure sys.sp_cdc_scan  is called internally by sys.sp_MScdc_capture_job. This procedure cannot be executed explicitly when a change data capture log scan operation is already active or when the database is enabled for transactional replication. This system SP enables SQL Server Agent, which in facts enable Change Data Capture feature.

2. cdc.AdventureWorks_cleanup – When this job is executed it runs the system stored procedure sys.sp_MScdc_cleanup_job. This system SP cleans up database changes tables.

The Stored Procedure sys.sp_cdc_enable_table enables CDC. There are several options available with this SP but we will only mention the required options for this SP. CDC is very powerful and versatile tool. By understanding the Stored Procedure  sys.sp_cdc_enable_table you will gain the true potential of the CDC feature. One more thing to notice is that when these jobs are created they are automatically enabled as well.

clip_image007

By default, all the columns of the specified table  is taken into consideration of this operation. If you want to only few columns of this table to be tracked in that case you can specify the columns as one of the parameters of above mentioned SP.

When everything is successfully completed,  check  the system tables again and you will find a new table  called cdc.HumanResources_Shift_CT. This table will contain all the changes in the table HumanResources.Shift. If you expand this table, you will find five additional columns as well.

As you will see there are five additional columnsto the mirrored original table

  • __$start_lsn
  • __$end_lsn
  • __$seqval
  • __$operation
  • __$update_mask

There are two values which are very important to us is __$operation and __$update_mask.

Column _$operation contains value which corresponds to DML Operations. Following is quick list of value and its corresponding meaning.

  • Delete Statement = 1
  • Insert Statement = 2
  • Value before Update Statement = 3
  • Value after Update Statement = 4

The column _$update_mask shows, via a bitmap,   which columns were updated in the DML operation that was specified by _$operation.  If this was  a DELETE or INSERT operation,   all columns are updated and so the mask contains value which has all 1’s in it. This mask is contains value which is formed with Bit values.

4. Example of Change Data Capture

We will test this feature by doing  DML operations such as INSERT, UPDATE and DELETE on the table HumanResources.Shift which we have set up for CDC. We will observe the effects on the CDC table cdc.HumanResources_Shift_CT.

Before we start let’s first SELECT from both tables and see what is in them.

   1: USE AdventureWorks
   2: GO
   3: SELECT *
   4: FROM HumanResources.Shift
   5: GO
   6: USE AdventureWorks
   7: GO
   8: SELECT *
   9: FROM cdc.HumanResources_Shift_CT
  10: GO

The result  of the query is as displayed here.

clip_image008

The original table HumanResources.Shift has three rows in it, whereas the  table cdc.HumanResources_Shift_CT is totally empty. This table will have entries after an operation on the tracked table.

Insert Operation

   1: Lets run an INSERT operation on the table HumanResources.Shift.
   2:
   3: USE AdventureWorks
   4: GO
   5: INSERT INTO [HumanResources].[Shift]
   6: ([Name],[StartTime],[EndTime],[ModifiedDate])
   7: VALUES ('Tracked Shift',GETDATE(), GETDATE(), GETDATE())
   8: GO
   9:

Once the script is run, we will check the content of two of our tables HumanResources.Shift and cdc.HumanResources_Shift_CT.

clip_image009

Because of the INSERT operation, we have a newly inserted fourth row in the tracked table HumanResources.Shift . The tracking table also has the same row visible. The value of _operation is 2 which means that this is an INSERT operation.

Update Operation

To illustrate the effects of an UPDATE we will update a newly inserted row.

   1: USE AdventureWorks
   2:
   3: GO
   4:
   5: UPDATE [HumanResources].[Shift]
   6:
   7: SET Name = 'New Name',
   8:
   9:       ModifiedDate = GETDATE()
  10:
  11: WHERE ShiftID = 4
  12:
  13: GO
  14:

Once more, we check our tables HumanResources.Shift and cdc.HumanResources_Shift_CT.

clip_image010

UPDATE operations always result in two different entries in the tracking table. One entry contains the previous values before the UPDATE is executed.  The second entry is for new data after the UPDATE is executed. In our case we have only changed two columns of the table but we are tracking the complete table so all the entries in the tableare logged before, and after, the update happens. The Change Data Capture mechanism always captures all the columns of the table unless, when CDC is set up on a table, it is restricted to track only a few columns. We will see how this can be done later on this article.

Delete Operation

To verify this option we will be running a DELETE operation on a newly inserted row.

   1: USE AdventureWorks
   2: GO
   3: DELETE
   4: FROM [HumanResources].[Shift]
   5: WHERE ShiftID = 4
   6: GO

Once this script is run, we can see the contents of  our tables HumanResources.Shift and cdc.HumanResources_Shift_CT.

clip_image011

Due to the DELETE operation, we now have only three rows in the tracked table HumanResources.Shift We can see the deleted row visible in the tracking table as new entry. The value of _operation is 4 , meaning that this is a delete operation.

Change Data Capture and Operations

We have now verified that, by using CDC, we are able to capture all the data  affected by DML operation. In the tracked table we have four values of the operation. We can see this operation’s value in the following image.

clip_image012

Understanding Update mask

It is important to understand the Update mask column in the tracking table. It is named as _$update_mask. The value displayed in the field is hexadecimal but is stored as binary.

In our example we have three different operations. INSERT and DELETE operations are done on the complete row and not on individual columns. These operations are listed marked masked with 0x1F is translated in binary as 0b11111, which means all the five columns of the table.

In our example, we had an UPDATE on only two columns – the second and fifth column. This is represented with 0x12 in hexadecimal value ( 0b10010 in binary).  Here, this value stands for second and fifth value if you look at it from the right, as a bitmap. This is a useful way of finding out which columns are being updated or changed.

The tracking table shows  two columns which contains the suffix lsn in them i.e. _$start_lsn and _$end_lsn. These two values correspond to the  Log Sequential Number. This number is associated with committed transaction of the DML operation on the tracked table.

Disabling Change Data Capture on a table

Disabling this feature is very simple. As we have seen earlier, if we have to enable CDC we have to do this in two steps – at table level and at database level,: In the same way, when we have to disable this feature, we can do this at same two levels. Let us see both of them one after one.

For dropping any tracking of any table we need three values the Source Schema, the Source Table name, and the Capture Instance. It is very easy to get schema and table name. In our case, the schema is HumanResource and table name is Shift, however we do not know the name of the Capture Instance. We can retrieve it very easily by running following T-SQL Query.

   1: USE AdventureWorks;
   2: GO
   3: EXEC sys.sp_cdc_help_change_data_capture
   4: GO

this will return a result which contains all the three required information for disabling CDC ona table.

clip_image013

The Stored Procedure  sys.sp_cdc_help_change_data_capture provides lots of other useful information as well. Once we have name of the capture instance, we can disable tracking of the table by running this T-SQL query.

   1: USE AdventureWorks;
   2:
   3: GO
   4:
   5: EXECUTE sys.sp_cdc_disable_table
   6:
   7: @source_schema = N'HumanResources',
   8:
   9:     @source_name = N'Shift',
  10:
  11:     @capture_instance = N'HumanResources_Shift';
  12:
  13: GO
  14:

Once Change Data Capture is disabled on any table, it drops the change data capture table as well all the functions which were associated with them. It also deletes all the rows and data associated with this feature from all the system tables and changes relevant data in catalog views.

clip_image014

In our example, we can clearly see that capture table cdc.HumanResources_Shift_CT is dropped.

Disable Change Data Capture Feature on Database

This is the easiest task out of all process. Running following T-SQL query will disable CDC on whole database.

   1: USE AdventureWorks
   2: GO
   3: EXEC sys.sp_cdc_disable_db
   4: GO

This Stored Procedure will delete all the data, functions, tables related to CDC. If this data is needed for any reason, you must take a  backup  before dropping CDC from any database

5. Capture Selected Column

When CDC is enabled on any table, it usually captures the data of all the columns. During INSERT or DELETE operations, it is necessary to capture all the data but in UPDATE operations  only the data of the updated columns are required. CDC is not yet advanced enough to provide this kind of dynamic column selection but CDC can let you select the columns from which changes to data should be captured from the beginning.

This stored procedure should be run in the context of each database to enable it at database level. Following script will enable CDC in AdventureWorks database.

   1: USE AdventureWorks
   2: GO
   3: EXEC sys.sp_cdc_enable_db
   4: GO

Now we will enable this feature at table level but for selected columns of ShiftID and Name only. This script will enable table-level change data capture for only two columns.

   1: USE AdventureWorks
   2: GO
   3: EXEC sys.sp_cdc_enable_table
   4: @source_schema = N'HumanResources',
   5: @source_name   = N'Shift',
   6: @role_name     = NULL,
   7: @captured_column_list = '[ShiftID],[Name]'
   8: GO

So what’s in the system table which will be created by data capturing purpose in AdventureWorks Database?

clip_image015

So you can see that there are now only two rows which are tracked.

We will change the data of one of the columns that weren’t specified so as to see  the value in cdc.HumanResources_Shift_CT table.

Before we start let us first select from both of the table and observe its content.

   1: USE AdventureWorks
   2: GO
   3: SELECT *
   4: FROM HumanResources.Shift
   5: GO
   6: USE AdventureWorks
   7: GO
   8: SELECT *
   9: FROM cdc.HumanResources_Shift_CT
  10: GO

Here is the result.

clip_image008[1]

The original table HumanResources.Shift now has three rows in it; whereas  table cdc.HumanResources_Shift_CT is totally empty. Lets update ModifiedDate for ShiftID =1 and see if that record creates an entry in the tracking table.

   1: USE AdventureWorks
   2: GO
   3: UPDATE [HumanResources].[Shift]
   4: SET        ModifiedDate = GETDATE()
   5: WHERE  ShiftID = 3
   6: GO

Now to check the contents of the tracking table  table cdc.HumanResources_Shift_CT and see whether that change is captured.

clip_image016

The tracking table is empty because it only tracks the changes which it contains, and it ignores any changes in other columns.

Retrieve Captured Data of Specific Time Frame

Quite often, one is asked for data to be tracked over a  time interval. If you look at the tracking data there is apparently no time captured at all. It always provides all the information. However, there are few fields which can definitely help us out i.e. _$start_lsn . LSN stands for Last Sequence Number. Every record in transaction log is uniquely identified by a LSN. They are always incrementing numbers.

LSN numbers are always associated with time and their mapping can be found after querying system table  cdc.lsn_time_mapping. This table is one of the tables which was created when AdventureWorks database was enabled for CDC. You can run this query to get all the data in the table  cdc.lsn_time_mapping.

   1: USE AdventureWorks
   2: GO
   3: SELECT *
   4: FROM cdc.lsn_time_mapping
   5: GO

When  this query is run it will give us all the rows of table. It is a little difficult to find the  necessary information from all the data. The usual case is when we need to inspect a change that occurred in a particular  time period.

clip_image017

We can find the time that corresponds to the LSN by using the system function sys.fn_cdc_map_time_to_lsn. If we want all the changes done yesterday, we can run this function as described below and it will return all the rows from yesterday.

Before we run this query let us explore two table valued functions (TVF) in AdventureWorks database. You can see that there are two new TVF are created with schema cfc. These functions are created when table level CDC was enabled.

clip_image018

The function cdc.fn_cdc_get_all_changes_HumanResources_Shift can be used to get events that occurred over a particular time period. You can run this T-SQL script to get event happened during any specific time period. In our case, we will be retrieving this data for the past 24 hours.

Following query should do retrieve data which was modified in the past 24 hours..

   1: USE AdventureWorks
   2: GO
   3: DECLARE @begin_time DATETIME, @end_time DATETIME, @begin_lsn BINARY(10), @end_lsn BINARY(10);
   4: SELECT @begin_time = GETDATE()-1, @end_time = GETDATE();
   5: SELECT @begin_lsn = sys.fn_cdc_map_time_to_lsn('smallest greater than', @begin_time);
   6: SELECT @end_lsn = sys.fn_cdc_map_time_to_lsn('largest less than or equal', @end_time);
   7: SELECT *
   8: FROM cdc.fn_cdc_get_all_changes_HumanResources_Shift(@begin_lsn,@end_lsn,'all')
   9: GO

we have used relational operations in the function sys.fn_cdc_map_time_to_lsn. There can be total of four different relational operations available to use in that function:

  • largest less than
  • largest less than or equal
  • smallest greater than
  • smallest greater than or equal

This way captured data can be queried very easily and query based on time interval.

Automatic Clean Up Process

If we track every change of all the  data in our database, there is very good chance that we will outgrow the hard drive of main server. This will also lead to issues with maintenance and input/output buffer issues.

In CDC this there is automatic cleanup process that runs at regular intervals. By default the interval is of 3 days but it can be configured. We have observed that, when we enable CDC on the database, there is one additional system stored procedure created with the  name sys.sp_cdc_cleanup_change_table which cleans up all the tracked data at interval.

Summary

For years, programmers have tried to create systems that record all the changes made to the data in a database application.  At last, with SQL Server 2008, we have a robust way, CDC, that comes ‘out of the box’ to deliver this functionality in a standard way. This should be useful for auditing databases and for tracking obscure problems that require you to know exactly when and where a change to a base table was made.
This article has been written keeping SQL Server 2008 SP1 Cumulative Update 3 in mind. I would encourage any of your suggestions or ideas on this subject as comments to the article.

Introduction to Change Data Capture (CDC) in SQL Server 2008[转] – Artech – 博客园已关闭评论
2019年7月16日 By mikel 分类: 数据库

来源: SQL Server 变更数据捕获(CDC) – pursuer.chen – 博客园

变更数据捕获用于捕获应用到 SQL Server 表中的插入、更新和删除活动,并以易于使用的关系格式提供这些变更的详细信息。变更数据捕获所使用的更改表中包含镜像所跟踪源表列结构的列,同时还包含了解所发生的变更所需的元数据。

对表开启了变更捕获之后,对该表的所有DML和DDL操作都会被记录,有助于跟踪表的变化。

测试环境: SQL SERVER 2008 R2,案例库AdventureWorks2008R2

 

目录

配置变更数据捕获

启动数据库捕获

复制代码
--开启cdc
USE [AdventureWorks2008R2]
EXECUTE sys.sp_cdc_enable_db
GO
---如果开启数据库捕获报错误:15517,使用下面的方法处理
ALTER AUTHORIZATION ON DATABASE::[AdventureWorks2008R2] TO [sa]

---查看是否设置成功,1代表开启cdc
SELECT is_cdc_enabled  FROM SYS.databases WHERE name='AdventureWorks2008R2'
复制代码

设置跟踪表

为当前数据库中指定的源表启用变更数据捕获。对表启用变更数据捕获时,应用于此表的每个数据操纵语言 (DML) 操作的记录都将写入事务日志中。变更数据捕获进程将从日志中检索此信息,并将其写入可通过使用一组函数访问的更改表中。

格式:

复制代码
sys.sp_cdc_enable_table 
    [ @source_schema = ] 'source_schema', ---表所属的架构名
    [ @source_name = ] 'source_name' ,----表名
    [ @role_name = ] 'role_name'---是用于控制更改数据访问的数据库角色的名称。
    [,[ @capture_instance = ] 'capture_instance' ]--是用于命名变更数据捕获对象的捕获实例的名称,这个名称在后面的存储过程和函数中需要经常用到。
    [,[ @supports_net_changes = ] supports_net_changes ]---指示是否对此捕获实例启用净更改查询支持如果此表有主键,或者有已使用 @index_name 参数进行标识的唯一索引,则此参数的默认值为 1。否则,此参数默认为 0。
    [,[ @index_name = ] 'index_name' ]--用于唯一标识源表中的行的唯一索引的名称。index_name 为 sysname,并且可以为 NULL。如果指定,则 index_name 必须是源表的唯一有效索引。如果指定 index_name,则标识的索引列优先于任何定义的主键列,就像表的唯一行标识符一样。
    [,[ @captured_column_list = ] 'captured_column_list' ]--需要对哪些列进行捕获。captured_column_list 的数据类型为 nvarchar(max),并且可以为 NULL。如果为 NULL,则所有列都将包括在更改表中。
    [,[ @filegroup_name = ] 'filegroup_name' ]--是要用于为捕获实例创建的更改表的文件组。
  [,[ @partition_switch = ] 'partition_switch' ]--指示是否可以对启用了变更数据捕获的表执行 ALTER TABLE 的 SWITCH PARTITION 命令。allow_partition_switch 为 bit,默认值为 1。
复制代码

注意:
对表启用变更数据捕获时,将生成一个更改表以及一个或两个查询函数。更改表充当捕获进程从事务日志中提取的源表更改的存储库。查询函数则用于从更改表中提取数据。这些函数的名称按以下方式派生自 capture_instance 参数:

所有更改函数: cdc.fn_cdc_get_all_changes_ < capture_instance >
净更改函数: cdc.fn_cdc_get_net_changes_ < capture_instance >

1.如果源表是数据库中第一个要启用变更数据捕获的表,并且数据库不存在事务发布,则 sys.sp_cdc_enable_table 还将为数据库创建捕获和清理作业。
它将 sys.tables 目录视图中的 is_tracked_by_cdc 列设置为 1。
2.对表启用变更数据捕获时,SQL Server 代理不必正在运行。但是,只有当 SQL Server 代理正在运行时,捕获进程才会处理事务日志并将条目写入更改表。

复制代码
USE AdventureWorks2008R2;
GO
EXEC sys.sp_cdc_enable_table
    @source_schema = N'HumanResources'
  , @source_name = N'Department'
  , @role_name = N'cdc_admin'--增加的角色
  , @capture_instance = N'HR_Department'--实例名 
  , @supports_net_changes = 1
  , @index_name = N'AK_Department_Name' 
  , @captured_column_list = N'Name, GroupName'--跟踪的字段
  , @filegroup_name = N'PRIMARY';
GO
---查询表是否启动跟踪,值为1标示已启动
SELECT is_tracked_by_cdc  FROM SYS.tables WHERE name='Department'
复制代码

数据库的变化

  •  增加的系统表

  • 增加的系统视图

 

  • 增加的系统存储过程

  • 增加的函数

 

 

  • 增加的用户与角色
---所有cdc有关的对象
SELECT * FROM SYS.all_objects  WHERE name LIKE '%CDC%'OR SCHEMA_ID IN(SELECT SCHEMA_ID FROM SYS.schemas WHERE name='CDC')
 ORDER BY TYPE
SELECT * FROM msdb.SYS.objects WHERE name='cdc_jobs'

对象分析

插入测试数据

复制代码
---查询插入数据之前表中的内容
USE [AdventureWorks2008R2] 
SELECT * FROM [HumanResources].[Department]


--1.插入一条记录
INSERT INTO [AdventureWorks2008R2].[HumanResources].[Department]([Name],[GroupName],[ModifiedDate])
VALUES('TEST','TEST',GETDATE())
GO
--更新一条记录
UPDATE [HumanResources].[Department]
SET GroupName='TEST1'
WHERE GroupName='TEST'

--增加一个字段
ALTER TABLE [HumanResources].[Department]
ADD Dcolumn INT
复制代码

分析系统表

复制代码
SELECT * FROM cdc.ddl_history---与DDL有关的操作记录
复制代码
复制代码
SELECT * FROM cdc.HR_Department_CT
/*
__$operation字段的值:
1 = 删除
2 = 插入
3 = 更新(旧值)
列数据中具有执行更新语句之前的行值。
4 = 更新(新值)
列数据中具有执行更新语句之后的行值。
*/
复制代码
为每个在更改表中存在行的事务返回一行。该表用于在日志序列号 (LSN) 提交值和提交事务的时间之间建立映射。没有对应的更改表项的项也可以记录下来,以便表在变更活动少或者无变更活动期间将 LSN 处理的完成过程记录下来。
SELECT * FROM cdc.lsn_time_mapping

分析存储过程

复制代码
---查询当前作业配置
SELECT * FROM MSDB.dbo.cdc_jobs
--或者使用
USE AdventureWorks2008R2;
GO
EXEC sys.sp_cdc_help_jobs;
GO
1.sys.sp_cdc_add_job
在当前数据库中创建变更数据捕获清理或捕获作业

    1.创建捕获作业
    USE AdventureWorks2008R2;
    GO
    EXEC sys.sp_cdc_add_job 
         @job_type = N'capture';
    GO
    2.创建清理作业
    ---创建清理作业,作业连续运行,更改数据行将在更改表中保留2880分钟,清除时使用一条语句最多删除4000条记录
    USE AdventureWorks2008R2;
    GO
    EXEC sys.sp_cdc_add_job
         @job_type = N'cleanup'
         ,@start_job=1
         ,@retention=2880
         ,@threshold =4000
2.sys.sp_cdc_change_job
修改当前数据库中变更数据捕获清除或捕获作业的配置
--仅在使用 sp_cdc_stop_job 停止作业并使用 sp_cdc_start_job 重新启动该作业后,对该作业所做的更改才会生效

    1.更改捕获作业
    --将每个循环扫描最多处理的事务数更改为200,为了从日志中提取所有行而要执行的最大扫描循环50次
    USE AdventureWorks2008R2;
    GO
    EXECUTE sys.sp_cdc_change_job 
        @job_type = N'capture',
        @maxtrans = 200,
        @maxscans = 50;
        
    GO
    2.更改清除作业,将记录保留时间更改为3440分钟
    USE AdventureWorks2008R2;
    GO
    EXECUTE sys.sp_cdc_change_job 
        @job_type = N'cleanup',
        @retention = 3440;
    GO
3.sys.sp_cdc_cleanup_change_table
根据指定的 low_water_mark 值从当前数据库的更改表中删除行,重置更改表中的最小 __$start_lsn,并删除小于该值的数据.
将同时清除cdc.HR_Department_CT,cdc.lsn_time_mapping表的记录

    USE AdventureWorks2008R2;
    GO
    EXEC sys.sp_cdc_cleanup_change_table 
    @capture_instance =N'HR_Department',
    @low_water_mark=0x0000037D000000D30008,
    @threshold=2000;
    
    
    SELECT sys.fn_cdc_increment_lsn(sys.fn_cdc_get_max_lsn())
4.sys.sp_cdc_drop_job
从 msdb 中删除当前数据库的变更数据捕获清除或捕获作业。

--下例删除 AdventureWorks2008R2 数据库的清除作业和捕获作业
    USE AdventureWorks2008R2;
    GO
    EXEC sys.sp_cdc_drop_job @job_type = N'cleanup';
    USE AdventureWorks2008R2;
    GO
    EXEC sys.sp_cdc_drop_job @job_type = N'capture';

5.sys.sp_cdc_disable_db    
对当前数据库禁用变更数据捕获

禁用当前对数据库中的所有表启用的变更数据捕获。与变更数据捕获相关的所有系统对象(如更改表、作业、存储过程和函数)都将被删除。sys.databases 目录视图中的数据库条目的 is_cdc_enabled 列设置为 0。
如果在禁用变更数据捕获时为数据库定义了很多捕获实例,则长时间运行事务可能导致 sys.sp_cdc_disable_db 的执行失败。通过在运行 sys.sp_cdc_disable_db 之前使用 sys.sp_cdc_disable_table 禁用单个捕获实例,可以避免此问题。
    USE AdventureWorks2008R2;
    GO
    EXECUTE sys.sp_cdc_disable_db;
    GO
6.sys.sp_cdc_disable_table
对当前数据库中指定的源表和捕获实例禁用变更数据捕获
删除与指定的源表和捕获实例相关联的变更数据捕获更改表和系统函数。它会删除任何与来自变更数据捕获系统表的指定捕获实例相关联的行,并将 sys.tables 目录视图中的表项的 is_tracked_by_cdc 列设置为 0。

---下例对 HumanResources.Department 表禁用了变更数据捕获
    USE AdventureWorks2008R2;
    GO
    EXEC sys.sp_cdc_disable_table
        @source_schema = N'HumanResources'
      , @source_name = N'Department'
      , @capture_instance = N'HR_Department' ---这里是定义的实例名称,在一开始创建捕获的时候创建的,这里也可以制定ALL(禁用表HumanResources.Department的所有捕获),



7.sys.sp_cdc_enable_db
对当前数据库启用变更数据捕获。必须先对数据库执行此过程,然后才能对该数据库中的任何表启用变更数据捕获。变更数据捕获可记录应用到所启用的表中的插入、更新和删除活动,同时采用易于使用的关系格式提供变更详细信息。此操作将为已修改的行捕获反映了所跟踪源表列结构的列信息,同时还捕获将更改应用到目标环境所需的元数据。
将创建以全数据库为作用域的变更数据捕获对象,包括元数据表和 DDL 触发器。它还会创建 cdc 架构和 cdc 数据库用户,并将 sys.databases 目录视图中的数据库条目的 is_cdc_enabled 列设置为 1。
    USE AdventureWorks2008R2;
    GO
    EXECUTE sys.sp_cdc_enable_db;
    GO

8.sys.sp_cdc_enable_table
--对需要进行跟踪的表启动CDC,sys.sp_cdc_enable_table
/*
为当前数据库中指定的源表启用变更数据捕获。对表启用变更数据捕获时,应用于此表的每个数据操纵语言 (DML) 操作的记录都将写入事务日志中。
变更数据捕获进程将从日志中检索此信息,并将其写入可通过使用一组函数访问的更改表中。
*/
sys.sp_cdc_enable_table 
    [ @source_schema = ] 'source_schema', ---表所属的架构名
    [ @source_name = ] 'source_name' ,----表名
    [ @role_name = ] 'role_name'---是用于控制更改数据访问的数据库角色的名称。
    [,[ @capture_instance = ] 'capture_instance' ]--是用于命名特定于实例的变更数据捕获对象的捕获实例的名称
    [,[ @supports_net_changes = ] supports_net_changes ]---指示是否对此捕获实例启用净更改查询支持
    [,[ @index_name = ] 'index_name' ]--用于唯一标识源表中的行的唯一索引的名称。index_name 为 sysname,并且可以为 NULL。如果指定,则 index_name 必须是源表的唯一有效索引。如果指定 index_name,则标识的索引列优先于任何定义的主键列,就像表的唯一行标识符一样。
    [,[ @captured_column_list = ] 'captured_column_list' ]--需要对哪些列进行捕获。captured_column_list 的数据类型为 nvarchar(max),并且可以为 NULL。如果为 NULL,则所有列都将包括在更改表中。
    [,[ @filegroup_name = ] 'filegroup_name' ]--是要用于为捕获实例创建的更改表的文件组。
  [,[ @partition_switch = ] 'partition_switch' ]--指示是否可以对启用了变更数据捕获的表执行 ALTER TABLE 的 SWITCH PARTITION 命令。allow_partition_switch 为 bit,默认值为 1。
  
9.sp_cdc_generate_wrapper_function 
 生成用于为 SQL Server 中可用的变更数据捕获查询函数创建包装函数的脚本
     EXEC sys.sp_cdc_generate_wrapper_function 
 

10. sys.sp_cdc_help_change_data_capture 
返回当前数据库中为变更数据捕获启用的每个表的变更数据捕获配置。最多可为每个源表返回两行,为每个捕获实例返回一行。
---返回制定表的捕获信息
    USE AdventureWorks2008R2;
    GO
    EXECUTE sys.sp_cdc_help_change_data_capture 
        @source_schema = N'HumanResources', --架构名
        @source_name = N'Department';--表名
    GO
    --返回所有表的捕获信息
    USE AdventureWorks2008R2;
    GO
    EXECUTE sys.sp_cdc_help_change_data_capture 
     
11.sys.sp_cdc_get_captured_columns
返回指定捕获实例所跟踪的捕获源列的变更数据捕获元数据信息。
    USE AdventureWorks2008R2;
    GO
    EXECUTE sys.sp_cdc_get_captured_columns 
        @capture_instance = N'HR_Department';
    GO

12.sys.sp_cdc_get_ddl_history
返回自对指定的捕获实例启用变更数据捕获后与该捕获实例关联的数据定义语言 (DDL) 更改历史记录。
与查询表是一样的结果
    SELECT * FROM cdc.ddl_history

    USE AdventureWorks2008R2;
    GO
    EXECUTE sys.sp_cdc_get_ddl_history
    @capture_instance = N'HR_Department';

13.sp_cdc_help_jobs
报告关于当前数据库中所有变更数据捕获清除或捕获作业的信息。因为一个数据库只会在第一个表创建捕获的时候创建作业
所以这里只需要在当前库执行就可以。
    sys.sp_cdc_help_jobs
    USE AdventureWorks2008R2;
    GO
    EXEC sys.sp_cdc_help_jobs;
    GO
14.sp_cdc_scan
执行变更数据捕获日志扫描操作,需要进行捕获的时候使用,默认情况下5会自动进行捕获。
如果变更数据捕获正在使用 SQL Server 代理捕获作业,则 sys.sp_MScdc_capture_job 将内部调用 sys.sp_cdc_scan。如果变更数据捕获日志扫描操作已经处于活动状态,或数据库启用了事务复制,则无法显式执行此过程。此存储过程应当由需要自定义自动配置的捕获作业的行为的管理员使用。
    USE AdventureWorks2008R2;
    GO
    EXEC sp_cdc_scan

15.sys.sp_cdc_start_job,
启动和停止当前数据库的变更数据捕获清除或捕获作业。

    ---启动清除作业
    USE AdventureWorks2008R2;
    GO
    EXEC sys.sp_cdc_start_job @job_type = N'cleanup';

    ---停止捕获作业
    USE AdventureWorks2008R2;
    GO
    EXEC sys.sp_cdc_stop_job @job_type = N'capture';
    GO

    USE AdventureWorks2008R2;
    GO
    EXEC sys.sp_cdc_stop_job @job_type = N'cleanup';
    GO
复制代码

分析函数

复制代码
1.fn_cdc_get_all_changes_capture_instance
针对在指定日志序列号 (LSN) 范围内应用到源表的每项更改均返回一行
    USE AdventureWorks2008R2;
    GO

    DECLARE @from_lsn binary(10), @to_lsn binary(10)
    SET @from_lsn =
       sys.fn_cdc_get_min_lsn('HR_Department')
    SET @to_lsn   = sys.fn_cdc_get_max_lsn()
    SELECT * FROM cdc.fn_cdc_get_all_changes_HR_Department
      (@from_lsn, @to_lsn, N'all');
    GO
2.fn_cdc_get_net_changes_capture_instance
针对指定 LSN 范围内每个已更改的源行返回一个净更改行。也就是说,如果在 LSN 范围内源行具有多项更改,
则该函数将返回反映该行最终内容的单一行。例如,如果事务在源表中插入一行,并且 LSN 范围内的后续事务更新了该行中的一个或多个列,
则该函数将只返回一行,其中包含多个更新的列值。
如果值最后是删除操作,则不返回该LSN的值
    USE AdventureWorks2008R2;
    GO
    DECLARE @begin_time datetime, @end_time datetime, @from_lsn binary(10), @to_lsn binary(10);
    -- Obtain the beginning of the time interval.
    SET @begin_time = GETDATE() -1;
    -- DML statements to produce changes in the HumanResources.Department table.
    INSERT INTO HumanResources.Department (Name, GroupName)
    VALUES (N'MyDept', N'MyNewGroup');

    UPDATE HumanResources.Department
    SET GroupName = N'Resource Control'
    WHERE GroupName = N'Inventory Management';

    DELETE FROM HumanResources.Department
    WHERE Name = N'MyDept';

    -- Obtain the end of the time interval.
    SET @end_time = GETDATE();
    -- Map the time interval to a change data capture query range.
    SET @from_lsn = sys.fn_cdc_map_time_to_lsn('smallest greater than or equal', @begin_time);
    SET @to_lsn = sys.fn_cdc_map_time_to_lsn('largest less than or equal', @end_time);

    -- Return the net changes occurring within the query window.
    SELECT * FROM cdc.fn_cdc_get_net_changes_HR_Department(@from_lsn, @to_lsn, 'all');
    
3.sys.fn_cdc_decrement_lsn
根据指定的 LSN 返回序列中的上一个日志序列号 (LSN)
    Use AdventureWorks2008R2;
    GO
    SELECT sys.fn_cdc_decrement_lsn(sys.fn_cdc_get_max_lsn())

下例在一个返回 LSN 值小于最大 LSN 值的更改数据行的查询中,使用 sys.fn_cdc_decrement_lsn 来设置 LSN 上限。

    Use AdventureWorks2008R2;
    GO
    DECLARE @from_lsn binary(10), @to_lsn binary(10);
    SET @from_lsn = sys.fn_cdc_get_min_lsn('HR_Department');
    SET @to_lsn = sys.fn_cdc_decrement_lsn(sys.fn_cdc_get_max_lsn());
    SELECT * FROM cdc.fn_cdc_get_net_changes_HR_Department( @from_lsn, @to_lsn, 'all'); 
    GO

4.sys.fn_cdc_increment_lsn
根据指定的 LSN 返回序列中的下一个日志序列号 (LSN)。
此函数返回的 LSN 值始终大于指定的值,并且不存在介于这两个值之间的 LSN 值。
若要系统地查询随时间变化的更改数据流,可以定期重复调用该查询函数,每次调用时指定一个新的查询间隔来限定查询中返回的更改的范围。为帮助确保不丢失数据,通常使用前一个查询的上限来生成后一个查询的下限。由于查询间隔是一个闭区间,因此新的下限必须大于前一个上限,但要足够小,以确保不存在 LSN 值介于此值与旧上限之间的更改。sys.fn_cdc_increment_lsn 函数就是用来获取此值的。
    Use AdventureWorks2008R2;
    GO
    SELECT sys.fn_cdc_increment_lsn(sys.fn_cdc_get_max_lsn())


5.sys.fn_cdc_get_column_ordinal
返回实例制定列的列序号。

    Use AdventureWorks2008R2;
    GO
    SELECT sys.fn_cdc_get_column_ordinal ( 'HR_Department','NAME');


6.sys.fn_cdc_get_max_lsn
返回 cdc.lsn_time_mapping 系统表的 start_lsn 列中的最大日志序列号 (LSN)。您可以使用此函数为任何捕获实例返回变更数据捕获时间线的高端点
    USE AdventureWorks2008R2;
    GO
    SELECT sys.fn_cdc_get_max_lsn()AS max_lsn;

    SELECT  sys.fn_cdc_get_min_lsn(N'HR_Department');


    USE AdventureWorks2008R2;
    GO
    DECLARE @from_lsn binary(10), @to_lsn binary(10);
    SET @from_lsn = sys.fn_cdc_get_min_lsn(N'HR_Department');
    SET @to_lsn = sys.fn_cdc_get_max_lsn();
    SELECT * FROM cdc.fn_cdc_get_net_changes_HR_Department(@from_lsn, @to_lsn, 'all');
    GO

7.sys.fn_cdc_get_min_lsn
    USE AdventureWorks2008R2;
    GO
    SELECT sys.fn_cdc_get_min_lsn ('HR_Department')AS min_lsn;---查询制定的实例名的最小LSN

8.sys.fn_cdc_has_column_changed ( 'capture_instance','column_name' , update_mask )
标识指定的更新掩码是否指示已更新关联的更改行中的指定列。
    USE AdventureWorks2008R2;
    GO
    SELECT sys.fn_cdc_has_column_changed ('HR_Department','name' , 2)


9.sys.fn_cdc_is_bit_set
指示捕获的列是否已更新,采用的方法是检查是否在提供的位掩码内设置了其序号位置。

    USE AdventureWorks2008R2;
    GO
    DECLARE @from_lsn binary(10), @to_lsn binary(10), @GroupNm_ordinal int;
    SET @from_lsn = sys.fn_cdc_get_min_lsn('HR_Department');
    SET @to_lsn = sys.fn_cdc_get_max_lsn();
    SET @GroupNm_ordinal = sys.fn_cdc_get_column_ordinal('HR_Department','GroupName');
    SELECT sys.fn_cdc_is_bit_set(@GroupNm_ordinal,__$update_mask) as 'IsGroupNmUpdated', *
    FROM cdc.fn_cdc_get_all_changes_HR_Department( @from_lsn, @to_lsn, 'all')
    WHERE __$operation = 4;
    GO

10.sys.fn_cdc_map_lsn_to_time
为指定的日志序列号 (LSN) 返回 cdc.lsn_time_mapping 系统表的 tran_end_time 列中的日期和时间值。
您可以使用此函数系统地将 LSN 范围映射到更改表中的日期范围
    SELECT sys.fn_cdc_map_lsn_to_time(sys.fn_cdc_get_max_lsn());

11.sys.fn_cdc_map_time_lsn
下面的示例使用 sys.fn_cdc_map_time_lsn 函数来确定在 cdc.lsn_time_mapping 表中是否有 tran_end_time 
值大于或等于指定时间的行。例如,可以用此查询来确定捕获进程是否已处理完截至前指定时间提交的更改
    DECLARE @extraction_time datetime, @lsn binary(10);
    SET @extraction_time = GETDATE();
    SELECT @lsn = sys.fn_cdc_map_time_to_lsn ('smallest greater than or equal', @extraction_time);
    IF @lsn IS NOT NULL
    BEGIN
    print '...'
    END

    DECLARE @begin_time datetime, @end_time datetime, @begin_lsn binary(10), @end_lsn binary(10);
    SET @begin_time = '2015-04-07 18:00:00.000';
    SET @end_time = '2015-04-08 18:00:00.000';
    SELECT @begin_lsn = sys.fn_cdc_map_time_to_lsn('smallest greater than', @begin_time);
    SELECT @end_lsn = sys.fn_cdc_map_time_to_lsn('largest less than or equal', @end_time);
    SELECT * FROM cdc.fn_cdc_get_net_changes_HR_Department(@begin_lsn, @end_lsn, 'all ');
复制代码

分析系统视图

复制代码
1.sys.dm_cdc_log_scan_sessions
针对当前数据库中的每个日志扫描会话返回一行。返回的最后一行表示当前会话。您可以使用此视图返回有关当前日志扫描会话的状态信息,
或有关自 SQL Server 实例上次启动以来所有会话的聚合信息。
    USE AdventureWorks2008R2;
    SELECT *
    FROM sys.dm_cdc_log_scan_sessions
--可以观察empty_scan_count字段的值可以发现它的变化,5秒增加一次,和前面配置的日志扫描作业的频率是一样的
    USE AdventureWorks2008R2;
    GO
    print getdate()
    SELECT empty_scan_count
    FROM sys.dm_cdc_log_scan_sessions
    WHERE session_id = (SELECT MAX(b.session_id) from sys.dm_cdc_log_scan_sessions AS b)

    waitfor DELAY '00:01' 

    print getdate()
    SELECT empty_scan_count
    FROM sys.dm_cdc_log_scan_sessions
    WHERE session_id = (SELECT MAX(b.session_id) from sys.dm_cdc_log_scan_sessions AS b)

2.sys.dm_cdc_errors
为变更数据捕获日志扫描会话中遇到的每个错误返回一行。
    USE AdventureWorks2008R2;
    GO
    SELECT *
    FROM sys.dm_cdc_errors
复制代码

 

总结

捕获方法可以跟踪一个表对象的更改操作,但是开启了变更捕获对性能存在一定的影响,特别在日志读写这一块,首先它比正常的操作需要进行更多的日志写操作,而且日志的读操作也是很频繁的,有时候可能会引起日志等待类型,所以要慎重使用。

SQL Server 变更数据捕获(CDC) – pursuer.chen – 博客园已关闭评论
2019年7月16日 By mikel 分类: 数据库

来源: SQL Server 2008中新增的变更数据捕获(CDC)和更改跟踪 – 邀月 – 博客园

SQL Server 2008中SQL应用系列–目录索引

本文主要介绍SQL Server中记录数据变更的四个方法:触发器、Output子句、变更数据捕获(Change Data Capture 即CDC)功能、同步更改跟踪。其中后两个为SQL Server 2008所新增。

一、触发器

SQL Server的早期版本中,如果要记录某个表或视图的Insert/Update/Delete操作,我们可以借助触发器(Trigger)(http://msdn.microsoft.com/zh-cn/library/ms189799.aspx),这在数据量较小的情况下往往是有效的方式之一,其中后触发器(After Trigger)只能跟踪表的三个操作中的任意组合,而前触发器(Instead Of trigger)可以处理表和视图的更新(即使普通的Update View语句在某些列不明确的情况下报错)。我们看两个例子:

准备基础数据:

复制代码
USE testDb2
GO
--创建两个测试表
IF NOT OBJECT_ID('DepartDemo') IS NULL
DROP TABLE [DepartDemo]
GO
IF NOT OBJECT_ID('DepartChangeLogs') IS NULL
DROP TABLE [DepartChangeLogs]
GO
--测试表
CREATE TABLE [dbo].[DepartDemo](
[DID] [int] IDENTITY(101,1) NOT NULL PRIMARY KEY,
[DName] [nvarchar](200) NULL,
[DCode] [nvarchar](500) NULL,
[Manager] [nvarchar](50) NULL,
[ParentID] [int] NOT NULL DEFAULT ((0)),
[AddUser] [nvarchar](50) NULL,
[AddTime] [datetime] NULL,
[ModUser] [nvarchar](50) NULL,
[ModTime] [datetime] NULL,
[CurState] [smallint] NOT NULL DEFAULT ((0)),
[Remark] [nvarchar](500) NULL,
[F1] [int] NOT NULL DEFAULT ((0)),
[F2] [nvarchar](300) NULL
)
GO
--记录日志表
CREATE TABLE [DepartChangeLogs]
([LogID] [bigint] IDENTITY(1001,1) NOT NULL PRIMARY KEY,
[DID] [int] NOT NULL,
[DName] [nvarchar](200) NULL,
[DCode] [nvarchar](500) NULL,
[Manager] [nvarchar](50) NULL,
[ParentID] [int] NOT NULL DEFAULT ((0)),
[AddUser] [nvarchar](50) NULL,
[AddTime] [datetime] NULL,
[ModUser] [nvarchar](50) NULL,
[ModTime] [datetime] NULL,
[CurState] [smallint] NOT NULL DEFAULT ((0)),
[Remark] [nvarchar](500) NULL,
[F1] [int] NOT NULL DEFAULT ((0)),
[F2] [nvarchar](300) NULL,
[LogTime] DateTime Default(Getdate()) Not Null,
[InsOrUpd] char not null
)
GO
复制代码

创建触发器:

复制代码
/*******   创建一个After DML触发器  ******/
/*********  3w@live.cn  邀月***************/
CREATE TRIGGER dbo.tri_LogDepartDemo
ON [dbo].[DepartDemo]
AFTER INSERT, Delete  /************此处使用update与“Insert,Delete”效果是一样的,邀月注  **********/
AS
SET NOCOUNT ON   --屏蔽触发器发送“受影响的行数”给应用程序

-- Inserted rows
INSERT [DepartChangeLogs]
(DID,[DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2],
 LogTime, InsOrUPD)
SELECT DISTINCT DID,[DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2],
 GETDATE(), 'I'
FROM inserted i

-- Deleted rows
INSERT [DepartChangeLogs]
(DID,[DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2],
 LogTime, InsOrUPD)
SELECT DISTINCT DID,[DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2],
 GETDATE(), 'D'
FROM deleted d
GO

INSERT [dbo].[DepartDemo] ([DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2])
VALUES (N'国家统计局房产审计一科', N'0', N'胡不归', 0, N'DeomUser',
 CAST(0x00009DF7017B6F96 AS DateTime), N'', CAST(0x0000000000000000 AS DateTime),
 1, N'专业评估全国房价,为老百姓谋福祉', 0, N'')
GO

----该Update不会被触发器记录,但Update会生效
UPDATE departDemo SET [Manager]='任我行' WHERE DID=101
GO

DELETE FROM departDemo where DID=101
GO

SELECT * FROM [DepartChangeLogs]
复制代码

统计效果:
邀月工作室
如果你觉得触发器过于浪费,你可以试着根据某些字段以缩小触发器的范围

复制代码
/********* 使用DML触发器记录特定列的修改 ***/
/*********  3w@live.cn  邀月***************/
CREATE TRIGGER dbo.[tri_LogDepartDemo2]
ON [dbo].[DepartDemo]
AFTER Update
AS
IF Update([Manager])
    Begin
        print '该部门主管实行终身任免制,不得中途更改!'
        Rollback ----回滚Update操作
    End

GO
UPDATE departDemo SET [Manager]='任我行' WHERE DID=101
GO
复制代码

执行结果:

邀月工作室
但触发器的缺陷也是显而易见的,使用触发器请注意以下几点:

1、触发器通常很隐蔽,换句话说,易忘记,特别在检查性能和逻辑问题时。

2、长时间运行的触发器会严重减慢数据操作,特别是在数据频繁修改的数据库中。

3、不记录日志的更新不会引起DML触发器的触发(如WRITETEXT、Trunacte table及批量插入操作)。

4、约束通常比触发器运行更快。

5、处理某些逻辑时,存储过程通常比触发器要更易维护和管理。

6、不允许在触发器中使用Select返回结果集。

关于触发器的更多内容,请看MSDN(http://msdn.microsoft.com/zh-cn/library/ms189799.aspx

 

二、使用Output子句

  官方解释:OutPut子句(http://technet.microsoft.com/zh-cn/library/ms177564.aspx返回受 INSERT、UPDATE、DELETE 或 MERGE 语句影响的各行中的信息,或返回基于受这些语句影响的各行的表达式。 这些结果可以返回到处理应用程序,以供在确认消息、存档以及其他类似的应用程序要求中使用。 也可以将这些结果插入表或表变量。 另外,您可以捕获嵌入的 INSERT、UPDATE、DELETE 或 MERGE 语句中 OUTPUT 子句的结果,然后将这些结果插入目标表或视图。

举例:

复制代码
 /********* 使用Output记录表记录的修改 *****/
/*********  3w@live.cn  邀月***************/

----删除前面的触发器
Drop TRIGGER dbo.[tri_LogDepartDemo]
DROP TRIGGER dbo.[tri_LogDepartDemo2]

INSERT [dbo].[DepartDemo] ([DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2])
OUTPUT Inserted.*,getdate(),'I' ---注意这行是新增的
INTO DepartChangeLogs           ---注意这行是新增的
VALUES (N'发改委', N'0', N'向问天', 0, N'DeomUser',
 CAST(0x00009DF7017B6F96 AS DateTime), N'', CAST(0x0000000000000000 AS DateTime),
 1, N'油价,我说了算', 0, N'')
GO

SELECT * FROM [DepartChangeLogs]
复制代码

邀月工作室

注意:

1、从OUTPUT 中返回的列反映 INSERT、UPDATE 或 DELETE 语句完成之后但在触发器执行之前的数据。

2、SQL Server 并不保证由使用 OUTPUT 子句的 DML 语句处理和返回行的顺序。

3、与触发器相比,OutPut子句可以直接处理Merge语句。

以上两种方法各有千秋,在合适的情况下采取合适的方法才是明智的选择,令人惊喜的是,SQL Server 2008起,为我们提供了更为强大的内建的方法-变更数据捕获(CDC,http://msdn.microsoft.com/zh-cn/library/bb500244%28v=sql.100%29.aspx)和更改跟踪,下面我们隆重介绍它们。

三、使用“变更数据捕获”(CDC)功能

SQL Server 2008提供了内建的方法变更数据捕获(Change Data Capture 即CDC)以实现异步跟踪用户表的数据修改,而且这一功能拥有最小的性能开销。可以用于其他数据源的持续更新,例如将OLTP数据库中的数据变更迁移到数据仓库数据库。

要使用CDC功能,首先我们得在数据库中启用该功能。在此我们沿用上例中使用的数据库Testdb2

复制代码
/**************异步跟踪数据更新演示*************/
/************* 3w@live.cn 邀月***************/
use master
GO

IF EXISTS (SELECT [name] FROM sys.databases WHERE name = 'TestDb2')
drop DATABASE TestDb2
Go
CREATE DATABASE TestDb2
GO

--查看是否启用CDC
SELECT is_cdc_enabled FROM sys.databases WHERE name = 'TestDb2'

USE TestDb2
GO
----启用当前数据库的CDC功能
EXEC sys.sp_cdc_enable_db
GO
复制代码

如果报15517错误,请换用其他owner,邀月注

复制代码
SELECT is_cdc_enabled FROM sys.databases WHERE name = 'TestDb2'

/*
is_cdc_enabled
1
*/
USE testDb2
GO

CREATE TABLE [dbo].[DepartDemo](
[DID] [int] IDENTITY(101,1) NOT NULL PRIMARY KEY,
[DName] [nvarchar](200) NULL,
[DCode] [nvarchar](500) NULL,
[Manager] [nvarchar](50) NULL,
[ParentID] [int] NOT NULL DEFAULT ((0)),
[AddUser] [nvarchar](50) NULL,
[AddTime] [datetime] NULL,
[ModUser] [nvarchar](50) NULL,
[ModTime] [datetime] NULL,
[CurState] [smallint] NOT NULL DEFAULT ((0)),
[Remark] [nvarchar](500) NULL,
[F1] [int] NOT NULL DEFAULT ((0)),
[F2] [nvarchar](300) NULL
)
GO

/**********************************
需要启用SQL Server Agent服务,否则会报错,邀月注
SQLServerAgent is not currently running so it cannot be notified of this action.
***********************************/

/****** 捕获所有的行变更,只返回行的净变更,其他默认 *******/
EXEC sys.sp_cdc_enable_table
@source_schema = 'dbo',
@source_name = 'DepartDemo',
@role_name = NULL,
@capture_instance = NULL,
@supports_net_changes = 1,
@index_name = NULL,
@captured_column_list = NULL,
@filegroup_name = default
复制代码

注意此时,SQL Server 自启动了两个job,一个捕获,一个清除,注意清除是默认凌晨2点,清除72小时以上的数据。如果同一数据库的表中CDC已经启用,不会重建job。

复制代码
/*
Job 'cdc.TestDb2_capture' started successfully.
Job 'cdc.TestDb2_cleanup' started successfully.
*/

--确认表已经被跟踪
SELECT is_tracked_by_cdc FROM sys.tables
WHERE name = 'DepartDemo' and schema_id = SCHEMA_ID('dbo')
/*
is_tracked_by_cdc
1
*/

--确认
EXEC sys.sp_cdc_help_change_data_capture 'dbo', 'DepartDemo'
复制代码

邀月工作室

可以看到,SQL Server 增加了一个表[cdc].[dbo_DepartDemo_CT]
相比源表多了个字段:
[__$start_lsn]
,[__$end_lsn]
,[__$seqval]
,[__$operation]
,[__$update_mask]

邀月工作室

不建议直接查询该表,而应该使用下面的技巧

复制代码
USE TestDb2
GO

INSERT [dbo].[DepartDemo] ([DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2])
VALUES (N'银监会', N'0', N'云中鹤', 0, N'DemoUser1',
CAST(0x00009DF7017B6F96 AS DateTime), N'', CAST(0x0000000000000000 AS DateTime),
1, N'监管汇率', 0, N'')

INSERT [dbo].[DepartDemo] ([DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2])
VALUES (N'统计局', N'0', N'神算子', 0, N'DemoUser2',
CAST(0x00009DF7017B6F96 AS DateTime), N'', CAST(0x0000000000000000 AS DateTime),
1, N'统计数据', 0, N'')
GO

UPDATE [dbo].[DepartDemo]
SET Manager='段正淳'
WHERE DID =101

DELETE [dbo].[DepartDemo]
WHERE DID = 102
复制代码

要查询变更,我们需要借助大名鼎鼎的日志序列号(Log Sequence Numbers)即LSN(http://msdn.microsoft.com/zh-cn/library/ms190411%28v=sql.100%29.aspx)来实现LSN级别的跟踪数据变更。 下面示例中sys.fn_cdc_map_time_to_lsn(http://msdn.microsoft.com/zh-cn/library/bb500137%28v=sql.100%29.aspx)用于LSN转换为时间。

复制代码
/******* 使用LSN 查看CDC记录 *********/

--http://msdn.microsoft.com/zh-cn/library/bb500137%28v=sql.100%29.aspx
SELECT sys.fn_cdc_map_time_to_lsn
( 'smallest greater than or equal' , '2012-04-09 16:09:30') as BeginLSN

/*
BeginLSN
0x0000002C000000AA0003
*/

SELECT sys.fn_cdc_map_time_to_lsn
( 'largest less than or equal' , '2012-04-09 23:59:59') as EndLSN

/*
EndLSN
0x0000002C000001C20005
*/

/**************查看所有CDC记录*************/
/************* 3w@live.cn 邀月***************/

DECLARE @FromLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'smallest greater than or equal' , '2012-04-09 16:09:30')

DECLARE @ToLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'largest less than or equal' , '2012-04-09 23:59:59')

SELECT
__$operation,
__$update_mask,
DID,
DName,
Manager
FROM [cdc].[fn_cdc_get_all_changes_dbo_DepartDemo]
(@FromLSN, @ToLSN, 'all')

/************查看所有更新*************************

__$operation __$update_mask DID DName Manager
2 0x1FFF 105 银监会 云中鹤
2 0x1FFF 106 统计局 神算子
1 0x1FFF 101 银监会 段正淳
1 0x1FFF 103 银监会 云中鹤
1 0x1FFF 104 统计局 神算子
1 0x1FFF 105 银监会 云中鹤
1 0x1FFF 106 统计局 神算子
2 0x1FFF 107 银监会 云中鹤
2 0x1FFF 108 统计局 神算子
4 0x0008 107 银监会 段正淳
1 0x1FFF 108 统计局 神算子
*/

/**************查看所有CDC记录*************/
/************* 3w@live.cn 邀月***************/
DECLARE @FromLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'smallest greater than or equal' , '2012-04-09 16:09:30')

DECLARE @ToLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'largest less than or equal' , '2012-04-09 23:59:59')

--解释一下Operation的具体含义
SELECT
CASE __$operation
WHEN 1 THEN 'DELETE'
WHEN 2 THEN 'INSERT'
WHEN 3 THEN 'Before UPDATE'
WHEN 4 THEN 'After UPDATE'
END Operation,
__$update_mask,
DID,
DName,
Manager
FROM [cdc].[fn_cdc_get_all_changes_dbo_DepartDemo]
(@FromLSN, @ToLSN, 'all update old')
复制代码

邀月工作室

复制代码
/**************查看净更改(Net changes)CDC记录*************/
/************* 3w@live.cn 邀月 ***************/

INSERT [dbo].[DepartDemo] ([DName], [DCode], [Manager], [ParentID],
[AddUser], [AddTime], [ModUser], [ModTime], [CurState], [Remark], [F1], [F2])
VALUES (N'药监局', N'0', N'蝶谷医仙', 0, N'DemoUser3',
CAST(0x00009DF7017B6F96 AS DateTime), N'', CAST(0x0000000000000000 AS DateTime),
1, N'制定药价', 0, N'')
GO

UPDATE [dbo].[DepartDemo]
SET Manager='胡青牛'
WHERE DID =109

DECLARE @FromLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'smallest greater than or equal' , '2012-04-09 16:09:30')

DECLARE @ToLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'largest less than or equal' , '2012-04-09 23:59:59')

SELECT
CASE __$operation
WHEN 1 THEN 'DELETE'
WHEN 2 THEN 'INSERT'
WHEN 3 THEN 'Before UPDATE'
WHEN 4 THEN 'After UPDATE'
WHEN 5 THEN 'MERGE'
END Operation,
__$update_mask,
DID,
DName,
Manager
FROM [cdc].[fn_cdc_get_net_changes_dbo_DepartDemo]
(@FromLSN, @ToLSN, 'all with mask')
复制代码

邀月工作室

我们还可以通过转换CDC更新掩码获得更为直观的结果,这里需要借助于另外两个函数sys.fn_cdc_is_bit_set(http://msdn.microsoft.com/zh-cn/library/bb500241%28v=SQL.110%29.aspx)和sys.fn_cdc_get_column_ordinal(http://msdn.microsoft.com/zh-cn/library/bb522549%28v=SQL.100%29.aspx

 

复制代码
/************** 转换CDC更新掩码 *************/
/************* 3w@live.cn 邀月 **************/

UPDATE dbo.[DepartDemo]
SET [Manager] = '东方不败'
WHERE DID =107

UPDATE dbo.[DepartDemo]
SET ParentID = 109
WHERE DID =107

DECLARE @FromLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'smallest greater than or equal' , '2012-04-09 16:09:30')

DECLARE @ToLSN varbinary(10) =
sys.fn_cdc_map_time_to_lsn
( 'largest less than or equal' , '2012-04-09 23:59:59')

SELECT
sys.fn_cdc_is_bit_set (
sys.fn_cdc_get_column_ordinal (
'dbo_DepartDemo' , 'Manager' ),
__$update_mask) Manager_Updated,
sys.fn_cdc_is_bit_set (
sys.fn_cdc_get_column_ordinal (
'dbo_DepartDemo' , 'ParentID' ),
__$update_mask) ParentID_Updated,
DID,
Manager,
ParentID
FROM cdc.fn_cdc_get_all_changes_dbo_DepartDemo
(@FromLSN, @ToLSN, 'all')
WHERE __$operation = 4
复制代码

 

邀月工作室

除了前面介绍的指定LSN边界的方法,SQL Server还提供了一系列的获取边界的方法:

sys.fn_cdc_get_max_lsn(http://msdn.microsoft.com/zh-cn/library/bb500304%28v=sql.100%29.aspx

sys.fn_cdc_get_min_lsn(http://msdn.microsoft.com/zh-cn/library/bb510621%28v=sql.100%29.aspx

sys.fn_cdc_increment_lsn(http://msdn.microsoft.com/zh-cn/library/bb510745%28v=sql.100%29.aspx

sys.fn_cdc_decrement_lsn(http://msdn.microsoft.com/zh-cn/library/bb500246%28v=sql.100%29.aspx

示例如下:

复制代码
/************** 获取LSN边界的其他方法 *************/
/************* 3w@live.cn 邀月 **************/

--获取最小边界
SELECT sys.fn_cdc_get_min_lsn ('dbo_DepartDemo') Min_LSN
--获取可用的最大边界
SELECT sys.fn_cdc_get_max_lsn () Max_LSN
--获取最大边界的下一个序号
SELECT sys.fn_cdc_increment_lsn (sys.fn_cdc_get_max_lsn()) New_Lower_Bound_LSN
--获取最大边界的前一个序号
SELECT sys.fn_cdc_decrement_lsn (sys.fn_cdc_get_max_lsn())
New_Lower_Bound_Minus_one_LSN
复制代码

邀月工作室

通过以下存储过程在数据库和表级禁用CDC

sys.sp_cdc_disable_table (http://msdn.microsoft.com/zh-cn/library/bb510702(v=sql.100).aspx

sys.sp_cdc_disable_db(http://msdn.microsoft.com/zh-cn/library/bb522508(v=sql.100).aspx)注意,该命令同时也删除了CDC架构和相关的SQL代理作业。

复制代码
/************** 在数据库和表级禁用CDC *************/
/************* 3w@live.cn 邀月 **************/

EXEC sys.sp_cdc_disable_table 'dbo', 'DepartDemo', 'all'

SELECT is_tracked_by_cdc FROM sys.tables
WHERE name = 'DepartDemo' and schema_id = SCHEMA_ID('dbo')

--当前数据库上禁用CDC
EXEC sys.sp_cdc_disable_db
复制代码

 

四、使用“更改跟踪”以最小的磁盘开销跟踪净数据更改

CDC可以用来对数据库和数据仓库的持续数据变更进行异步数据跟踪,而SQL Server 2008中新增的“更改跟踪”却是一个同步进程,是DML操作本身(I/D/U)事务的一部分,它的最大优势是以最小的磁盘开销来侦测净行变更,它允许修改的数据以事务一致的形式表现,并提供了检测数据冲突的能力。它甚至可以根据外部传入的应用程序上下文,来完成更细颗粒度的更改处理,参看WITH CHANGE_TRACKING_CONTEXT (http://msdn.microsoft.com/zh-cn/library/bb895330%28v=sql.100%29.aspx

复制代码
 /***使用“更改跟踪”以最小的磁盘开销跟踪净数据更改****/
/************* 3w@live.cn 邀月 **************/

IF EXISTS (SELECT [name] FROM sys.databases WHERE name = 'TestDb4')
drop DATABASE TestDb4
Go
CREATE DATABASE TestDb4
GO

--启用更新跟踪,36小时清理一次
ALTER DATABASE TestDb4
SET CHANGE_TRACKING = ON
(CHANGE_RETENTION = 36 HOURS,
AUTO_CLEANUP = ON)
复制代码

注意,下一步是允许快照隔离,这是微软推祟的“最佳实践”,尽管这样行版本的生成会增加额外的空间使用,从而会增加总的I/O数量,但不使用快照会引发事务不一致的变更信息。

复制代码
ALTER DATABASE TestDb4
SET ALLOW_SNAPSHOT_ISOLATION ON
GO

SELECT DB_NAME(database_id) 数据库名称,is_auto_cleanup_on,
retention_period,retention_period_units_desc
FROM sys.change_tracking_databases
/*
数据库名称 is_auto_cleanup_on retention_period retention_period_units_desc
TestDb4 1 36 HOURS
*/

USE TestDb4
GO
--创建测试表
CREATE TABLE dbo.DepartDemo
([DID] [int] IDENTITY(101,1) NOT NULL PRIMARY KEY,
[DName] [nvarchar](200) NULL,
[Manager] [nvarchar](50) NULL,
[ParentID] [int] NOT NULL DEFAULT ((0)),
[CurState] [smallint] NOT NULL DEFAULT ((0)),
)
GO

----TRUNCATE table dbo.DepartDemo
----GO

--启用表的列更新跟踪
ALTER TABLE dbo.DepartDemo
ENABLE CHANGE_TRACKING
WITH (TRACK_COLUMNS_UPDATED = ON)

--确认是否更新跟踪开启
SELECT OBJECT_NAME(object_id) ObjNM,is_track_columns_updated_on
FROM sys.change_tracking_tables

/*
ObjNM is_track_columns_updated_on
DepartDemo 1
*/

--增加测试数据
INSERT dbo.DepartDemo
(DName,ParentID)
VALUES
('明教', 0),
('五行集', 101),
('少林派',0)

SELECT * FROM dbo.DepartDemo

--当前版本
SELECT CHANGE_TRACKING_CURRENT_VERSION ()
as 当前版本
/*
当前版本
1
*/
SELECT CHANGE_TRACKING_MIN_VALID_VERSION
( OBJECT_ID('dbo.DepartDemo') )as 最小可用版本

/*
最小可用版本
0
*/
复制代码

 

函数ChangeTable有两种用法来检测更改:
一、使用Changes关键字
二、使用Version关键字

复制代码
/*
一、使用Changes关键字
*/

SELECT DID,SYS_CHANGE_OPERATION,
SYS_CHANGE_VERSION
FROM CHANGETABLE
(CHANGES dbo.DepartDemo, 0) AS CT
复制代码

邀月工作室

复制代码
UPDATE dbo.DepartDemo
SET Manager='张无忌'
WHERE DID = 101

UPDATE dbo.DepartDemo
SET [DName] = '五行旗'
WHERE DID = 102

DELETE dbo.DepartDemo
WHERE DID = 103

SELECT CHANGE_TRACKING_CURRENT_VERSION () as 当前版本
/*
当前版本
4
*/

--版本1之后的更改
SELECT DID,
SYS_CHANGE_VERSION,
SYS_CHANGE_OPERATION,
SYS_CHANGE_COLUMNS
FROM CHANGETABLE
(CHANGES dbo.DepartDemo, 1) AS CT
复制代码

邀月工作室

复制代码
--返回哪些列被修改,1为真,0为假
SELECT DID,
CHANGE_TRACKING_IS_COLUMN_IN_MASK(
COLUMNPROPERTY(
OBJECT_ID('dbo.DepartDemo'),'DName', 'ColumnId') ,
SYS_CHANGE_COLUMNS) 是否改变DName,
CHANGE_TRACKING_IS_COLUMN_IN_MASK(
COLUMNPROPERTY(
OBJECT_ID('dbo.DepartDemo'), 'Manager', 'ColumnId') ,
SYS_CHANGE_COLUMNS) 是否改变Manager
FROM CHANGETABLE
(CHANGES dbo.DepartDemo, 1) AS CT
WHERE SYS_CHANGE_OPERATION = 'U'
/*
DID 是否改变DName 是否改变Manager
101 0 1
102 1 0
*/
复制代码

 

复制代码
/*
二、使用Version关键字
*/

SELECT d.DID, d.DName, d.Manager,
ct.SYS_CHANGE_VERSION
FROM dbo.DepartDemo d
CROSS APPLY CHANGETABLE
(VERSION dbo.DepartDemo , (DID), (d.DID)) as ct
复制代码

邀月工作室

复制代码
UPDATE dbo.DepartDemo
SET DName = '中原明教',
CurState = 0
WHERE DID = 101

SELECT d.DID, d.DName, d.Manager,
ct.SYS_CHANGE_VERSION
FROM dbo.DepartDemo d
CROSS APPLY CHANGETABLE
(VERSION dbo.DepartDemo , (DID), (d.DID)) as ct
复制代码

邀月工作室

复制代码
SELECT CHANGE_TRACKING_CURRENT_VERSION () as 当前版本
/*
当前版本
5
*/

--跟踪外部程序哪一部分引起的更改,这样好找出源头
DECLARE @context varbinary(128) = CAST('明教内讧引起分裂' as varbinary(128));
WITH CHANGE_TRACKING_CONTEXT (@context)
INSERT dbo.DepartDemo
(DName, Manager)
VALUES
('天鹰教', '殷天正')

--查询Context更改
SELECT DID,
SYS_CHANGE_OPERATION,
SYS_CHANGE_VERSION,
CAST(SYS_CHANGE_CONTEXT as varchar) ApplicationContext
FROM CHANGETABLE
(CHANGES dbo.DepartDemo, 5) AS CT
/*
DID SYS_CHANGE_OPERATION SYS_CHANGE_VERSION ApplicationContext
104 I 6 明教内讧引起分裂
*/
复制代码

 

邀月工作室

小结:

本文总结了SQL Server中记录数据变更的四个方法:触发器、Output子句、SQL Server 2008中新增的变更数据捕获(CDC)功能、同步更改跟踪。其中后两个是SQL Server 2008中新增的功能,在SQL Server 2012中更是与Always ON紧密集成。

1、不建议前两个。

2、CDC用以实现异步跟踪用户表的数据修改,而且这一功能拥有最小的性能开销,可以用于其他数据源的持续更新,例如将OLTP数据库中的数据变更迁移到数据仓库数据库。

3、”更改跟踪”的最大优势是以最小的磁盘开销来侦测净行变更,它允许修改的数据以事务一致的形式表现,并提供了检测数据冲突的能力。

 

其他推荐文章:

1、在VS中如何将数据同步配置为使用 SQL Server 更改跟踪

http://msdn.microsoft.com/zh-cn/library/cc714038.aspx

2、SQL Server 2012中复制、更改跟踪、更改数据捕获和 AlwaysOn 可用性组 (SQL Server)

http://msdn.microsoft.com/zh-cn/library/hh403414%28v=sql.110%29.aspx

3、在SSIS 2012中使用CDC(数据变更捕获)

http://www.cnblogs.com/downmoon/p/3700045.html

4、在SQL Server 2012中实现CDC for Oracle

http://www.cnblogs.com/downmoon/p/3702145.html

 

邀月注:本文版权由邀月和博客园共同所有,转载请注明出处。
助人等于自助!  3w@live.cn
SQL Server 2008中新增的变更数据捕获(CDC)和更改跟踪 – 邀月 – 博客园已关闭评论
2019年7月16日 By mikel 分类: C#

来源: 200行代码,7个对象——让你了解ASP.NET Core框架的本质 – Artech – 博客园

2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为《ASP.NET Core框架揭秘》的分享。在此次分享中,我按照ASP.NET Core自身的运行原理和设计思想创建了一个 “迷你版” 的ASP.NET Core框架,并且利用这个 “极简” 的模拟框架阐述了ASP.NET Core框架最核心、最本质的东西。整个框架涉及到的核心代码不会超过200行,涉及到7个核心的对象。

PPT下载
源代码下载

目录
1. 从Hello World谈起
2. ASP.NET Core Mini
3. Hello World 2
4. 第一个对象:HttpContext
5. 第二个对象:RequetDelegate
6. 第三个对象:Middleware
7. 第四个对象:ApplicationBuilder
8. 第五个对象:Server
9. HttpContext和Server之间的适配
10. HttpListenerServer
11. 第六个对象:WebHost
12. 第七个对象:WebHostBuilder
13. 回顾一下Hello World 2
14. 打个广告:《ASP.NET Core框架揭秘》

1、从Hello World谈起

当我们最开始学习一门技术的时候都喜欢从Hello World来时,貌似和我们本篇的主题不太搭。但事实却非如此,在我们看来如下这个Hello World是对ASP.NET Core框架本质最好的体现。

public class Program
{
    public static void Main()
    => new WebHostBuilder()
        .UseKestrel()
        .Configure(app => app.Run(context => context.Response.WriteAsync("Hello World!")))
        .Build()
        .Run();
}

如上这个Hello World程序虽然人为地划分为若干行,但是整个应用程序其实只有一个语句。这个语句涉及到了ASP.NET Core程序两个核心对象WebHostWebHostBuilder。我们可以将WebHost理解为寄宿或者承载Web应用的宿主,应用的启动可以通过启动作为宿主的WebHost来实现。至于WebHostBuilder,顾名思义,就是WebHost的构建者。

在调用WebHostBuilder的Build方法创建出WebHost之前,我们调用了它的两个方法,其中UseKestrel旨在注册一个名为Kestrel的服务器,而Configure方法的调用则是为了注册一个用来处理请求的中间件,后者在响应的主体内容中写入一个“Hello World”文本。

当我们调用Run方法启动作为应用宿主的WebHost的时候,后者会利用WebHostBuilder提供的服务器和中间件构建一个请求处理管道。这个由一个服务器和若干中间件构成的管道就是ASP.NET Core框架的核心,我们接下来的核心任务就是让大家搞清楚这个管道是如何被构建起来的,以及该管道采用怎样的请求处理流程。

clip_image002[6]

2、ASP.NET Core Mini

在过去这些年中,我不断地被问到同一个问题:如何深入地去一个开发框架。我知道每个人都具有适合自己的学习方式,而且我觉得我个人的学习方法也算不上高效,所以我很少会正面回应这个问题。不过有一个方法我倒很乐意与大家分享,那就是当你在学习一个开发框架的时候不要只关注编程层面的东西,而应该将更多的精力集中到对架构设计层面的学习。

针对某个框架来说,它提供的编程模式纷繁复杂,而底层的设计原理倒显得简单明了。那么如何检验我们对框架的设计原理是否透彻呢,我觉得最好的方式就是根据你的理解对框架进行“再造”。当你按照你的方式对框架进行“重建”的过程中,你会发现很多遗漏的东西。如果被你重建的框架能够支撑一个可以运行的Hello World应用,那么可以基本上证明你已经基本理解了这个框架最本质的东西。

虽然ASP.NET Core目前是一个开源的项目,我们可以完全通过源码来学习它,但是我相信这对于绝大部分人来说是有难度的。为此我们将ASP.NET Core最本质、最核心的部分提取出来,重新构建了一个迷你版的ASP.NET Core框架。

clip_image004[6]

ASP.NET Core Mini具有如上所示的三大特点。第一、它是对真实ASP.NET Core框架的真实模拟,所以在部分API的定义上我们做了最大限度的简化,但是两者的本质是完全一致的。如果你能理解ASP.NET Core Mini,意味着你也就是理解了真实ASP.NET Core框架。第二、这个框架是可执行的,我们提供的并不是伪代码。第三、为了让大家能够在最短的时间内理解ASP.NET Core框架的精髓,ASP.NET Core Mini必需足够简单,所以我们整个实现的核心代码不会超过200行。

3、Hello World 2

既然我们的ASP.NET Core Mini是可执行的,意味着我们可以在上面构建我们自己的应用,如下所示的就是在ASP.NET Core Mini上面开发的Hello World,可以看出它采用了与真实ASP.NET Core框架一致的编程模式。

public class Program
{
    public static async Task Main()
    {
        await new WebHostBuilder()
            .UseHttpListener()
            .Configure(app => app
                .Use(FooMiddleware)
                .Use(BarMiddleware)
                .Use(BazMiddleware))
            .Build()
            .StartAsync();
    }

    public static RequestDelegate FooMiddleware(RequestDelegate next)
    => async context => {
        await context.Response.WriteAsync("Foo=>");
        await next(context);
    };

    public static RequestDelegate BarMiddleware(RequestDelegate next)
    => async context => {
            await context.Response.WriteAsync("Bar=>");

            await next(context);
        };

    public static RequestDelegate BazMiddleware(RequestDelegate next)
    => context => context.Response.WriteAsync("Baz");
}

我们有必要对上面这个Hello World程序作一个简答的介绍:在创建出WebHostBuilder之后,我们调用了它的扩展方法UseHttpListener注册了一个自定义的基于HttpListener的服务器,我们会在后续内容中介绍该服务器的实现。在随后针对Configure方法的调用中,我们注册了三个中间件。由于中间件最终是通过Delegate对象来体现的,所以我们可以将中间件定义成与Delegate类型具有相同签名的方法。

我们目前可以先不用考虑表示中间件的三个方法为什么需要成如上的形式,只需要知道三个中间件在针对请求的处理流程中都作了些什么。上面的代码很清楚,三个中间件分别会在响应的内容中写入一段文字,所以程序运行后,如果我们利用浏览器访问该应用,会得到如下所示的输出结果。

clip_image006[6]

4、第一个对象:HttpContext

正如本篇文章表示所说,我们的ASP.NET Core Mini由7个核心对象构建而成。第一个就是大家非常熟悉的HttpContext对象,它可以说是ASP.NET Core应用开发中使用频率最高的对象。要说明HttpContext的本质,还得从请求处理管道的层面来讲。对于由一个服务器和多个中间件构建的管道来说,面向传输层的服务器负责请求的监听、接收和最终的响应,当它接收到客户端发送的请求后,需要将它分发给后续中间件进行处理。对于某个中间件来说,当我们完成了自身的请求处理任务之后,在大部分情况下也需要将请求分发给后续的中间件。请求在服务器与中间件之间,以及在中间件之间的分发是通过共享上下文的方式实现的。

clip_image008[6]

如上图所示,当服务器接收到请求之后,会创建一个通过HttpContext表示的上下文对象,所有中间件都是在这个上下文中处理请求的,那么一个HttpContext对象究竟携带怎样的上下文信息呢?我们知道一个HTTP事务(Transaction)具有非常清晰的界定,即接收请求、发送响应,所以请求响应是两个基本的要素,也是HttpContext承载的最核心的上下文信息。

我们可以将请求理解为输入、响应理解为输出,所以应用程序可以利用HttpContext得到当前请求所有的输入信息,也可以利用它完成我们所需的所有输出工作。为此我们为ASP.NET Core Mini定义了如下这个极简版本的HttpContext。

public class HttpContext
{           
    public  HttpRequest Request { get; }
    public  HttpResponse Response { get; }
}
public class HttpRequest
{
    public  Uri Url { get; }
    public  NameValueCollection Headers { get; }
    public  Stream Body { get; }
}
public class HttpResponse
{
    public  NameValueCollection Headers { get; }
    public  Stream Body { get; }
    public int StatusCode { get; set;}
}

如上面的代码片段所示,HttpContext通过它的两个属性Request和Response来表示请求和响应,它们对应的类型分别为HttpRequest和HttpResponse。通过前者,我们可以得到请求的地址、手部集合和主体内容,利用后者,我们可以设置响应状态码,也可以设置首部和主体内容。

5、第二个对象:RequestDelegate

RequestDelegate是我们介绍的第二个核心对象。我们从命名可以看出这是一个委托(Delegate)对象,和上面介绍的HttpContext一样,我们也只有从管道的角度才能充分理解这个委托对象的本质。

在从事软件行业10多年来,我对软件的架构设计越来越具有这样的认识:好的设计一定是“简单”的设计。所以每当我在设计某个开发框架的时候,一直会不断告诉我自己:“还能再简单点吗?”。我们上面介绍的ASP.NET Core管道的设计就具有“简单”的特质:Pipeline = Server + Middlewares。但是“还能再简单点吗?”,其实是可以的:我们可以将多个Middleware构建成一个单一的“HttpHandler”,那么整个ASP.NET Core框架将具有更加简单的表达:Pipeline =Server + HttpHandler

clip_image010[6]

那么我们如来表达HttpHandler呢?我们可以这样想:既然针对当前请求的所有输入和输出都通过HttpContext来表示,那么HttpHandler就可以表示成一个Action<HttpContext>对象。那么HttpHandler在ASP.NET Core中是通过Action<HttpContext>来表示的吗?其实不是的,原因很简单:Action<HttpContext>只能表示针对请求的 “同步” 处理操作,但是针对HTTP请求既可以是同步的,也可以是异步的,更多地其实是异步的。

那么在.NET Core的世界中如何来表示一个同步或者异步操作呢?你应该想得到,那就是Task对象,那么HttpHandler自然就可以表示为一个Func<HttpContext,Task>对象。由于这个委托对象实在太重要了,所以我们将它定义成一个独立的类型。

clip_image012[6]

6、第三个对象:Middleware

在对RequestDelegate这个委托对象具有充分认识之后,我们来聊聊中间件又如何表达,这也是我们介绍的第三个核心对象。中间件在ASP.NET Core被表示成一个Func<RequestDelegate, RequestDelegate>对象,也就是说它的输入和输出都是一个RequestDelegate

clip_image014[6]

对于为什么会采用一个Func<RequestDelegate, RequestDelegate>对象来表示中间件,很多初学者会很难理解。我们可以这样的考虑:对于管道的中的某一个中间件来说,由后续中间件组成的管道体现为一个RequestDelegate对象,由于当前中间件在完成了自身的请求处理任务之后,往往需要将请求分发给后续中间件进行处理,所有它它需要将由后续中间件构成的RequestDelegate作为输入

当代表中间件的委托对象执行之后,我们希望的是将当前中间件“纳入”这个管道,那么新的管道体现的RequestDelegate自然成为了输出结果。所以中间件自然就表示成输入和输出均为RequestDelegate的Func<RequestDelegate, RequestDelegate>对象。

7、第四个对象:ApplicationBuilder

ApplicationBuilder是我们认识的第四个核心对象。从命名来看,这是我们接触到的第二个Builder,既然它被命名为ApplicationBuilder,意味着由它构建的就是一个Application。那么在ASP.NET Core框架的语义下应用(Application)又具有怎样的表达呢?

对于这个问题,我们可以这样来理解:既然Pipeline = Server + HttpHandler,那么用来处理请求的HttpHandler不就承载了当前应用的所有职责吗?那么HttpHandler就等于Application,由于HttpHandler通过RequestDelegate表示,那么由ApplicationBuilder构建的Application就是一个RequestDelegate对象。

clip_image016[6]

由于表示HttpHandler的RequestDelegate是由注册的中间件来构建的,所以ApplicationBuilder还具有注册中间件的功能。基于ApplicationBuilder具有的这两个基本职责,我们可以将对应的接口定义成如下的形式。Use方法用来注册提供的中间件,Build方法则将注册的中间件构建成一个RequestDelegate对象。

public interface  IApplicationBuilder
{
    IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
    RequestDelegate Build();
}

如下所示的是针对该接口的具体实现。我们利用一个列表来保存注册的中间件,所以Use方法只需要将提供的中间件添加到这个列表中即可。当Build方法被调用之后,我们只需按照与注册相反的顺序依次执行表示中间件的Func<RequestDelegate, RequestDelegate>对象就能最终构建出代表HttpHandler的RequestDelegate对象。

public class ApplicationBuilder : IApplicationBuilder
{
    private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares = new List<Func<RequestDelegate, RequestDelegate>>();
    public RequestDelegate Build()
    {
        _middlewares.Reverse();
        return httpContext =>
        {
            RequestDelegate next = _ => { _.Response.StatusCode = 404; return Task.CompletedTask; };
            foreach (var middleware in _middlewares)
            {
                next = middleware(next);
            }
            return next(httpContext);
        };
    }

    public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
    {
        _middlewares.Add(middleware);
        return this;
    }
}

在调用第一个中间件(最后注册)的时候,我们创建了一个RequestDelegate作为输入,后者会将响应状态码设置为404。所以如果ASP.NET Core应用在没有注册任何中间的情况下总是会返回一个404的响应。如果所有的中间件在完成了自身的请求处理任务之后都选择将请求向后分发,同样会返回一个404响应。

8、第五个对象:Server

服务器在管道中的职责非常明确,当我们自动作应用宿主的WebHost的时候,服务它被自动启动。启动后的服务器会绑定到指定的端口进行请求监听,一旦有请求抵达,服务器会根据该请求创建出代表上下文的HttpContext对象,并将该上下文作为输入调用由所有注册中间件构建而成的RequestDelegate对象。

clip_image018[6]

简单起见,我们使用如下这个简写的IServer接口来表示服务器。我们通过定义在IServer接口的唯一方法StartAsync启动服务器,作为参数的handler正是由所有注册中间件共同构建而成的RequestDelegate对象

public interface IServer
{ 
    Task StartAsync(RequestDelegate handler);
}

9、HttpContext和Server之间的适配

面向应用层的HttpContext对象是对请求和响应的封装,但是请求最初来源于服务器,针对HttpContext的任何响应操作也必需作用于当前的服务器才能真正起作用。现在问题来了,所有的ASP.NET Core应用使用的都是同一个HttpContext类型,但是却可以注册不同类型的服务器,我们必需解决两者之间的适配问题。

clip_image020[6]

计算机领域有一句非常经典的话:“任何问题都可以通过添加一个抽象层的方式来解决,如果解决不了,那就再加一层”。同一个HttpContext类型与不同服务器类型之间的适配问题也可可以通过添加一个抽象层来解决,我们定义在该层的对象称为Feature。如上图所示,我们可以定义一系列的Feature接口来为HttpContext提供上下文信息,其中最重要的就是提供请求的IRequestFeature和完成响应的IResponseFeature接口。那么具体的服务器只需要实现这些Feature接口就可以了。

clip_image022[6]

我们接着从代码层面来看看具体的实现。如下面的代码片段所示,我们定义了一个IFeatureCollection接口来表示存放Feature对象的集合。从定义可以看出这是一个以Type和Object作为Key和Value的字典,Key代表注册Feature所采用的类型,而Value自然就代表Feature对象本身,话句话说我们提供的Feature对象最终是以对应Feature类型(一般为接口类型)进行注册的。为了编程上便利,我们定义了两个扩展方法Set<T>和Get<T>来设置和获取Feature对象。

public interface IFeatureCollection : IDictionary<Type, object> { }
public class FeatureCollection : Dictionary<Type, object>, IFeatureCollection { }   
public static partial class Extensions
{
    public static T Get<T>(this IFeatureCollection features) => features.TryGetValue(typeof(T), out var value) ? (T)value : default(T);
    public static IFeatureCollection Set<T>(this IFeatureCollection features, T feature)
    { 
        features[typeof(T)] = feature;
        return features;
    }
}

如下所示的用来提供请求和响应IHttpRequestFeature和IHttpResponseFeature接口的定义,可以看出它们具有与HttpRequest和HttpResponse完全一致的成员定义。

public interface IHttpRequestFeature
{
    Uri                     Url { get; }
    NameValueCollection     Headers { get; }
    Stream                  Body { get; }
}    
public interface IHttpResponseFeature
{
    int                       StatusCode { get; set; }
    NameValueCollection     Headers { get; }
    Stream                  Body { get; }
}

接下来我们来看看HttpContext的具体实现。ASP.NET Core Mini的HttpContext只包含Request和Response两个属性成员,对应的类型分别为HttpRequest和HttpResponse,如下所示的就是这两个类型的具体实现。我们可以看出HttpRequest和HttpResponse都是通过一个IFeatureCollection对象构建而成的,它们对应的属性成员均有分别由包含在这个Feature集合中的IHttpRequestFeature和IHttpResponseFeature对象来提供的。

public class HttpRequest
{
    private readonly IHttpRequestFeature _feature;    
      
    public  Uri Url => _feature.Url;
    public  NameValueCollection Headers => _feature.Headers;
    public  Stream Body => _feature.Body;

    public HttpRequest(IFeatureCollection features) => _feature = features.Get<IHttpRequestFeature>();
}

public class HttpResponse
{
    private readonly IHttpResponseFeature _feature;

    public  NameValueCollection Headers => _feature.Headers;
    public  Stream Body => _feature.Body;
    public int StatusCode { get => _feature.StatusCode; set => _feature.StatusCode = value; }

    public HttpResponse(IFeatureCollection features) => _feature = features.Get<IHttpResponseFeature>();

}

HttpContext的实现就更加简单了。如下面的代码片段所示,我们在创建一个HttpContext对象是同样会提供一个IFeatureCollection对象,我们利用该对象创建对应的HttpRequest和HttpResponse对象,并作为对应的属性值。

public class HttpContext
{           
    public  HttpRequest Request { get; }
    public  HttpResponse Response { get; }

    public HttpContext(IFeatureCollection features)
    {
        Request = new HttpRequest(features);
        Response = new HttpResponse(features);
    }
}

10、HttpListenerServer

在对服务器和它与HttpContext的适配原理具有清晰的认识之后,我们来尝试着自己定义一个服务器。在前面的Hello World实例中,我们利用WebHostBuilder的扩展方法UseHttpListener注册了一个HttpListenerServer,我们现在就来看看这个采用HttpListener作为监听器的服务器类型是如何实现的。

由于所有的服务器都需要自动自己的Feature实现来为HttpContext提供对应的上下文信息,所以我们得先来为HttpListenerServer定义相应的接口。对HttpListener稍微了解的朋友应该知道它在接收到请求之后同行会创建一个自己的上下文对象,对应的类型为HttpListenerContext。如果采用HttpListenerServer作为应用的服务器,意味着HttpContext承载的上下文信息最初来源于这个HttpListenerContext,所以Feature的目的旨在解决这两个上下文之间的适配问题。

clip_image024[6]

如下所示的HttpListenerFeature就是我们为HttpListenerServer定义的Feature。HttpListenerFeature同时实现了IHttpRequestFeature和IHttpResponseFeature,实现的6个属性成员最初都来源于创建该Feature对象提供的HttpListenerContext对象。

public class HttpListenerFeature : IHttpRequestFeature, IHttpResponseFeature
{
    private readonly HttpListenerContext _context;
    public HttpListenerFeature(HttpListenerContext context) => _context = context;

    Uri IHttpRequestFeature.Url => _context.Request.Url;
    NameValueCollection IHttpRequestFeature.Headers => _context.Request.Headers;
    NameValueCollection IHttpResponseFeature.Headers => _context.Response.Headers;
    Stream IHttpRequestFeature.Body => _context.Request.InputStream;
    Stream IHttpResponseFeature.Body => _context.Response.OutputStream;
    int IHttpResponseFeature.StatusCode { get => _context.Response.StatusCode; set => _context.Response.StatusCode = value; }
}

如下所示的是HttpListenerServer的最终定义。我们在构造一个HttpListenerServer对象的时候可以提供一组监听地址,如果没有提供,会采用“localhost:5000”作为默认的监听地址。在实现的StartAsync方法中,我们启动了在构造函数中创建的HttpListenerServer对象,并在一个循环中通过调用其GetContextAsync方法实现了针对请求的监听和接收。

public class HttpListenerServer : IServer
{
    private readonly HttpListener     _httpListener;
    private readonly string[]             _urls;

    public HttpListenerServer(params string[] urls)
    {
        _httpListener = new HttpListener();
        _urls = urls.Any()?urls: new string[] { "http://localhost:5000/"};
    }

    public async Task StartAsync(RequestDelegate handler)
    {
        Array.ForEach(_urls, url => _httpListener.Prefixes.Add(url));    
        _httpListener.Start();
        while (true)
        {
            var listenerContext = await _httpListener.GetContextAsync(); 
            var feature = new HttpListenerFeature(listenerContext);
            var features = new FeatureCollection()
                .Set<IHttpRequestFeature>(feature)
                .Set<IHttpResponseFeature>(feature);
            var httpContext = new HttpContext(features);
            await handler(httpContext);
            listenerContext.Response.Close();
        }
    }
}

当HttpListener监听到抵达的请求后,我们会得到一个HttpListenerContext对象,此时我们只需要据此创建一个HttpListenerFeature对象并它分别以IHttpRequestFeature和IHttpResponseFeature接口类型注册到创建FeatureCollection集合上。我们最终利用这个FeatureCollection对象创建出代表上下文的HttpContext,然后将它作为参数调用由所有中间件共同构建的RequestDelegate对象即可。

11、第六个对象:WebHost

到目前为止我们已经知道了由一个服务器和多个中间件构成的管道是如何完整针对请求的监听、接收、处理和最终响应的,接下来来讨论这样的管道是如何被构建出来的。管道是在作为应用宿主的WebHost对象启动的时候被构建出来的,在ASP.NET Core Mini中,我们将表示应用宿主的IWebHost接口简写成如下的形式:只包含一个StartAsync方法用来启动应用程序。

public interface IWebHost
{
    Task StartAsync();
}

由于由WebHost构建的管道由Server和HttpHandler构成,我们在默认实现的WebHost类型中,我们直接提供者两个对象。在实现的StartAsync方法中,我么只需要将后者作为参数调用前者的StartAsync方法将服务器启动就可以了。

public class WebHost : IWebHost
{
    private readonly IServer _server;
    private readonly RequestDelegate _handler; 
    public WebHost(IServer server, RequestDelegate handler)
    {
        _server = server;
        _handler = handler;
    } 
    public Task StartAsync() => _server.StartAsync(_handler);
}

12、第七个对象:WebHostBuilder

作为最后一个着重介绍的核心对象,WebHostBuilder的使命非常明确:就是创建作为应用宿主的WebHost。由于在创建WebHost的时候需要提供注册的服务器和由所有注册中间件构建而成的RequestDelegate,所以在对应接口IWebHostBuilder中,我们为它定义了三个核心方法。

public interface IWebHostBuilder
{
    IWebHostBuilder UseServer(IServer server);
    IWebHostBuilder Configure(Action<IApplicationBuilder> configure);
    IWebHost Build();
}

除了用来创建WebHost的Build方法之外,我们提供了用来注册服务器的UseServer方法和用来注册中间件的Configure方法。Configure方法提供了一个类型为Action<IApplicationBuilder>的参数,意味着我们针对中间件的注册是利用上面介绍的IApplicationBuilder对象来完成的。

如下所示的WebHostBuilder是针对IWebHostBuilder接口的默认实现,它具有两个字段分别用来保存注册的中间件和调用Configure方法提供的Action<IApplicationBuilder>对象。当Build方法被调用之后,我们创建一个ApplicationBuilder对象,并将它作为参数调用这些Action<IApplicationBuilder>委托,进而将所有中间件全部注册到这个ApplicationBuilder对象上。我们最终调用它的Build方法得到由所有中间件共同构建的RequestDelegate对象,并利用它和注册的服务器构建作为应用宿主的WebHost对象。

public class WebHostBuilder : IWebHostBuilder
{
    private IServer _server;
    private readonly List<Action<IApplicationBuilder>> _configures = new List<Action<IApplicationBuilder>>();   

    public IWebHostBuilder Configure(Action<IApplicationBuilder> configure)
    {
        _configures.Add(configure);
        return this;
    }
    public IWebHostBuilder UseServer(IServer server)
    {
        _server = server;
        return this;
    }   

    public IWebHost Build()
    {
        var builder = new ApplicationBuilder();
        foreach (var configure in _configures)
        {
            configure(builder);
        }
        return new WebHost(_server, builder.Build());
    }
}

13、回顾一下Hello World 2

到目前为止,我们已经将ASP.NET Core Mini涉及的七个核心对象介绍完了,然后我们再来回顾一下建立在这个模拟框架上的Hello World程序。

public class Program
{
    public static async Task Main()
    {
        await new WebHostBuilder()
            .UseHttpListener()
            .Configure(app => app
                .Use(FooMiddleware)
                .Use(BarMiddleware)
                .Use(BazMiddleware))
            .Build()
            .StartAsync();
    }

    public static RequestDelegate FooMiddleware(RequestDelegate next)
    => async context => {
        await context.Response.WriteAsync("Foo=>");
        await next(context);
    };

    public static RequestDelegate BarMiddleware(RequestDelegate next)
    => async context => {
            await context.Response.WriteAsync("Bar=>");

            await next(context);
        };

    public static RequestDelegate BazMiddleware(RequestDelegate next)
    => context => context.Response.WriteAsync("Baz");
}

首选我们调用WebHostBuilder的扩展方法UseHttpListener采用如下的方式完成了针对HttpListenerServer的注册。由于中间件体现为一个Func<RequestDelegate, RequestDelegate>对象,我们自然可以采用与之具有相同声明的方法(FooMiddleware、BarMiddleware和BazMiddleware)来定义对应的中间件。中间件调用HttpResponse的WriteAsync以如下的方式将指定的字符串写入响应主体的输出流。

public static partial class Extensions
{
   public static IWebHostBuilder UseHttpListener(this IWebHostBuilder builder, params string[] urls)
    => builder.UseServer(new HttpListenerServer(urls));

    public static Task WriteAsync(this HttpResponse response, string contents)
    {
        var buffer = Encoding.UTF8.GetBytes(contents);
        return response.Body.WriteAsync(buffer, 0, buffer.Length);
     }
}

14、打个广告:《ASP.NET Core框架揭秘》

ASP.NET Core Mini模拟了真实ASP.NET Core框架最核心的部分,即由服务器和中间件构成的请求处理管道。真正的ASP.NET Core框架自然要复杂得多得多,那么我们究竟遗漏了什么呢?

clip_image026[6]

如上所示的5个部分是ASP.NET Core Mini没有涉及的,其中包括依赖注入、以Startup和StartupFilter的中间件注册方式、针对多种数据源的配置系统、诊断日志系统和一系列预定义的中间件,上述的每个方面都涉及到一个庞大的主题,我们将ASP.NET Core涉及到的方方面都写在我将要出版的《ASP.NET Core框架揭秘》中,如果你想全方面了解一个真实的ASP.NET Core框架,敬请期待新书出版。

image

200行代码,7个对象——让你了解ASP.NET Core框架的本质 – Artech – 博客园已关闭评论
2019年7月16日 By mikel 分类: 数据库

SQLServer无法作为数据库主体执行,因为主体 “dbo” 不存在… 解决方案_魔帥ぁ拽拽_新浪博客,魔帥ぁ拽拽,

来源: sqlserver无法作为数据库主体执行,因为主体 “dbo” 不存在… 解决方案_魔帥ぁ拽拽_新浪博客

USE 出问题的库名; 
EXEC sp_changedbowner ‘sa’;

为用户添加数据库角色时提示已经存在此帐号请查看数据库下是否已经存在该帐号并删除重新授权
sqlserver无法作为数据库主体执行,因为主体 “dbo” 不存在… 解决方案_魔帥ぁ拽拽_新浪博客已关闭评论
2019年7月16日 By mikel 分类: C#

来源: 与VS集成的若干种代码生成解决方案[博文汇总(共8篇)] – Artech – 博客园

[第1篇] 通过CodeDOM定义生成代码的结构

我不知道大家对CodeDOM的代码生成机制是否熟悉,但是有一点可以确定:如果你使用过Visual Studio,你就应该体验过它带给我们在编程上的便利。随便列举三种典型的代码生成的场景:在创建强类型DataSet的时候,VS会自动根据Schema生成相应的C#或者VB.NET代码;当我们编辑Resource文件的时候,相应的的后台代码也会自动生成;当我们通过添加Web Reference调用Web Service或者WCF Service的时候,VS会自动生成服务代理的代码和相应的配置。总的来说,通过和VS集成的动态代码生成工具使我们可以“强类型”的方式进行编程,进而提供我们的效率并减低错误的几率。

CodeDOM 提供了表示许多常见的源代码元素类型的类型。您可以设计一个生成源代码模型的程序,使用CodeDOM 元素构成一个对象图。而这个对象图包含C#或者VB.NET代码包含的基本元素:命名空间、类型、类型成员(方法、属性、构造函数、事件等),并且包括方法实现的具体语句(Statement)。也就是说它的结构就是对一个具体.vb或者.cs文件代码的反映。在这里我不会具体介绍CodeDOM体系结构……[阅读全文]

[第2篇] 通过Visual Studio的Custom Tool定义代码生成器

在《前篇》中我们已经通过CodeDOM的形式定义了将要生成的代码结构,而这个CodeDOM对象就是CodeCompileUnitCodeCompileUnit本身是与编程语言无关的,我们可以利用基于某种编程语言(VB.NET或者C#)的CodeDomProviderCodeCompileUnit对象转换成具体的代码文本。为了让我们的VS自动地为我们生成代码,我们需要以Custom Tool的形式编写相应的代码生成器。我们编写的代码生成器最终通过COM组件的形式对外提供服务,所以其中涉及到COM组件的注册。为了实现同VS的集成,还涉及到相应注册表设置……[阅读全文]

[第3篇] 不同于CodeDOM的代码生成机制——T4

前面两篇介绍了如何通过CodeDOM+Custom Tool的代码生成方式实现了代码的自动生成。实际上,我们最常用的代码生成当时不是CodeDOM,而是T4,这是一个更为强大,并且适用范围更广的代码生成技术。T4是对“Text Template Transformation Toolkit”(4个T)的简称。T4直接包含在VS2008和VS2010中,是一个基于文本文件转换的工具包。T4的核心是一个基于“文本模板”的转换引擎,我们可以通过它生成一切类型的文本型文件,比如我们常用的代码文件类型包括:C#、VB.NET、T-SQL、XML甚至是配置文件等。

对于需要通过T4来进行代码生成工作的我们来说,需要做的仅仅是根据转换源(Transformation Source),比如数据表、XML等(由于例子简单,HelloWord模板没有输入源)和目标文本(比如最终需要的C#或者T-SQL代码等)定义相应的模板。T4模板作用就相当于进行XML转化过程中使用的XSLT。

T4模板的定义非常简单,整个模板的内容包括两种形式:静态形式和动态动态。前者就是直接写在模板中作为原样输出的文本,后者是基于某种语言编写代码,T4引擎会动态执行它们。这和我们通过内联的方式编写的ASP.NET页面很相似:HTML是静态的,以C#或者VB.NET代码便写的动态执行的代码通过相应的标签内嵌其中。[阅读全文]

[第4篇] 通过T4模板实现单文件的代码生成

在《前一篇》中,我对T4模板的组成结构、语法,以及T4引擎的工作原理进行了大体的介绍,并且编写了一个T4模板实现了如何将一个XML转变成C#代码。为了让由此需求的读者对T4有更深的了解,我们通过T4来做一些更加实际的事情——SQL Generator。在这里,我们可以通过SQL Generator为某个数据表自动生成进行插入、修改和删除的存储过程……[阅读全文]

[第5篇] 通过T4模板实现多文件的代码生成

在《前一篇》中我们通过T4模板为我们指定的数据表成功生成了我们需要的用于添加、修改和删除操作的存储过程。但是这是一种基于单个文件的解决方案,即我们必须为每一个生成的存储过程建立一个模板。如果我们提供一种基于多文件的代码生成方式,将会为编程人员带来极大的便利。借助于T4 ToolBox这个开源工具箱,多文件的SQL Generator的实现变得异常简单。[阅读全文]

[第6篇] 解决T4模板的程序集引用的五种方案

如果你的T4模板需要调用一个自定义的类型,并且该类型定义在某个非系统程序集中,你就需要通过T4<#@ assembly…#>指令引用该程序集。在VS 2008环境下,你只需要为当前项目添加相应的程序集引用,并且通过<@ assembly…#>指令的name属性指定程序集(Dll)的文件名即可。但是这种方式对于VS 2010则行不通,因为T4引擎在解析和编译模板的时候并不会从项目引用列表中去定位目标程序集。本篇文章为你介绍在VS2010下5种不同的程序集引用的方式……[ 阅读全文]

[第7篇] 编写T4模板进行代码生成无法避免的两个话题:”Assembly Locking”&”Debug”

出于提高性能考虑,T4引擎在进行基于代码生成的模板转换(Template Transformation)的时候,会始终重用同一个AppDomain。由于该AppDomain不会自动卸载,这就会导致该AppDomain始终锁定所有被它加载的程序集。如果我们需要释放程序集,我们不得不重启VS。但是,对于T4模板的开发调试阶段,这种通过重新启动VS的方式去释放程序集以确保我们的项目能够成功编译是不能接受的。那么,是否有一种解决方案既能够确保T4引擎能够进行正常的模板转换,又能避免它强行锁定引用程序集呢?

VS和一些T4编辑器虽然给了基本的智能感知支持,但是在绝大部分我们相当于在编写纯文本的脚本,所以对于一些比较复杂的模板转换逻辑,我们需要通过Debug的方式去发现一些无法避免的问题……[阅读全文]

[第8篇] 通过自定义BuildProvider为ASP.NET提供代码生成

对于ASP.NET应用的开发者来说,你可能不知道什么是BuildProvider,但是你几乎无时无刻不在使用它所带来的代码生成机制。当你创建一个.aspx文件的时候,为什么会自动创建对应源代码?当你在该.aspx页面中以XML的方式添加一个按钮,源代码中为什么会自动添加一个同名的属性。实际上,ASP.NET就是通过一个特殊的BuildProvider实现了将.aspx文件内容转换成相应的源代码,这个特殊的.aspx文件就是:PageBuildProvider。基于不同的文件类型,ASP.NET会采用不同的BuildProvider进行源代码的生成。比如UserControlBuildProvider和MasterPageBuildProvider分别实现了基于用户控件文件(.ascx)和母板页(.master)的源代码生成……[阅读全文]

与VS集成的若干种代码生成解决方案[博文汇总(共8篇)] – Artech – 博客园已关闭评论
2019年7月16日 By mikel 分类: C#, 数据库

来源: 一个完整的用于追踪数据改变的解决方案 – Artech – 博客园

在之前一篇介绍CDC的文章中,我说Audit Trail(或者Audit Log)是大部分企业级应用不可以或缺的功能。本篇给你一个完整的Audit Trail解决方案,不仅可以记录每一笔业务操作的信息(比如操作时间、操作者等),并且可以追踪每一笔业务引起的说有数据的改变(如果需要)。

目录
一、数据表的设计
二、数据变化的表示
三、AuditLog基本信息的写入
四、通过SQLCDC追踪源表数据变化
五、删除操作的TransactionId如何被记录?
六、通过SQL Job转储AuditLog详细信息
七、代码生成的应用

一、数据表的设计

clip_image001在数据库中,我们通过如右图所示的具有主子关系的两个表存储AuditLog相关信息。我们将“事务”作为我们进行追踪的单位,不过这里的讲的“事务”更多地指业务处理事务的概念。每一个被追踪的事务在AuditLog表具有一条匹配的记录,该记录表示该事务的基本信息:UserName(操作者)、AuditTime(操作时间)、Activity(可以看成是对事物的命名)和Description(事务补充性的描述)。主键TransactionId唯一标识一个事务。

子表AuditLogData记录事务详细的信息,即事务所引起的数据变化。一个完整的业务逻辑往往涉及到对多个数据表、多条记录的操作。而AuditLogData每一条记录表示某个事务针对某个单一数据表所带来的数据变化,而SourceTable字段表示源表的名称。而DataChange字段以XML的形式表示数据的改变,它具有如下的格式。

二、数据变化的表示

数据操作类型无外乎添加、更新和删除,我们通过不同的XML结构表示不同操作引起的数据改变。具体来说,对于添加操作,我们需要记录下插入的记录;对于删除操作,需要记录下原来的记录;而对于数据更新,则需要同时记录下更新先后的记录。

举个例子,假设我们具有一个Users表,它具有三个基本字段:Id、Name和Birthday。下面的XML分别表示添加、删除和更新操作后我们需要记录下的数据变化。

添加:

1: <?xml version="1.0" encoding="utf-8" ?>
2: <cdc operation="insert">
3:   <current>
4:     <Id type="VARCHAR(50)">001</Id>
5:     <Name type="NVARCHAR(50)">Foo</Name>
6:     <BirthDay type="DATE">1981-08-24</BirthDay>
7:   </current>
8: </cdc>

删除:

1: <?xml version="1.0" encoding="utf-8" ?>
2: <cdc operation="delete">
3:   <original>
4:     <Id type="VARCHAR(50)">001</Id>
5:     <Name type="NVARCHAR(50)">Foo</Name>
6:     <BirthDay type="DATE">1981-08-24</BirthDay>
7:   </original>
8: </cdc>

更新:

1: <?xml version="1.0" encoding="utf-8" ?>
2: <cdc operation="update">
3:   <original>
4:     <Id type="VARCHAR(50)">001</Id>
5:     <Name type="NVARCHAR(50)">Foo</Name>
6:     <BirthDay type="DATE">1981-08-24</BirthDay>
7:   </original>
8:   <current>
9:     <Id type="VARCHAR(50)">001</Id>
10:     <Name type="NVARCHAR(50)">Bar</Name>
11:     <BirthDay type="DATE">1982-07-10</BirthDay>
12:   </current>
13: </cdc>

当然,你也可以根据需要自定义XML的结构。

三、AuditLog基本信息的写入

我们现在我们的目标就是如何将追踪到的基于一个事务相关的信息写入到上面我们创建的两个表中。主表AuditLog的信息是很容易被写入的,比如你可以定义像下面一样的一个AuditLogger类。

   1: public class AuditLogger
   2: {
   3:     public void Write(string activity)
   4:     { }
   5:
   6:     public void Write(string activity, string description)
   7:     { }
   8: }

AuditLogger的Write方法进行传入了Activity和Description,而没有TransactionId、UserName和AuditTime。其中AuditTime自然是当前时间,而UserName应该是登录系统的用户。而对于TransactionId,我们应该采用上下文的方式来获取,具体原因会在下面谈到。如果你直接使用System.Transactions事务实现我们进行追踪的“事务”,你可以直接使用当前事务(Transaction.Current)的DistributedIdentifier或者LocalIdentifier。

   1: var transactionId = Transaction.Current.TransactionInformation.DistributedIdentifier;
   2: //Or
   3: var transactionId = Transaction.Current.TransactionInformation.LocalIdentifier;

基于AuditLog表的事务基本信息的日志好解决,那么我们如何将事务引起的事务变化记录到AuditLogData表中呢?这样的工作我们完全实现在SQL Server中

四、通过SQLCDC追踪源表数据变化

追踪记录每笔业务操作数据改变的利器——SQLCDC》介绍了一种有效记录基于某个数据表数据变化的方式:SQLCDC,在这里我们直接利用它来记录AuditLog的详细信息。当我们为某个表(比如Users)开启了CDC特性之后,SQL Server会为之创建一个相应的CT表(Users_CT),在默认的情况下Users_CT包含与Users表的所有字段。如果你不希望CDC追踪所有的字段,你可以显式地设定具体的字段。

AuditLogData表中有一个字段TransactionId表示记录属于哪个具体的事务,为了让CDC可以记录下正确TransactionId,需要在每一个被追踪的表中添加这么一个额外的字段。这个应该不是什么问题,比如我们的每个表中都具有6个系统字段:TransactionId、VersionNo、CreatedBy、CreatedTime、LatestUpdatedBy和LatestUpdatedTime。

由于每个数据表都具有了一个TransactionId字段,那么在进行数据提交的时候,需要将当前事务的ID为之赋值,这就是为什么我推荐采用上下文的方式来获取当前TransactionId的原因。但是,还有一个问题没有解决——数据删除操作的TransactionId如何被记录下来呢?

五、删除操作的TransactionId如何被记录?

由于代表当前事务的TransactionId最终会通过Insert或者Update SQL语句写入数据表,但是对于删除操作呢?由于我们直接调用Delete语句将相应的数据操作,表示当前删除操作所在的事务是无法被写入的,最终CDC记录下来的数据是无法反映出删除的记录隶属于哪个事务。

由于最终对数据库操作都是通过SQL提交的,或者是存储过程,或者是SQL文本。为了解决这个问题,我们只需要改变我们的SQL脚本,在Delete执行之前执行Update语句写入新的TransactionId

也就是说,对于一个删除操作,实际上是先做Update,最后做Delete。在这种情况下,CDC会为你记录下三条记录,前两条是为Update记录的,最后一条是为Delete记录的。为了区分CDC追踪的记录是正常的Update还是为了Delete而进行的Update,我们可以做一些标记。比如你可以在TransactionId的值之前添加一个前缀,表示Update操作是为Delete而作的。

六、通过SQL Job转储AuditLog详细信息

clip_image002CDC仅仅会将基于某个表的数据改变记录到基于该表的CT表中,最终我们需要将这些CT表中的数据转存到我们指定的AuditLogData表中,这个工作可以通过SQLJob来实现。你自行创建一个SQL Job实现从若干CT表到AuditLogData的数据转存,并根据你的需要(主要是实时性的需要)配制Job执行的时间或者间隔。右图揭示了AuditLog详细信息是如何一步步地被记录的。

七、代码生成的应用

在这个解决方案中,我们需要一个不可或缺的东西:代码生成器。它用于自动生成如下的SQL脚本:为某个表开启CDC特性并指定追踪字段的T-SQL脚本,和进行AuditLog详细信息转存(丛CT表到AuditLogData表)的SQL Job脚本。关于代码生成,可以参考《与VS集成的若干种代码生成解决方案

一个完整的用于追踪数据改变的解决方案 – Artech – 博客园已关闭评论
2019年7月16日 By mikel 分类: C#, 数据库

来源: 追踪记录每笔业务操作数据改变的利器——SQLCDC – Artech – 博客园

对于大部分企业应用来用,有一个基本的功能必不可少,那就是Audit Trail或者Audit Log,中文翻译为追踪检查、审核检查或者审核记录。我们采用Audit Trail记录每一笔业务操作的基本信息,比如操作的基本描述、操作时间、操作者等。对于一些安全级别比较高的应用,或者操作一些比较敏感的数据,我们甚至需要记录该笔业务操作引起的数据的改变。具体来说,这里的“数据改变”指的是每一条影响的记录在操作执行前后的变化。对于添加的记录,需要记录下新插入的记录;对于删除的记录,需要记录下原来的记录;对于更新的记录,则需要同时记录下更新前后的记录。

说到这里,很多人都会想到采用触发器的方式来实现对数据改变的捕捉。但是这种实现方案具有一个最大的局限:由于触发器是在数据操作所在事务范围内执行的,所有会带来性能的问题,严重的话还会因为触发器的执行导致事务超市。所以在这里,我们介绍一种更好的解决方案:SQLCDC

目录
一、SQLCDC简介
二、在数据库级别开启CDC
三、为某个数据表开启CDC
四、记录添加记录的数据改变
五、记录删除数据的数据改变
六、记录更新记录的数据改变

一、SQLCDC简介

CDC的全名为Change Data Capture,顾名思义,就是用于追踪和捕捉数据改变。CDC是在SQL Server 2008中才出现的新特性,而这个特性则在很早之前就出现在了Oracle中。对于SQL Server之前版本来说,在没有CDC的情况下,如果需要记录基于某个数据表的数据改变,我们只能采用触发器,具体来说就是通过手工创建After Insert、After Update和After Delete触发器去记录变化的数据。而CDC给了我们一种更为方便、易用和省心的方式去记录某个数据表的历史操作。

二、在数据库级别开启CDC

在默认的情况下,数据库的CDC特性是被关闭的,你可以通过系统表sys.databasesis_cdc_enabled字段确定某个数据库的CDC是否开启。如果在默认的情况下,我执行如下的SQL语句查看数据库TestDb的CDC是否开启,你将会看到该字段的值为0。image

你可以通过执行系统存储过程sys.sp_cdc_enable_db为当前数据库开启CDC特性。下面的T-SQL代码片断中,我们通过执行该存储过程为TestDb打开了CDC特性。

Use TestDb
Go
Exec sys.sp_cdc_enable_db
Go

 

三、为某个数据表开启CDC

由于CDC用于记录基于某个数据表的数据改变,所以在当前数据库CDC开启的情况下,你还需要显式地为某个数据表开启CDC特性。作为演示,我们通过如下T-SQL在TestDb下创建了一个简单的Users表,它仅仅具有三个字段:Id、Name和Birthday。

CREATE TABLE [dbo].[Users](
    [Id] [varchar](50) PRIMARY KEY,
    [Name] [nvarchar](50) NOT NULL,
    [Birthday] [date] NOT NULL)

数据表的CDC特性的开启通过执行sys.sp_cdc_enable_table存储过程实现。调用该存储过程的最简的方式就是指定数据表的Schema、名称和用于提取改变数据必须具有的权限(角色)。我通过执行下面的T-SQL将我们创建的Users表的CDC特性打开,其中@role_name参数被设置成NULL,表明我不对读取改变数据操作进行授权。sys.sp_cdc_enable_table具有很多参数,至于相应参数所影响的CDC行为,可以参考SQL Server 2008在线文档。

Use TestDb
Go
Exec sys.sp_cdc_enable_table 'dbo', 'Users', @role_name = NULL
Go

需要注意的是,CDC实际上建立在SQL Server Agent之上的,所以在执行上述T-SQL之前需要启动SQL Server Agent。当某个数据表的CDC特性被开启之后,系统会为创建一个用于保存数据变化的追踪表(Tracking Table)。该表的Schema为cdc,命名方式为被追踪表的表名后加“CT”后缀。执行上面一段T-SQL之后,会有如下一个系统表被创建出来,我们发现Users表的三个字段也在该表中。此外。该表还具有5个额外字段:__$start_lsn、__$end_lsn、__$seqval、__$operation 和__$update_mask,表示日志系列号(Log Sequence Number)、操作(删除、插入、修改前和修改后)信息。

image

四、记录添加记录的数据改变

现在我们就可以来试验CDC针对某个数据表的数据改变的捕捉功能了,我们先来试试记录的添加操作。为此,我们执行如下一段T-SQL,插入两笔User记录。

Insert Into Users(Id, Name, Birthday)
Values ('001','Foo','1981-08-24')

Insert Into Users(Id, Name, Birthday)
Values ('002','Bar','1981-08-24')

然后通过如下的T-SQL查看cdc.dbo_Users_CT表的数据是否将添加操作涉及到的数据改变保存起来。从查询结果我们清晰地看到,上面添加的两笔记录已经被记录下来,而__$operation字段为2表示的是“插入”操作。

image

五、记录更新数据的数据改变

接下来我们来CDC对更新操作的追踪记录,为此我们通过下面的T-SQL改变了用户Foo的Birthday。

Update Users
Set Birthday = '1982-7-10'
Where Name = 'Foo'

再次执行对于cdc.dbo_Users_CT的全表查询,你会看到这次多了两笔记录。其中第3条记录的是修改之前的数据,而第四条则是修改之后的数据,它们的__$operation字段德值分别为34

image

在这里值得一提的是__$update_mask字段的值,它表示的记录更新操作改变的字段。这是一个以16进制表示的数字,在进行对修改字段进行判断的时候需要将其转换成2进制。上述的更新操作对应的__$update_mask值为0x04,转化成2进制就是100,这三位分别代表3个字段。不过这里的顺序是从右到左,所以100这三位表示的字段为Birthday、Name和Id。1表示改变,0则表示保持不变。由于在上面的T-SQL中,我们只改动了Birthday,这个和100这个值是吻合的。

六、记录删除记录的数据改变

我们最后来演示当我们对记录实施删除操作的时候,CDC会为我们记录下怎样的数据。现在我们执行如下的T-SQL将Users表中所有的记录均删除。

Delete From Users

查看cdc.dbo_Users_CT的记录,多出的两笔记录正式我们删除的User记录,__$operation字段的值为1表示“删除”操作。

image

本篇文章仅仅是简单介绍SQLCDC的基本原理和大体上的使用方式,这篇文章《Introduction to Change Data Capture (CDC) in SQL Server 2008[转]》会给你更加详尽的介绍。如果你想深入研究SQLCDC,还是参考SQL Server 2008在线文档。

一个完整的用于追踪数据改变的解决方案

追踪记录每笔业务操作数据改变的利器——SQLCDC – Artech – 博客园已关闭评论
备案信息冀ICP 0007948