2019年7月15日 By mikel 分类: 数据库

来源: SqlServer中查询出多条记录变成一个记录(行变列) – 音乐啤酒 – 博客园

碰到一个SQL问题,不知道怎么搞,问了群里面的一个SQL高手 ,夕颜大哥,他使用短短一句SQL就实现我的要求,太牛了。原来sql还可以这么神奇,唉,还是怪自己sql不精通。

一个表A,两个字段ID,name,一个ID可能对应多name,要求根据一个ID查询出对应的name,但是不是以多行的形式显示出来,而是以一个行的形式出来,多个name以,分割。
比如 数据
i name
1 a
1 b
1 c
应该查询来的结果是; [a,b,c 1]
sql是这样写的

DECLARE @names varchar(1000)
SET @names = ”
SELECT @names=@names+ISNULL(name, ”)+’,’
FROM A
WHERE id = 1
select @names

用的是sql的所谓自拼接,这是夕颜的说法,大家研究一下,感觉很有意思的。

SqlServer中查询出多条记录变成一个记录(行变列) – 音乐啤酒 – 博客园已关闭评论
2019年7月15日 By mikel 分类: ASP.NET MVC, C#

来源: 零基础ASP.NET Core MVC插件式开发 – sylla – 博客园

零基础ASP.NET Core MVC插件式开发

一个项目随着业务模块的不断增加,系统会越来越庞大。如果参与开发的人员越多,管理起来难度也很大。面对这样的情况,首先想到的是模块化插件式开发,根据业务模块,拆分成各个独立的插件,然后分配不同开发人员开发,互相之间没有依赖完全独立。

这里介绍一种使用ASP.NET Core MVC 技术开发Web后台系统,使用插件式的方案。这个方案在项目中已经使用效果觉得还不错,这里把主要思想提取出来,做个简单的demo分享下

 

一、创建主项目

这里使用的开发工具是vs2019,ASP.NET Core 2.1。

创建一个ASP.NET Core MVC项目,项目结构如下图1,完全是脚手架自动创建好的标准项目

图1

 

这里是一个简单的默认程序,在实际的项目中,特别是传统信息管理web后台系统,登录,以及登录之后的主框架,还有一些公共的模块,可以在主项目里面做,不会随业务而变动的。然后各个具体业务模块分成独立的插件开发。

这个主项目建立好之后,运行正常,如图2

图2

 

下面开始做代码部分添加,为了方便阅读代码以截图提供,最后会附加完整的demo程序提供下载。

1、在Startup.cs类里面增加如下代码图3,具体作用看注释,这里就不再赘述。

图3

 

这里是把插件程序注册到主程序里面,核心就是使用了ApplicationParts

 

2、Startup.cs类里面还有一个地方要修改,增加一个Areas区域的路由映射,图4

图4

增加这个的具体原因等会看插件项目的说明

 

3、还有为了演示能否访问到插件,这里增加两个插件的超链接,具体到业务中,菜单肯定是数据库动态维护的。

修改Views/Shared/_Layout.cshtml代码如下 图5

图5

 

 

二、创建插件项目

插件项目也是一个标准的ASP.NET Core MVC 项目。我这里为了演示,创建了两个独立的插件项目。如下图6,图7

图6

 

图7

 

插件项目说明,插件是根据业务模块划分的,还有为了防止注册到主项目之后路由命名的冲突,插件项目就使用MVC自带的区块Areas功能来存放。这里就是前面主项目要增加Areas路由映射的原因。然后删除脚手架默认创建的外层控制器和视图文件,因为主项目也有这样同名的控制器路由,这些不删除,到时候注册到主项目,会出现重复路由错误。

插件项目增加各自的Areas,新建控制器如图8,新建对应试图如图9。这里就添加几个模拟数据。

图8

 

图9

 

到这里,把插件项目发布出来的xxx.dll和xxx.Views.dll,放到主项目里面去就可以运行了。

 

但是作为插件开发者,每次需要把文件发布放到主项目去才能运行,对开发调试都是不方便,因为插件开发者可能就没有主项目的权限,主项目是统一管理的。分配一个插件还每次都附带一个主项目也比较繁琐。

想到这里就可以反过来把主程序dll注册到插件项目里面,这样就可以在插件项目里面vs直接运行调试起来。

 

以插件1为例,Startup.cs类里面增加如下代码,图10

图10

 

这样把主程序的dll提供,比如这个演示demo主程序就需要这两个(Agile.WebPlatform.Main.dll与Agile.WebPlatform.Main.Views.dll)把他们放到插件项目的运行目录。vs直接运行插件项目,就可以正常启动了。各种开发调试完全不受其他影响。

插件里面直接启动调试图11

图11

 

运行效果如下 图12

图12

 

 

点击插件1 显示如下图13

图13

注意!这里虽然可以看到插件2,但点击插件2是会提示找不到路由的,只能调试各自插件的内容。

 

开发调试其他插件类似,提供主项目的dll,各个插件项目完全可以独立开发自己的业务模块,然后运行调试。

 

三、发布

最后,只要把插件项目的发布文件放到主项目的发布文件里面,使用主项目来运行,整个系统所有的插件都能正常使用了。这样就做到了开发调试独立,最后交付插件自己的dll,放到主项目的发布里面就可以了。

如下图14,是最后发布的程序

图14

 

使用命令运行主项目,或者宿主到iis也一样,如图15,命令行运行

图15

 

 

 

浏览器打开访问,如图16

图16

 

 

点击插件1,显示如图17

图17

 

点击插件2,显示如图18

 

图18

 

 

 

希望你看了之后有点收获,代码程序下面附件提供

demo程序

零基础ASP.NET Core MVC插件式开发 – sylla – 博客园已关闭评论
2019年7月15日 By mikel 分类: C#

来源: C# 超高速高性能写日志 代码开源 – Emrys5 – 博客园

1、需求

需求很简单,就是在C#开发中高速写日志。比如在高并发,高流量的地方需要写日志。我们知道程序在操作磁盘时是比较耗时的,所以我们把日志写到磁盘上会有一定的时间耗在上面,这些并不是我们想看到的。

 

2、解决方案

2.1、简单原理说明

使用列队先缓存到内存,然后我们一直有个线程再从列队中写到磁盘上,这样就可以高速高性能的写日志了。因为速度慢的地方我们分离出来了,也就是说程序在把日志扔给列队后,程序的日志部分就算完成了,后面操作磁盘耗时的部分程序是不需要关心的,由另一个线程操作。

俗话说,鱼和熊掌不可兼得,这样会有一个问题,就是如果日志已经到列队了这个时候程序崩溃或者电脑断电都会导致日志部分丢失,但是有些地方为了高性能的写日志,是否可以忽略一些情况,请各位根据情况而定。

 

2.2、示例图

 

3、关键代码部分

这里写日志的部分LZ选用了比较常用的log4net,当然也可以选择其他的日志组件,比如nlog等等。

3.1、日志至列队部分

第一步我们首先需要把日志放到列队中,然后才能从列队中写到磁盘上。

复制代码
        public void EnqueueMessage(string message, FlashLogLevel level, Exception ex = null)
        {
            if ((level == FlashLogLevel.Debug && _log.IsDebugEnabled)
             || (level == FlashLogLevel.Error && _log.IsErrorEnabled)
             || (level == FlashLogLevel.Fatal && _log.IsFatalEnabled)
             || (level == FlashLogLevel.Info && _log.IsInfoEnabled)
             || (level == FlashLogLevel.Warn && _log.IsWarnEnabled))
            {
                _que.Enqueue(new FlashLogMessage
                {
                    Message = "[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss,fff") + "]\r\n" + message,
                    Level = level,
                    Exception = ex
                });

                // 通知线程往磁盘中写日志
                _mre.Set();
            }
        }
复制代码

_log是log4net日志组件的ILog,其中包含了写日志,判断日志等级等功能,代码开始部分的if判断就是判断等级和现在的日志等级做对比,看是否需要写入列队,这样可以有效的提高日志的性能。

其中的_que是ConcurrentQueue列队。_mre是ManualResetEvent信号,ManualResetEvent是用来通知线程列队中有新的日志,可以从列队中写入磁盘了。当从列队中写完日志后,重新设置信号,在等待下次有新的日志到来。

 

3.2、列队到磁盘

从列队到磁盘我们需要有一个线程从列队写入磁盘,也就是说我们在程序启动时就要加载这个线程,比如ASP.NET中就要在global中的Application_Start中加载。

 

复制代码
       /// <summary>
        /// 另一个线程记录日志,只在程序初始化时调用一次
        /// </summary>
        public void Register()
        {
            Thread t = new Thread(new ThreadStart(WriteLog));
            t.IsBackground = false;
            t.Start();
        }

        /// <summary>
        /// 从队列中写日志至磁盘
        /// </summary>
        private void WriteLog()
        {
            while (true)
            {
                // 等待信号通知
                _mre.WaitOne();

                FlashLogMessage msg;
                // 判断是否有内容需要如磁盘 从列队中获取内容,并删除列队中的内容
                while (_que.Count > 0 && _que.TryDequeue(out msg))
                {
                    // 判断日志等级,然后写日志
                    switch (msg.Level)
                    {
                        case FlashLogLevel.Debug:
                            _log.Debug(msg.Message, msg.Exception);
                            break;
                        case FlashLogLevel.Info:
                            _log.Info(msg.Message, msg.Exception);
                            break;
                        case FlashLogLevel.Error:
                            _log.Error(msg.Message, msg.Exception);
                            break;
                        case FlashLogLevel.Warn:
                            _log.Warn(msg.Message, msg.Exception);
                            break;
                        case FlashLogLevel.Fatal:
                            _log.Fatal(msg.Message, msg.Exception);
                            break;
                    }
                }

                // 重新设置信号
                _mre.Reset();
          Thread.Sleep(1);
            }
        }
复制代码

 

3.3、完整代码

 View Code

 

 

4、性能对比和应用

4.1、性能对比

经过测试发现

使用原始的log4net写入日志100000条数据需要:19104毫秒。

同样数据使用列队方式只需要251毫秒。

 

 

 

4.2、应用

4.2.1、需要在程序启动时注册,如ASP.NET 程序中在Global.asax中的Application_Start注册。

复制代码
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            FlashLogger.Instance().Register();
        }
    }
复制代码

 

4.2.2、在需要写入日志的地方直接调用FlashLogger的静态方法即可。

            FlashLogger.Debug("Debug");
            FlashLogger.Debug("Debug", new Exception("testexception"));
            FlashLogger.Info("Info");
            FlashLogger.Fatal("Fatal");
            FlashLogger.Error("Error");
            FlashLogger.Warn("Warn", new Exception("testexception"));

 

5、代码开源

https://github.com/Emrys5/Emrys.FlashLog

 

最后望对各位有所帮助,本文原创,欢迎拍砖和推荐

C# 超高速高性能写日志 代码开源 – Emrys5 – 博客园已关闭评论
2019年7月13日 By mikel 分类: C#

来源: 在 SQL Server 中打开 SQL Server 配置管理器时出现错误消息:”无法连接到 WMI 提供程序。您没有权限或该服务器不可用”


症状


在 64 位计算机上,您安装的 32 位 (基于 x86 的) 版本的 Microsoft SQL Server 实例。在同一台计算机上安装 64 位版本的 SQL Server 2008年实例。如果您然后卸载 64 位实例,打开 SQL Server 配置管理器时您会收到下面的错误消息︰

无法连接到 WMI 提供程序。您没有权限或者服务器无法访问。请注意,您只能管理 SQL Server 2005 及更高的服务器与 SQL Server 配置管理器。
无效的命名空间 [0x8004100e]

如果卸载的 32 位实例,并且您随后打开 SQL Server 配置管理器,也会发生此问题。

原因


因为 WMI 提供程序已卸载 SQL Server 的实例时,将发生此问题。32 位实例和 SQL Server 的 64 位实例共享相同的 WMI 配置文件。此文件位于 %programfiles(x86)%文件夹中。

解决方法


若要变通解决此问题,请打开一个命令提示符,键入以下命令,然后按 enter 键︰

mofcomp”%programfiles (x86) %\Microsoft SQL Server\\Shared\sqlmgmproviderxpsp2up.mof”

注意:若要成功执行此命令,必须 %programfiles(x86) %\Microsoft SQL Server\\Shared 文件夹中存在 Sqlmgmproviderxpsp2up.mof 文件。

数字的值取决于 SQL Server 的版本︰nnn

Microsoft SQL Server 2012 110
Microsoft SQL Server 2008 R2 100
Microsoft SQL Server 2008 100
Microsoft SQL Server 2005 90

运行 Mofcomp 工具后,请重新启动 WMI 服务以使更改生效。服务名称是 Windows 管理规范。

状态


Microsoft 已经确认这是“适用于”一节中列出的 Microsoft 产品中的问题。

详细信息


不支持在 64 位群集环境中安装 32 位实例的 SQL Server 2008年。有关 Microsoft SQL Server 支持策略的详细信息,请单击下面的文章编号,以查看 Microsoft 知识库中相应的文章︰

327518 Microsoft SQL Server 的 Microsoft 群集支持政策

有关管理对象编译器 (Mofcomp) 工具的其他信息,请参阅 MSDN 上的以下页面︰

http://msdn.microsoft.com/en-us/library/windows/desktop/aa392389(v=vs.85).aspx

在 SQL Server 中打开 SQL Server 配置管理器时出现错误消息:”无法连接到 WMI 提供程序。您没有权限或该服务器不可用”已关闭评论
2019年7月13日 By mikel 分类: C#

来源: 关于数据库‘状态’字段设计的思考与实践 – 倒骑的驴 – 博客园

关于数据库‘状态’字段设计的思考与实践

 

正文

最近在做订单及支付相关的系统,在订单表的设计阶段,团队成员就‘订单状态’数据库字段设计有了一些分歧,网上也有不少关于这方面的思考和探讨,结合这些资料和项目的实际情况,拟对一些共性问题进行更深一层的思考,笔耕在此,和大家一起探讨。

1. 问题综述

这里的分歧点即有团队内部的分歧点,也有网络上常见的一些分歧点,先将存在的分歧点抛出来:

1、订单表的‘订单状态’字段对应的字典值应当包含哪些状态值?对于‘已评论’、‘已退货’、’已退款’这类状态是放到‘订单状态’中?还是独立一个字段标识?

2、订单表的‘订单状态’字段对应的字典值如何表示?可选项有:使用数字标识、使用多‘位’存储方式标识、使用具有明确业务含义的英文字符串标识;

3、订单表的‘订单状态’字段使用何种类型?可选项有:number(N)、char(N)、varchar2(N);

如果嫌分析过程过于啰嗦,可以直接拉到最后看结论。

2. 业务分析

我们先不去看问题,先来看看和‘订单(Order)’实体相关的业务是怎样的。下面我们会针对可能改变订单实体状态的行为已经状态变化的可能性进行详细的分析。

订单业务实体相关的业务流程如下:下单(create)–> 买家付款(pay)–> 卖家发货(deliver)–>买家收货(receive)–>退货(rereturn);此外,还有退款(refund)和评论(comment),这两个行为比较特殊,其前向行为可能存在多个。

首先,可以改变订单业务状态【这里的状态不是指‘订单状态’(OrderState)这个数据库字段,而是指实际业务状态,我们简记为(BizState),以和OrderState区分开】的行为有哪些?按照典型电商的业务流程,主要的行为(action)有:下单、付款、发货、收货、退款/退货、评论;每一种行为的发生,都会导致订单的业务状态BizState发生变化,比如‘下单’行为会创建订单,‘付款’行为会使订单变为‘已付款’,‘发货’行为可以使订单状态变为‘已发货’,‘收货’行为会使订单状态变为‘已收货’,‘评论’行为会使订单状态变为‘已评论’。‘退款/退货’action不是所有订单都支持的,为减小复杂度,暂不考虑它们。

其次,细分下每种action对BizState带来的影响,会发现还可以细分为四种子状态(subState):action未开始(标记为0)、action进行中(标记为1)、action成功(标记为2)、action失败(标记为3);理论上,将所有action的所有subState进行排列得到4*4*4*4*4=1024(暂未考虑‘退货’);实际上,很多组合是没有业务意义的,是不可能存在的,比如‘未开始已付款…’(***20)这一类组合是不可能发生的,应当舍弃。用表格将上述的组合分析如下:

通过上表,我们可以发现些的规律:

‘下单’、‘付款’、‘发货’、‘收货’前四种action是存在依赖关系的,亦即后一个action依赖于前一个action的完成;所以,他们的SubState组合情况就会非常少;

‘评论comment’这个action的SubState和其他状态组合会有很多种可能性;除了前面了两行是‘X’,后面是‘?’或者‘Y’,‘?’是指需求上是否允许在对应的BizState上进行评论,如果允许,则每种BizState需要多出4种可能,这样组合的可能性就会变得很大。

没有业务意义的SubState组合被舍弃。表中的标黑单元格,表示这个BizState是毫无意义的,因为‘未下单’的订单对于我们来讲是不存在的,这类组合需要舍弃;同样的,还有很多其他的组合也是不存在的,被舍弃掉,未展示在上表中,如‘已下单已付款未发货已收货’这种。

通常某个action的SubState为‘1进行中’、‘3失败’时,会被忽略,但也有例外;比如‘付款’action的‘3失败’状态,和‘付款’action的‘1进行中’状态,具体分析见后面内容。

忽略所有action的‘0未开始’SubState状态。因为这类SubState对于BizState不会带来变化。

综合下来,我们得到上表的BizState,注意这里的Comment action未进行细化处理,如果细化处理,会发现BizState的可能性会增大很多很多。

接下来我们就之前提出的这些问题进行逐个讨论。

3. 问题一、订单表的‘订单状态’字段应当包含哪些状态值?

什么样的‘订单业务状态’(BizState)需要记录到系统层面的‘订单状态’(OrderState)字段呢?如果记录多了,则系统处理的复杂度会增大;记录少了,那么‘订单状态’(OrderState)字段就不能完整的表示出订单实体状态变化情况。

核心状态

通过上面的业务分析可知:大部分存在依赖关系的action(create、pay、deliver、receive),他们产生的合理的SubState组合是非常少的,而且他们之间的依赖是单向依赖,状态机的处理也很简单,因此,我们先将这部分BizState纳入到OrderState中:

  • 等待买家付款
  • 买家付款成功
  • 卖家已发货
  • 买家已收货

目前的订单状态流转:

‘action行为’失败的情况

对于action的SubState是‘3失败’的处理,需要针对不同的action进行分析。类似‘下单Create’这样的action,如果失败,则可以直接将OrderState置为‘订单创建失败’,因为Create action是第一个action,它的失败意味着Order实体出生即死,BizState置为终态,对于这个BizState应当纳入到OrderState中记录,不过这个OrderState其实对于用户并无多大用处,因为用户并不会关心下单失败的订单,他更关心的是重新下单;

对于‘支付’失败,则要看需求,如果需求要求用户可以继续支付,则订单需要保留,并且状态仍然为‘等待买家付款’,如果不允许再支付,则理论上可以将BizState置为‘支付失败’终态,所以,‘支付失败’的BizState终态也应当记录到OrderState字段中。

对于‘发货’失败、‘收货’失败的情况,通常是不会发生的,即使发生也不属于系统能够控制的范畴,系统记录并无意义,更具建设性的做法是通过线下手段尽快解决问题,重新发货等等,所以对于这些状态系统的OrderState字段不予记录。

这样下来我们的OrderState字典值增加到6个,加粗项为新增:

  • 创建订单失败(终态)
  • 等待买家付款
  • 买家付款失败(终态,依赖需求而定)
  • 买家付款成功
  • 卖家已发货
  • 买家已收货

目前的订单状态流转:

‘action行为’进行中的情况

对于action的SubState是‘1进行中’的处理,同样需要具体场景具体分析。‘付款’行为是用户发起的,但是并不是和订单系统之间的交互,涉及到支付系统的处理,这个领域也不是订单系统可控的,但关系到钱,用户比较关系,所以对于这样一个中间态,我们需要记录,以便用户通过订单系统查询订单状态,为便于用户理解,将此状态在OrderState中记为‘付款确认中’;‘发货’‘收货’进行中的情况,不是订单系统可以控制的领域,我们可以把他们当着行为‘未开始’处理,比如‘发货进行中’,订单系统的OrderState值为‘买家已付款’,但给用户看到的提示信息是‘买家已付款,等待卖家发货’,实际上这时候卖家可能正在发货中,但是用户不会去关心到底有没有打包好货物什么的,所以这类‘进行中’状态可以舍弃。这样下来订单系统的OrderState字段又多了一个字典值:‘付款确认中’:

  • 创建订单失败(终态)
  • 等待买家付款
  • 付款确认中
  • 买家付款失败(终态,依赖需求而定)
  • 买家付款成功
  • 卖家已发货
  • 买家已收货

目前的订单状态流转:

‘action行为’未开始的情况

忽略所有action的‘0未开始’SubState状态。因为这类SubState对于BizState不会带来变化。

‘评论comment’的处理

最后,再来看看‘评论comment’这个action。如果需求上要求:只有买家收货后才能发起‘评论’操作,则可以任务‘评论comment’单向依赖于‘receive收货’行为,那么可以将这个action的subState对应的少量BizState(应当只有‘买家已评论’、‘卖家已评论’状态)纳入OrderState字段统一记录;但是如果需求是:买家在下单后就可以开始评论,比如如果卖家发货慢了,买家可以上去吐槽,那么‘评论comment’就不是单向依赖于‘receive收货’行为了,而是多向依赖于‘pay付款’、‘deliver发货’、‘receive收货’,那么这些actions的subState组合可能性就暴增,BizState的字典取值也会暴增,显然,不应当将这么多的BizState交给OrderState来记录,而应当由一个独立的数据库字段负责记录‘评论comment’的SubState,我们可以将这个字段取名为‘CommentState’(评论状态),它的字典值不多,只有:‘未评论’、‘买家已评论’、‘卖家已评论’;其实,对于前一种需求,也可以不讲‘评论comment’对应的SubState产生的BizState纳入OrderState,因为用户对于评论与否其实并不是那么关心的,也就是说‘评论comment’并不是核心业务流程,为了降低核心业务流程的系统处理复杂度,将其从核心业务流程中剥离出来较好。

综上,我们应当将‘评论comment’对应的BizState独立到一个字段中记录。

‘退货rereturn’的处理

再来看看‘退货rereturn’行为对应的BizState的处理。‘退货rereturn’并不是所有订单都会经历的,但是一旦涉及,则‘退货return’在业务流程上必定是单向依赖于‘receive收货’,所以应当将‘退货return’产生的BizState(‘退货中’、‘退货成功’,‘退款失败’和‘未退货’被忽略,见上面解释)纳入OrderState一并记录;这样我们的OrderState有多了两种字典值,这里我们不考虑一个订单中有多种商品的情况,故把‘退货成功’当着终态处理,如果是一个订单多种货物的情况,需要重新仔细分析。加粗项为新增:

  • 创建订单失败(终态)
  • 等待买家付款
  • 付款确认中
  • 买家付款失败(终态,依赖需求而定)
  • 买家付款成功
  • 卖家已发货
  • 买家已收货
  • 退货中
  • 退货成功(终态)

目前的订单状态流转:

‘退款refund’的处理

最后来看下‘退款refund’行为对应的BizState的处理。首先,我们需要知道‘退货’和‘退款’是两种不同的业务行为,他们的关系是:通常意义上,‘退货’必然导致‘退款’,但是‘退款’可以没有‘退货’的参与(这里不讨论特殊情况,比如对于虚拟货物来讲,付款成功通常以为着收货成功,这时候就只能是在由‘退货’导致‘退款’),比如电商允许用户付款成功后收到货物前发起‘退款’。也就是说‘退款refund’并不单向依赖于‘退货rereturn’,和‘评论comment’一样是多项依赖,所以,我们可以参考‘评论comment’的处理方式,单独建立一个字段‘RefundState退款状态’记录‘退款refund’产生的BizState,这个状态字段的字典值有:退款中,退款成功。

其他情况考虑

另外,可能还有一些增强型需求,让客户体验更好,比如用户可以创建订单之后付款之前,将订单取消,或者由系统跑批将用户长时间未支付的订单关闭,这会产生一种新的action——‘close关闭’,对应的会产生一种新的有意义的BizState——‘订单关闭/取消’,这个不属于核心流程中的,且并无纠结之处,不予详细讨论,罗列如下:

  • 创建订单失败(终态)
  • 等待买家付款
  • 付款确认中
  • 买家付款失败(终态,依赖需求而定)
  • 买家付款成功
  • 卖家已发货
  • 买家已收货
  • 退货中
  • 退货成功(终态)
  • 订单关闭(终态)

结论

综上,我们可以得出放入数据库’订单状态‘字段的标准:核心业务流程,向前单向依赖。扩展到其他业务实体是一样的,这里说的’订单状态‘字段实际是指该业务实体对应的数据表的主业务状态字段。我们把结论扩展一下:

如果某个action属于业务实体对应的核心业务流程,且该action单向依赖于其前向的action,则需要将这个action产生的BizState放入到业务实体对应的数据库表的主状态字段中记录。

OrderState字段记录的BizState业务状态有10种,其中4种是终态,其余状态为中间态。这些状态的流转关系为:

4. 问题二、订单表的‘订单状态’字段的字典值的表示形式?

先列出可选项:使用数字标识、使用多‘位’存储方式标识、使用具有明确业务含义的英文字符串标识;对可选项做逐一解释:

a、使用数字标识——使用一个数字标识一种状态,并未要求是sequence的;如‘等待买家付款’表示为‘0’;

b、使用多‘位’存储方式标识——将某种行为是否发生对应的状态对应到一个位上,比如‘是否付款’定义在第一位,‘是否发货’定义在第二位,‘是否收货’定义在第三位,‘是否评论’定义在第四位,则状态‘卖家已收货未评论’可以表示为:0111;而‘等待买家付款’则表示为‘0000’;当然这里的‘位’可能是二进制的也可能是N进制,后面我们详细讨论。

c、使用具有明确业务含义的英文字符串标识——该方案和方案a类似,不过字典值变为具有明确业务含义的英文支付串,如‘等待买家付款’表示为‘WAIT_BUYER_PAY’;

方案a是数据库字段字典的惯用方式,简单直观,但是有一个坏处在于:当字典值较多时,数据库表的使用者记不住字典的含义,需要反复查找资料确认;有人会说将字典值写到字段的注释里,这个在实践中不是很靠谱,通常表建立后,如果字段增加了字典值,通常开发人员都会忽略更改字典值;而且在使用工具(如pl/SQL)查询数据库时,并不会将所有字典值展示出来;

通过问题一的分析,可知:方案b使用多‘位’存储方式会增加复杂度,并没有必要,可以通过将‘是否评论’状态独立成一个字段进行表示。

方案c和方案a类似,好处在于通过字典值直接知道业务含义,坏处在于会给编码和手工查询时带来复杂度,通常人们也记不住‘等待买家付款’的英文字典是‘WAIT_BUYER_PAY’,那么手动写SQL查询‘等待买家付款’时就犯迷糊了。

折中之后,我们组合方案a和方案c,得到方案d:另外建立一张字典表,存储:数字形式的字典值、字典英文名称、字典中文简称、字典解释;订单实体表的OrderState字段使用数字作为字典值。

对于方案d,看到OrderState的数字形式状态时,可以先看看字段注释是否有此字典的定义,如果没有就取查下字典表,得到字典值和含义;在编码和手动sql查询时也会变得比较容易,数字的位数毕竟要少些;建立字典表的其他好处还有:字典的解释可以写的很详细,在报表中要求展示字典中文名时,也能直接从数据库联表查询得到,而不必额外做一次映射。(有参考:数据库表设计(状态字段)

那么对于字典数量很少的状态字段是否有必要额外新建一张字典表呢?这个根据实际情况考虑,通常可以先不建,如果后续有业务场景需要再行创建也不迟。

而对于非业务实体表的系统日志/跑批记录表等的状态,则完全可以使用数字形式的字典,因为通常不会有业务场景使用到这些字典值,而且这些字典值域应当会比较小,所以没有必要为他们创建单独的字典表。

综上得出结论:

1、字典值域较多、变化较多、报表等业务场景会使用到的业务实体表的业务状态字段,使用‘方案d:新建字典表’的方案处理;如‘订单业务实体表’中的‘订单状态’字段。

2、字典值域较少、变化较少等业务场景不会使用到的业务实体表的业务状态字段,使用‘方案a:使用数字标识字典’的方案处理;如‘支付宝的支付流水表’的‘支付流水状态’字段。

3、系统日志/跑批记录表的状态字段,使用‘方案a:使用数字标识字典’的方案处理;如‘待收货记录表’的‘跑批状态’字段。

5. 问题三、数据库表的‘状态’字段使用何种类型

列出可选项:number(N)、char(N)、varchar2(N),其中N是一个长度值。

这个问题主要需要考虑使用场景、扩展性、性能、存储。

‘状态’字段主要使用在查询场景,且通常是‘=’或者‘in’的查询,并没有区间类的查询,故三者差别不大;

对于性能,参考[原创]在Oracle 10g,Number、Char和Varchar2类型作为主键,查询效率分析 char(N)、varchar2(N)性能优于number(N),故舍弃number(N)。

考虑到扩展性,char(N)、varchar2(N)差不多;

考虑到存储,varchar2更加占用空间更小,故选择varchar2(N)。

综上:选择varchar2(N)作为数据库‘状态’字段的类型。

6. 问题结论汇总

1、订单表的‘订单状态’字段对应的字典值应当包含哪些状态值?对于‘已评论’、‘已退货’这类状态是放到‘订单状态’中?还是独立一个字段标识?

如果某个action(行为,如支付)属于业务实体对应的核心业务流程,且该action单向依赖于其前向的action,则需要将这个action产生的业务状态放入到业务实体对应的数据库表的主状态字段中记录。

问题中的‘已评论’由‘评论’行为产生,而‘评论’这个action并不是订单业务实体的核心业务流程,且可能存在多个前向依赖action(支付、发货、收货等),所以应当独立到一个字段标识。

问题中的‘已退货’由‘退货’行为产生,而‘退货’这个action是订单业务实体的核心业务流程,用户非常关心,且只单向依赖于‘收货’action,所以应当记录到订单业务实体表的‘订单状态’字段中。

问题中的‘已退款’由‘退款’行为产生,而‘退款’这个action是订单业务实体的核心业务流程,用户非常关心,但是这个action存在多个前向依赖action(支付、发货、收货等),所以应当独立到一个字段标识。

2、订单表的‘订单状态’字段对应的字典值如何表示?可选项有:使用数字标识、使用多‘位’存储方式标识、使用具有明确业务含义的英文字符串标识;

i、字典值域较多、变化较多、报表等业务场景会使用到的业务实体表的业务状态字段,使用‘方案d:新建字典表’的方案处理;如‘订单业务实体表’中的‘订单状态’字段。

j、字典值域较少、变化较少等业务场景使用到的业务实体表的业务状态字段,使用‘方案a:使用数字标识字典’的方案处理;如‘支付宝的支付流水表’的‘支付流水状态’字段。

k、系统日志/跑批记录表的状态字段,使用‘方案a:使用数字标识字典’的方案处理;如‘待收货记录表’的‘跑批状态’字段。

3、订单表的‘订单状态’字段使用何种类型?可选项有:number(N)、char(N)、varchar2(N);

varchar2(N)占用存储更少,且具有同等的性能、扩展性,选择varchar2(N)作为数据库‘状态’字段的类型。

关于数据库‘状态’字段设计的思考与实践 – 倒骑的驴 – 博客园已关闭评论
2019年7月11日 By mikel 分类: 数据库

来源: SQLServer性能优化之—数据库级日记监控 – 鲲逸鹏 – 博客园

上节回顾:https://www.cnblogs.com/dotnetcrazy/p/11029323.html

4.6.6.SQLServer监控

脚本示意:https://github.com/lotapp/BaseCode/tree/master/database/SQL/SQLServer

PS:这些脚本都是我以前用SQLServer手写的,参考即可(现在用MySQL,下次也整理一下)

之前写SQLServer监控系列文章因为换环境断篇了,只是简单演示了下基础功能,现在准备写MySQL监控相关内容了,于是补了下:

SQLServer性能优化之—数据库级日记监控https://www.cnblogs.com/dunitian/p/6022967.html

在说监控前你可以先看下数据库发邮件https://www.cnblogs.com/dunitian/p/6022826.html

应用:一般就是设置个定时任务,把耗时SQL信息或者错误信息通过邮件的方式及时预警

好处就太多了,eg:客户出错如果是数据库层面,那瞬间就可以场景重放(PS:等客户找会降低业绩)

以往都是程序的try+catch来捕获错误,但数据库定时任务之类的出错程序是捕获不到的,所以就需要数据库层面的监控了

PS:开发的时候通过SQLServer Profiler来监控

先说说本质吧:SQLServer2012的XEVENT机制已经完善,eg:常用的扩展事件error_reported就可以在错误的时候通过邮件来通知管理员了

PS:扩展事件性能较高,而且比较轻量级

PS:SQLServer的监控大体思路三步走:发邮件事件监控定时执行

4.6.6.1 发送邮件

这个之前讲过,这边就再说下SQL的方式:

1.配置发件人邮箱

这个配置一次即可,以后使用就可以直接通过配置名发邮件

--开启发邮件功能
exec sp_configure 'show advanced options',1
reconfigure with override
go
exec sp_configure 'database mail xps',1
reconfigure with override
go

--创建邮件帐户信息
exec msdb.dbo.sysmail_add_account_sp
  @account_name ='dunitian',                     -- 邮件帐户名称
  @email_address ='xxx@163.com',                 -- 发件人邮件地址
  @display_name ='SQLServer2014_192.168.36.250', -- 发件人姓名
  @MAILSERVER_NAME = 'smtp.163.com',             -- 邮件服务器地址
  @PORT =25,                                     -- 邮件服务器端口
  @USERNAME = 'xxx@163.com',                     -- 用户名
  @PASSWORD = '邮件密码或授权码'                 -- 密码(授权码)
GO

--数据库配置文件
exec msdb.dbo.sysmail_add_profile_sp
  @profile_name = 'SQLServer_DotNetCrazy',       -- 配置名称
  @description = '数据库邮件配置文件'            -- 配置描述
go

--用户和邮件配置文件相关联
exec msdb.dbo.sysmail_add_profileaccount_sp
  @profile_name = 'SQLServer_DotNetCrazy',     -- 配置名称
  @account_name = 'dunitian',                  -- 邮件帐户名称
  @sequence_number = 1                         -- account 在 profile 中顺序(默认是1)
go
2.发生预警邮箱

同样我只演示SQL的方式,图形化的方式可以看我以前写的文章:

-- 发邮件测试
exec msdb.dbo.sp_send_dbmail
@profile_name = 'SQLServer_DotNetCrazy',         --配置名称
@recipients = 'xxx@qq.com',                   --收件邮箱
@body_format = 'HTML',                         --内容格式
@subject = '文章标题',                         --文章标题
@body = '邮件内容<br/><h2>This is Test</h2>...' --邮件内容

效果:

06-10/1.mail.png

3.邮件查询相关

主要用途其实就是出错排查:

-- 查询相关
select * from msdb.dbo.sysmail_allitems     --查看所有邮件消息
select * from msdb.dbo.sysmail_mailitems    --查看邮件消息(更多列)

select * from msdb.dbo.sysmail_sentitems    --查看已发送的消息
select * from msdb.dbo.sysmail_faileditems  --失败状态的消息
select * from msdb.dbo.sysmail_unsentitems  --看未发送的消息

select * from msdb.dbo.sysmail_event_log    --查看记录日记

4.6.6.2.监控实现

会了邮件的发送,那下面就是监控了

1.图形化演示

不推荐使用图形化的方式,但可以来理解扩展事件的监控

1.新建一个会话向导(熟悉后可以直接新建会话)

1.新建会话向导.png

1.新建会话向导2.png

2.设置需要捕获的扩展事件

2.设置需要捕获的扩展事件.png

3.这边捕获的全局字段和左边SQL是一样的(截图全太麻烦了,所以偷个懒,后面会说怎么生成左边的核心SQL)

3.捕获的全局字段.png

4.自己根据服务器性能设置一个合理的值(IO、内存、CPU)

4.设置.png

5.生成核心SQL(我们图形化的目的就是生成核心SQL,后面可以根据这个SQL自己扩展)

5.生成核心SQL.png

6.核心代码如下

6.核心代码.png

7.启动会话后一个简单的扩展事件监控就有了

7.启动会话.png

8.SQLServer提供了查看方式

8.查看.png

9.日志可以自己查下xxx\Microsoft SQL Server\MSSQL12.MSSQLSERVER\MSSQL\Log

9.日志.png


2.SQL的方式

上面只是过家家,主要目的就是让大家知道核心SQL是怎么来的,凭什么这么写

下面就来个制定化监控:

先截图演示下各个核心点,然后贴一个我封装的存储过程附件

1.扩展事件相关的核心代码

1.扩展事件相关的核心代码.png

2.内存中数据存储到临时表

2.内存中数据存储到临时表.png

3.临时表中的数据存储到自己建立的表中

我抛一个课后小问给大家:为什么先存储在临时表中?(提示:效率)

3.临时表中的数据存储到自己建立的表中.png

4.发送监控提醒的邮件

4.发送监控提醒的邮件.png

5.看看数据库层面多了什么:

5.看看数据库层面.png

6.来个测试

6.测试.png

7.效果(可以自己美化)

7.效果.png

SQL附录
-- 切换到需要监控的数据库
USE [dotnetcrazy]
GO

--收集服务器上逻辑错误的信息
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO

-- 自定义的错误信息表
IF OBJECT_ID('log_error_message') IS NULL
BEGIN
    CREATE TABLE [dbo].[log_error_message]
    (
    [login_message_id] [uniqueidentifier] NULL CONSTRAINT [DF__PerfLogic__Login__7ACA4E21] DEFAULT (newid()),
    [start_time] [datetime] NULL,
    [database_name] [nvarchar] (128) COLLATE Chinese_PRC_CI_AS NULL,
    [message] [nvarchar] (max) COLLATE Chinese_PRC_CI_AS NULL,
    [sql_text] [nvarchar] (max) COLLATE Chinese_PRC_CI_AS NULL,
    [alltext] [nvarchar] (max) COLLATE Chinese_PRC_CI_AS NULL,
    -- [worker_address] [nvarchar] (1000) COLLATE Chinese_PRC_CI_AS NULL,
    [username] [nvarchar] (1000) COLLATE Chinese_PRC_CI_AS NULL,
    [client_hostname] [nvarchar] (1000) COLLATE Chinese_PRC_CI_AS NULL,
    [client_app_name] [nvarchar] (1000) COLLATE Chinese_PRC_CI_AS NULL
    ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
END
GO

-- 创建存储过程
CREATE PROCEDURE [dbo].[event_error_monitor]
AS
    IF NOT EXISTS( SELECT 1 FROM sys.dm_xe_sessions dxs(NOLOCK) WHERE name = 'event_error_monitor') -- 不存在就创建EVENT
        -- 创建扩展事件,并把数据放入内存中
        BEGIN
            CREATE EVENT session event_error_monitor on server
            ADD EVENT sqlserver.error_reported -- error_reported扩展事件
            (
            ACTION -- 返回结果
            (
            sqlserver.session_id, -- 会话id
            sqlserver.plan_handle, -- 计划句柄,可用于检索图形计划
            sqlserver.tsql_stack, -- T-SQ堆栈信息
            package0.callstack, -- 当前调用堆栈
            sqlserver.sql_text, -- 遇到错误的SQL查询
            sqlserver.username, -- 用户名
            sqlserver.client_app_name, -- 客户端应用程序名称
            sqlserver.client_hostname, -- 客户端主机名
            -- sqlos.worker_address, -- 当前任务执行时间
            sqlserver.database_name -- 当前数据库名称
            )
            WHERE severity >= 11 AND Severity <=16 -- 指定用户级错误
            )
            ADD TARGET package0.ring_buffer -- 临时放入内存中
            WITH (max_dispatch_latency=1seconds)

            -- 启动监控事件
            ALTER EVENT SESSION event_error_monitor on server state = START
        END
    ELSE
        -- 存储过程已经存在就把数据插入表中
        BEGIN
            -- 将内存中已经收集到的错误信息转存到临时表中(方便处理)
            SELECT
                DATEADD(hh,
                        DATEDIFF(hh, GETUTCDATE(), CURRENT_TIMESTAMP),
                        n.value('(event/@timestamp)[1]', 'datetime2')) AS [timestamp],
                n.value('(event/action[@name="database_name"]/value)[1]', 'nvarchar(128)') AS [database_name],
                n.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(max)') AS [sql_text],
                n.value('(event/data[@name="message"]/value)[1]', 'nvarchar(max)') AS [message],
                n.value('(event/action[@name="username"]/value)[1]', 'nvarchar(max)') AS [username],
                n.value('(event/action[@name="client_hostname"]/value)[1]', 'nvarchar(max)') AS [client_hostname],
                n.value('(event/action[@name="client_app_name"]/value)[1]', 'nvarchar(max)') AS [client_app_name],
                n.value('(event/action[@name="tsql_stack"]/value/frames/frame/@handle)[1]', 'varchar(max)') AS [tsql_stack],
                n.value('(event/action[@name="tsql_stack"]/value/frames/frame/@offsetStart)[1]', 'int') AS [statement_start_offset],
                n.value('(event/action[@name="tsql_stack"]/value/frames/frame/@offsetEnd)[1]', 'int') AS [statement_end_offset]
            into #error_monitor -- 临时表
            FROM
            (    SELECT td.query('.') as n
                FROM
                (
                    SELECT CAST(target_data AS XML) as target_data
                    FROM sys.dm_xe_sessions AS s
                    JOIN sys.dm_xe_session_targets AS t
                        ON t.event_session_address = s.address
                    WHERE s.name = 'event_error_monitor'
                    --AND t.target_name = 'ring_buffer'
                ) AS sub
                CROSS APPLY target_data.nodes('RingBufferTarget/event') AS q(td)
            ) as TAB

            -- 把数据存储到自己新建的表中(有SQL语句的直接插入到表中)
            INSERT INTO log_error_message(start_time,database_name,message,sql_text,alltext,username,client_hostname,client_app_name)
            SELECT TIMESTAMP,database_name,[message],sql_text,'',username,client_hostname,client_app_name
            FROM #error_monitor as a
            WHERE a.sql_text != '' --AND client_app_name !='Microsoft SQL Server Management Studio - 查询'
            AND a.MESSAGE NOT LIKE '找不到会话句柄%' AND a.MESSAGE NOT LIKE '%SqlQueryNotification%' --排除server broker
            AND a.MESSAGE NOT LIKE '远程服务已删除%'

            -- 插入应用执行信息(没有SQL的语句通过句柄查询下SQL)
            INSERT INTO log_error_message(start_time,database_name,message,sql_text,alltext,username,client_hostname,client_app_name)
            SELECT TIMESTAMP,database_name,[message],
            SUBSTRING(qt.text,a.statement_start_offset/2+1,
                        (case when a.statement_end_offset = -1
                        then DATALENGTH(qt.text)
                        else a.statement_end_offset end -a.statement_start_offset)/2 + 1) sql_text,qt.text alltext,
            username,client_hostname,client_app_name
            FROM #error_monitor as a
            CROSS APPLY sys.dm_exec_sql_text(CONVERT(VARBINARY(max),a.tsql_stack,1)) qt -- 通过句柄查询具体的SQL语句
            WHERE a.sql_text IS NULL AND tsql_stack != '' --AND client_app_name = '.Net SqlClient Data Provider'

            DROP TABLE #error_monitor -- 删除临时表

            --重启清空
            ALTER EVENT SESSION event_error_monitor ON SERVER STATE = STOP
            ALTER EVENT SESSION event_error_monitor on server state = START
        END

    -- 美化版预警邮箱
    DECLARE @body_html VARCHAR(max)
    set @body_html = '<table style="width:100%" cellspacing="0"><tr><td colspan="6" align="center" style="font-weight:bold;color:red">数据库错误监控</td></tr>'
    set @body_html = @body_html + '<tr style="text-align: left;"><th>运行时间</th><th>数据库</th><th>发生错误的SQL语句</th><th>消息</th><th>用户名</th><th>应用</th><th>应用程序名</th></tr>'
    -- 格式处理(没内容就空格填充)
    select @body_html = @body_html + '<tr><td>'
        + case (isnull(start_time, '')) when '' then '&nbsp;' else convert(varchar(20), start_time, 120) end + '</td><td>'
        + case (isnull(database_name, '')) when '' then '&nbsp;' else database_name end + '</td><td>'
        + case (isnull(sql_text, '')) when '' then '&nbsp;' else sql_text end + '</td><td>'
        + case (isnull(message, '')) when '' then '&nbsp;' else message end + '</td><td>'
        + case (isnull(username, '')) when '' then '&nbsp;' else username end + '</td><td>'
        + case (isnull(client_hostname, '')) when '' then '&nbsp;' else client_hostname end + '</td><td>'
        + case (isnull(client_app_name, '')) when '' then '&nbsp;' else client_app_name end + '</td></tr>'
    from (
             select start_time, database_name,sql_text, message, username, client_hostname, client_app_name
             from [dbo].[log_error_message]
             where start_time >= dateadd(hh,-2,getdate()) -- 当前时间 - 定时任务的时间间隔(2h)
               and client_app_name != 'Microsoft SQL Server Management Studio - 查询' -- and client_hostname in('')
         ) as temp_message
    set @body_html= @body_html+'</table>'

    -- 发送警告邮件
    exec msdb.dbo.sp_send_dbmail
    @profile_name = 'SQLServer_DotNetCrazy',         --配置名称
    @recipients = 'xxxxx@qq.com',                  --收件邮箱
    @body_format = 'HTML',                           --内容格式
    @subject = '数据库监控通知',                       --文章标题
    @body = @body_html --邮件内容
go

下节预估:定时任务、完整版监控

PS:估计先得更八字的文章(拖太久)然后更完SQLServer更MySQL,等MySQL监控更完会说下备份与恢复,接着我们开架构篇(MyCat系列先不讲放在Redis和爬虫系列的后面)

晚点在下面补上

SQLServer性能优化之—数据库级日记监控 – 鲲逸鹏 – 博客园已关闭评论
2019年7月8日 By mikel 分类: 架构设计

来源: ERP设计之系统基础管理(BS)-日志模块设计(转载) – woork – 博客园

日志模块基本要素包括:

用户会话、登录、注销、模块加载/卸载、数据操作(增/删/改/审/弃/关等等)、数据恢复、日志查询,如果高要求的客户可能还需要审计分析、总结报告。

如果想提高用户体验,可以从用户日志分析中得出更多用户操作行为上的数据,以便我们改进程序模块,加深用户体验。

 

设计日志模块,要考虑多个层面:

1、  用户会话管理:主要记录登录、注销、用户端信息。

2、  用户行为管理:主要记录用户操作行为习惯,记录模块加载/卸载、功能使用率。

3、  数据操作日志管理:主要记录用户数据流的变化情况,可追溯、分析、恢复。

4、  日志分析审计:处理分析日志,总结与报告。

 

会话/任务日志比较简 单,重点在数据操作日志,因为ERP系统数据表多,结构复杂,数据量也大。数据操作先要理清记录日志的方式。增加记录是否要记日志,笔者的理解不需要。只 有当数据被更改或删除时才需要记录日志。如果增加需要记录,那么它的数据量就非常大了,不推荐。修改数据时或删除时,记录修改或删除前的数据,同时记录用 户会话信息,而不是用户ID,这样防止非法用户篡改日志数据。

 

数据日志表如何设计,是 用一个表,还是每个表对应一个日志表,笔者推荐后者。如果技术达不到的话,就用一个表,但追溯、恢复、分析数据就难了,因为表结构不同,不能冗余的将日志 数据放在一起,不利于恢复,分析。同时,如果不同的表对应一个日志表处理起来也是非常复杂,增加编程难度,但是技术是可以克服的。

 

注意:强烈不推荐使用DBMS引擎的触发器,使用它后,数据库服务器的性能会大大地降低(特别是在使用不当的时候,情况更糟),并且也不可以在每个表上做触发器,有时业务逻辑日志,触发器根本没用,另外触发器记录的信息有限,不足以提供分析、审计所要的信息。

 

日志模块架构体系: 用户表—》用户登录会话日志—》用户任务(模块)日志—》用户数据操作日志。

 

如果是按照每张业务单据表对应一个日志表,那么操作日志表最好不要放在同一个数据库上,可单独建立一个日志库,表结构就是对应的每张业务单据的表结构加上日志记录相关字段,日志表名则以业务单据表名+“_Logs”为统一后缀格式,以方便统计及批量处理日志表。

批量处理日志表,因为这 样相关的业务单据太多,不太可能每个业务单据都去手工建立一一对应的日志表,对于批量处理的事务,交给DBMS。主要是处理思路,数据库一般都支持处理数 据定义DML语句(创建表、视图等),在程序中动态调用处理定义日志表结构,然后将数据日志内容一起提交给数据库服务器就可以了,或者在数据库定义一个存 储过程处理。

如何用SQL脚本复制创建表结构,笔者在此提供一个简单的SQL2000  SQL代码:复制表结构

 

注意:以上代码只是取表对像信息,如Image、二进制数据等等字段没有加入,因为这些数据没必加入日志。在插入日志数据内容时,同样也可以用上面的方式,提取需要的字段,插入日志表,并记录用户操作信息。

 

下图为笔者的日志浏览界面:

ERP设计之系统基础管理(BS)-日志模块设计(转载) – woork – 博客园已关闭评论
2019年7月6日 By mikel 分类: 架构设计

来源: 面向对象架构模式之:领域模型(Domain Model) – 陆敏技 – 博客园

一:面向对象设计中最简单的部分与最难的部分

如果说事务脚本是 面向过程 的,那么领域模型就是 面向对象 的。面向对象的一个很重要的点就是:“把事情交给最适合的类去做”,即:“你得在一个个领域类之间跳转,才能找出他们如何交互”,Martin Flower 说这是面向对象中最难的部分,这具有误导的成份。确切地说,我们作为程序员如果已经掌握了 OOD 和 OOP 中技术手段,那么如何寻找类之间的关系,可能就成了最难的部分。但在实际的情况中,即便我们不是程序员,也总能描述一件事情(即寻求关系),所以,找 对象之间的关系 还真的并不是程序员最关系的部分,从技术层面来讲,寻找类之间的关系因为与具体的编码技巧无关,所以它现在对于程序员的我们来说,应该是最简单的部分,技术手段才是这里面的最难部分。

好,切入正题。

 

二:构筑类之间的关系(最简单部分)

先来完成最简单的部分,即找关系。也就是说,按照所谓的关系,我们来重构 事务脚本 中的代码。上篇“你在用什么思想编码:事务脚本 OR 面向对象?”中同样的需求,如果用领域模式来做的话,我们大概可以这样设计:

image

(备注:Product 和 RecognitionStrategy  为 * –> 1 的关系是因为 一种确认算法可以被多个产品的实例对象使用)

从下面的示例代码我们就可以看到这点:

class RevenueRecognition
{
private double amount;
private DateTime recognizedOn;

public RevenueRecognition(double amount, DateTime recognizedOn)
{
this.amount = amount;
this.recognizedOn = recognizedOn;
}

public double GetAmount()
{
return this.amount;
}

public bool IsRecognizedBy(DateTime asOf)
{
return asOf.CompareTo(this.recognizedOn) > 0 || asOf.CompareTo(this.recognizedOn) == 0;
}
}

class Contract
{
// 多 对 1 的关系,* -> 1。即:一个产品可有多个合同订单
private Product product;
private long id;
// 合同金额
private double revenue;
private DateTime whenSigned;

// 1 对 多 的关系, 1 -> *
private List<RevenueRecognition> revenueRecognitions = new List<RevenueRecognition>();

public Contract(Product product, double revenue, DateTime whenSigned)
{
this.product = product;
this.revenue = revenue;
this.whenSigned = whenSigned;
}

public void AddRevenueRecognition(RevenueRecognition r)
{
revenueRecognitions.Add(r);
}

public double GetRevenue()
{
return this.revenue;
}

public DateTime GetWhenSigned()
{
return this.whenSigned;
}

// 得到哪天前入账了多少
public double RecognizedRevenue(DateTime asOf)
{
double re = 0.0;
foreach(var r in revenueRecognitions)
{
if(r.IsRecognizedBy(asOf))
{
re += r.GetAmount();
}
}

return re;
}

public void CalculateRecognitions()
{
product.CalculateRevenueRecognitions(this);
}
}

class Product
{
private string name;
private RecognitionStrategy recognitionStrategy;

public Product(string name, RecognitionStrategy recognitionStrategy)
{
this.name = name;
this.recognitionStrategy = recognitionStrategy;
}

public void CalculateRevenueRecognitions(Contract contract)
{
recognitionStrategy.CalculateRevenueRecognitions(contract);
}

public static Product NewWordProcessor(string name)
{
return new Product(name, new CompleteRecognitionStrategy());
}

public static Product NewSpreadsheet(string name)
{
return new Product(name, new ThreeWayRecognitionStrategy(60, 90));
}

public static Product NewDatabase(string name)
{
return new Product(name, new ThreeWayRecognitionStrategy(30, 60));
}
}

abstract class RecognitionStrategy
{
public abstract void CalculateRevenueRecognitions(Contract contract);
}

class CompleteRecognitionStrategy : RecognitionStrategy
{
public override void CalculateRevenueRecognitions(Contract contract)
{
contract.AddRevenueRecognition(new RevenueRecognition(contract.GetRevenue(), contract.GetWhenSigned()));
}
}

class ThreeWayRecognitionStrategy : RecognitionStrategy
{
private int firstRecognitionOffset;
private int secondRecognitionOffset;

public ThreeWayRecognitionStrategy(int firstRoff, int secondRoff)
{
this.firstRecognitionOffset = firstRoff;
this.secondRecognitionOffset = secondRoff;
}

public override void CalculateRevenueRecognitions(Contract contract)
{
contract.AddRevenueRecognition(
new RevenueRecognition(contract.GetRevenue() / 3, contract.GetWhenSigned()));
contract.AddRevenueRecognition(
new RevenueRecognition(contract.GetRevenue() / 3, contract.GetWhenSigned().AddDays(firstRecognitionOffset)));
contract.AddRevenueRecognition(
new RevenueRecognition(contract.GetRevenue() / 3, contract.GetWhenSigned().AddDays(secondRecognitionOffset)));
}
}

 

正像我说的,以上的代码是最简单部分,每个 OOP 的初学者都能写出这样的代码来。但是我心想,即便我们能写出这样的代码来,我们恐怕都不会心虚的告诉自己:是的,我正在进行领域驱动开发吧。

那么,真正难的部分是什么?

2.1 领域模型 对于程序员来说真正困难或者困惑的部分

是领域模型本身怎么和其它模块(或者其它层)进行交互,这些交互或者说关系是:

1:领域模型 自身具备些什么语言层面的特性;

2:领域模型 和 领域模型 之间的关系;

3:领域模型 和 Repository 的关系;

4:工作单元 和 领域模型 及 Repository 的关系;

5:领域模型 的缓存;

6:领域模型 和 会话之间的关系;

 

三:那些交互与关系

3.1 领域模型 自身具备些什么语言层面的特性

先看代码:

public class User2 : DomainObj
{
#region Field
#endregion

#region Property

#endregion

#region 领域自身逻辑

#endregion

#region 领域服务
#endregion
}

对于一个领域模型来说,从语言层面来讲,它具备 5 方面的特性:

1:有父类,放置公共的属性之类的内容,同时,存在一个父类,也表示它不是一个 值对象(领域概念中的值对象);

2:有实例字段;

3:有实例属性;

4:领域自身逻辑,非 static 方法,有 public 的和 非public;

5:领域服务,static 方法,可独立出去放置到对应的 服务类 中;

现在,我们具体展开一下。不过,为了展开讲,我们必须提供一个稍稍完整的 User2 的例子,它在真正的项目是这个样子的:

     public class User2 : DomainObj
{
#region Field
private Organization2 organization;

private List<YhbjTest> myTests;

private List<YhbjClass> myClasses;

#endregion

#region Property

public override IRepository RootRep
{
get { return RepRegistory.UserRepository; }
}

public string UserName { get; private set; }

public string Password { get; private set; }

/* 演示了同时存在 Organization 和 OrganizationId 两个属性的情况 */
public string OrganizationId { get; private set; }

public Organization2 Organization
{
get
{
if (organization == null && !string.IsNullOrEmpty(OrganizationId))
{
organization = Organization2.FindById(OrganizationId);
}

return organization;
}
}

/* 演示了存在 列表 属性的情况 */
public List<YhbjClass> MyClasses
{
get
{
if (myClasses == null)
{
myClasses = YhbjClass.GetClassesByUserId(this);
}

return myClasses;
}
}

public List<YhbjTest> MyTests
{
get
{
/* 我的考试来自两个地方,1:班级、项目上的考试;2:选人的考试;
* 故,有两种设计方法
* 1:选人的考试没有疑议;
* 2:班级、项目考试,可以从本模型的 Classes -> Projects -> Tests 获取;
* 3:也可以直接从数据库得到获取;
* 在这里的实际实现,采用第 2 种做法。因为:
* 1:数据本身是缓存的,第一获取的时候,貌似存在多次查询,但是一旦获取就缓存了;
* 2:存在很多地方的数据一致性问题,采用方法 3 貌似快速了,但会带来不可知 BUG ;
* 3:即便将来考试还有课程上的考试,可以很方便的获取,不然还需要重改 SQL
*/
if (myTests == null)
{
myTests = new List<YhbjTest>();

/* 加指定人的考试,这些考试没有对应的 项目 和 班级*/
myTests.AddRange(YhbjTest.GetTestsByUserId(this.Id));

/* 加班级的考试,有对应的 班级 */
foreach (var c in MyClasses)
{
myTests.AddRange(c.Tests);
foreach (var t in c.Tests)
{
t.SetOwnerClass(c);
}

/* 加项目的考试,有对应的 班级 和 项目,代码略 */
}
}

/* 其它逻辑 */
foreach (var test in myTests)
{
if (test.TestHistory == null)
{
test.SetHistory(MyTestHistories
.FirstOrDefault(p => p.TestId == test.Id && p.UserId == this.Id));
}
}

return myTests;
}
}
#endregion

#region 领域自身逻辑

public void InitWithOrganization(Organization2 o)
{
/* 在这个方法中不用 MakeDirty,因为相当于初始化分为两步进行了
*/
this.organization = o;
}

/* 不需要对外开放的逻辑,使用 internal*/
internal virtual void UpdateOnline(string loginTime, string token, string loginIp, string loginPort)
{
/* 这样做的好处是什么呢?
*  createnew 方法用户不负责自己的持久化,而是由事务代码进行负责
*  但是 createnew 方法会标识自己为 new,即 makenew 方法调用
*  然后,由于 ut 用的都是同一个 ut,所以在事务这里 commit 了
*  就是 commit 了 根 和 非根
* 这里也同时演示了多个领域对象共用一个 ut
*/
this.UnitOfWork = new UnitOfWork();
UserOnline2 userOnline2Old = UserOnline2.GetUserOnline(this.UserName);
if (userOnline2Old != null)
{
userOnline2Old.UnitOfWork = this.UnitOfWork;
userOnline2Old.Delete();
}

UserOnline2 = UserOnline2.CreateNew(UserName, loginIp, loginPort);
UnitOfWork.RegisterNew(UserOnline2);
UnitOfWork.Commit();
}

/* 对外开放的逻辑,使用 public */
public List<YhbjTest> GetMyTest(string testName, int type, int page, int size, out int totalCount)
{
IEnumerable<YhbjTest> expName = from p in MyTests orderby p.CreateTime descending select p;
IEnumerable<YhbjTest> expState = null;

switch (type)
{
case 0:
/* 未考
* 需要排除掉 有效期 之外
*/
expState =
from p in expName
where
p.StartTime <= DateTime.Now &&
p.EndTime >= DateTime.Now &&
(this.MyTestHistories.Exists(q => q.TestId == p.Id) == false
|| (this.MyTestHistories.Exists(q => q.TestId == p.Id) == true && this.myTestHistories.Find(h => h.TestId == p.Id).TestState != 1)) &&
p.AuditState == AuditState.Audited
select p;
break;
default:
throw new ArgumentOutOfRangeException();
}

var re = expState.ToList();
totalCount = re.Count;
return re.Skip((page – 1) * size).Take(size).ToList();
}

public YhbjTest StartTest(string testId)
{
// 逻辑略
}

#endregion

/// <summary>
/// 1:服务是无状态的,所以是 static 的
/// 2:服务是公开的,所以是 public 的
/// 3:服务实际是可以创建专门的服务类的,这里为了演示需要,就放在一起了
/// </summary>
#region 领域服务

/* 这两个字段演示其实服务部分的代码是随意的 */
private static readonly CookieWrapper CookieWrapper;

private static readonly HttpWrapper HttpWrapper;

static User2()
{

CookieWrapper = new CookieWrapper();
HttpWrapper = new HttpWrapper();
}

/* 内部的方法当然是私有的 */
private static List<PaperQuestionStrategy3> GetUserPaperByUserAndTest(User2 user2, YhbjTest test)
{
var x = RepRegistory.UserRepository.FindTestUserPaper(user2, test);
return x;
}

/* 获取领域对象的方法,全部属于领域服务部分,再次强调是静态的 */
public static User2 GetUserByName(string username)
{
var user = (RepRegistory.UserRepository).FindByName(username);
return user as User2;
}
/* 领域对象的获取和产生,还有另外的做法,就是在对象工厂中生成,但这不属于本文要阐述的范畴 */
public static User2 CreateCreater(
string creatorOrganizationId, string creatorOrganizationName, string id, string name)
{
var user = new User2 { Id = id, Name = name, UnitOfWork = new UnitOfWork() };
user.MakeNew();
return user;
}
#endregion
}

请仔细查看上面代码,为了本文接下来的阐述,上面的代码几乎都是有意义的,我已经很精简了。好了,基于上面这个例子,我们展开讲:

1:父类

public abstract class DomainObj
{
public Key Key { get; set; }

/// <summary>
/// 根仓储
/// TIP: 因为是充血模式,所以每个领域模型都有一个根仓储
/// 用于提交自身的变动
/// </summary>
public abstract IRepository RootRep { get; }

protected DomainObj()
{
}

public UnitOfWork UnitOfWork { get; set; }

public string Id { get; protected set; }

public string Name { get; protected set; }

protected void MakeNew()
{
UnitOfWork.RegisterNew(this);
}

protected void MakeDirty()
{
UnitOfWork.RegisterDirty(this);
}

protected void MakeRemoved()
{
UnitOfWork.RegisterRemoved(this);
}

}

父类包含了,让一个 领域模型 成为 领域模型 所必备的那些特点,它有 标识映射(架构模式对象与关系结构模式之:标识域(Identity Field)),它持有 工作单元(),它负责调用 工作单元的API(换个角度说工作单元(Unit Of Work):创建、持有与API调用)。

如果我们的对象是一个 领域模型对象,那么它必定需要继承之这个父类;

2:有实例字段

有人可能会有疑问,不是有属性就可以了吗,为什么要有字段,一个理由是,如果我们需要 延迟加载(),就需要使用字段来进行辅助。我们在上面的源码中看到的 if XX == NULL ,这样的属性代码,就是延迟加载,其中使用到了字段。注意,如果使用了延迟加载,你应该会遇到序列化的问题,这是你需要注意的《延迟加载与序列化》。

3:有实例属性

属性是必然的,没有属性的领域模型很稀少的。有几个地方需要大家注意,

1:属性的 get 方法,可以是很复杂的,其地位相当于是领域自身逻辑;

2:set 方法,都是 private 的,领域对象自身负责自身属性的赋值;

3:在有必要的情况下,使用 延迟加载,这可能需要另外一个主题来讲;

4:延迟加载的那些属性,很多时候就是 导航属性,即 Organization 和 MyClasses 这样的属性,就是导航属性;

4:领域自身逻辑

领域自身逻辑,包含了应用系统大多数的业务逻辑,可以理解为:它就是传统 3 层架构中的业务逻辑层的代码。如果一段代码,你不知道把它放到哪里,那么,它多半就属于应该放在这里。注意,只有应该公开的那些方法,才 public;

5:领域服务

领域服务,可以独立出去,成为领域服务类。那么,什么样的代码是领域服务代码?第一种情况:

生成领域对象实例的方法,都应该是领域服务类。如 查询 或者 Create New。

在实际场景中,我们可能使用对象工厂来生成它们,这里为了纯粹的演示哪些是 领域自身逻辑,哪些是 领域服务,特意使用了领域类的 static 方法来生成领域对象。即:

领域对象,不能随便被外界生成,要严格控制其生成。所以领域父类的构造器,我们看到是 protected 的。

那么,实际上,除了上面这种情况外,任何代码都应该是 领域自身逻辑的。我在上面还演示了这样的一段代码:

private static List<PaperQuestionStrategy3> GetUserPaperByUserAndTest(User2 user2, YhbjTest test)
{
var x = RepRegistory.UserRepository.FindTestUserPaper(user2, test);
return x;
}

这段代码,实际上作为领域服务部分,就是错误的,它应该被放置在 YhbjTest 这个领域类中。

 

3.2 领域模型 和 领域模型 之间的关系

也就是说那些导航属性和领域模型有什么关系。导航属性必须都是延迟加载的吗?当然不是。比如, User 所在的 Organization,我们在在使用到用户这个对象的时候,几乎总是要使用到其组织信息,那么,我们在获取用户的时候,就应该立即获取到组织对象,那么,我们的持久化代码是这样的:

        public override DomainObj Find(Key key)
{
var user = base.Find(key) as User2;
if (user == null)
{
//
string SQL = @”
DECLARE @ORGID VARCHAR(32)=”;
SELECT @ORGID=OrganizationId FROM [EL_Organization].[USER] WHERE ID=@Id
SELECT * FROM [EL_Organization].[USER] WHERE ID=@Id
SELECT * FROM [EL_Organization].[ORGANIZATION] WHERE ID=@ORGID”;
var pms = new SQLParameter[]
{
new SqlParameter(“@Id”, key.GetId())
};

var ds = SqlHelper.ExecuteDataset(CommandType.Text, sql, pms);
user = DataTableHelper.ToList<User2>(ds.Tables[0]).FirstOrDefault();
var o = DataTableHelper.ToList<Organization2>(ds.Tables[1]).FirstOrDefault();
if (user == null)
{
return null;
}

user = Load(user);
// 注意,除了 Load User 还需要 Load Organization
user.InitWithOrganization(o);
Load(user.Organization);

return user;
}

return user;
}

可以看到,我们在一次 sql 执行的时候,就得到了 organization,然后,User2 类型中,有个属于领域自身逻辑方法:

        public void InitWithOrganization(Organization2 o)
{
/* 在这个方法中不用 MakeDirty,因为相当于初始化分为两步进行了
*/
this.organization = o;
}

在这里要多说一下,如果不是初始化时候的改属性,如修改了用户的组织信息,就应该 MakeDirty。

注意,还有一个比较重要的领域自身逻辑,就是 SetOwned,如下:

public void SetOwnerClass(YhbjClass yhbjClass)
{
this.OwnerClass = yhbjClass;
/* should not makeDirty, but if class repalced or removed, should makedirty*/
}

比如,领域模型 考试,就可能会有这个方法,考试本身需要知道:我属于哪个班级。

 

3.3 领域模型 和 Repository 之间的关系

第一,如果我们在使用 领域模型,我们必须使用 Repository 模式吗?答案是:当然不是,我们可以使用 活动记录模式(什么是活动记录,当前我们可以暂时理解为传统3层架构中的DAL层)。如果我们在使用 Repository ,那么,领域模型和 Respository 之间是什么关系呢?这里,有两点需要阐述:

第一点是,一般的做法,Repository 是被注入的,它可能被注入到系统的某个地方,示例代码是被注入到了类型 RepRegistory中。

领域模型要不要使用 Repository,我的答案是:要。

为什么,因为我们要让领域逻辑自己决定合适调用 Repository。

第二点是,每个领域模型都有一个 RootRep,用于自身以及把自身当成根的那些导航属性对象的持久化操作;

 

3.4 工作单元 和 领域模型 及 Repository 的关系

这一点比较复杂,我们单独在 《换个角度说工作单元(Unit Of Work):创建、持有与API调用》 进行了阐述。当然,跟 Repository 一样,使用 领域模型,必须使用 工作单元 吗?答案也是不是。只是,在使用 工作单元 后,更易于我们处理 领域模型 中的事务问题。

 

3.5 领域模型的缓存

缓存分为两类,第一类我们可以称之为 一级缓存,这对于客户端程序员来说,不可见,它被放置在 AbstractRepository 中,往往在当前请求中有用:

public abstract class AbstractRepository : IRepository
{
/* LoadedDomains 在有些文献中可以作为高速缓存,但是这个缓存可不是指的
* 业务上的那个缓存,而是 片段 的缓存,指在当前实例的生命周期中的缓存。
* 业务上的缓存在我们的系统中,由每个领域模型的服务部分自身持有。
*/
protected Dictionary<Key, DomainObj> LoadedDomains =
new Dictionary<Key, DomainObj>();

public virtual DomainObj Find(Key key)
{
if (LoadedDomains.ContainsKey(key))
{
return LoadedDomains[key] as DomainObj;
}
else
{
return null;
}

//return null;
}

public abstract void Insert(DomainObj t);

public abstract void Update(DomainObj t);

public abstract void Delete(DomainObj t);

public void CheckLoaedDomains()
{
foreach (var m in LoadedDomains)
{
Console.WriteLine(m.Value);
}
}
/// <summary>
/// 当缓存内容发生变动时进行重置
/// </summary>
/// <param name=”keyField”>缓存key的id</param>
/// <param name=”type”>缓存的对象类型</param>
public void ResetLoadedDomainByKey(string keyId,Type type)
{
var key=new Key(keyId,type);
if (LoadedDomains.ContainsKey(key))
{
LoadedDomains.Remove(key);
}
}

protected T Load<T>(T t) where T : DomainObj
{
var key = new Key(t.Id, typeof (T));
/* 1:这一句很重要,因为我们不会想要放到每个子类里去赋值
* 2:其次,如果子类没有调用 Load ,则永远没有 Key,不过这说得过去
*/
t.Key = key;

if (LoadedDomains.ContainsKey(key))
{
return LoadedDomains[key] as T;
}
else
{
LoadedDomains.Add(key, t);
return t;
}

//return t;
}

protected List<T> LoadAll<T>(List<T> ts) where T : DomainObj
{
for (int i = 0; i < ts.Count; i++)
{
ts[i] = Load(ts[i]);
}

return ts;
}
}

业务系统中的缓存,需要我们随着业务系统自身的特点,自己来创建,比如,如果我们针对 User2 这个领域模型建立缓存,就应该把这个缓存挂接到当前会话中。此处不表。

 

3.6 领域模型 与 会话之间的关系

这是一个有意思的话题,无论是理论上还是实际中,在一次会话当中(如果我们会话的参照中,可以回味下 ASP.NET 中的 Session,它们所表达的概念是一致的),只要会话不失效,那么 领域对象 的状态,就应该是被保持的。这里难的是,我们怎么来创建这个 Session。Session 回到语言层面,就是一个类,它可能会将领域对象保持在 内存中,或者文件中,或者数据库中,或者在一个分布式系统中(如 Memcached,《ASP.NET性能优化之分布式Session》)。

最简单的,我们可以使用 ASP.NET 的 Session 来保存我们的会话,然后把领域对象存储到这里。

 

四:总结

以上描述了让领域模型成为领域模型的一些最基本的技术手段。解决了这些技术手段,我们的开发才基本算是 DDD 的,才是面向领域模型的。解决了这些技术问题,接下来,我们才能毫无后顾之忧地去解决 Martin Flower 所说的最难的部分:“你得在一个个领域类之间跳转,才能找出他们如何交互”。

面向对象架构模式之:领域模型(Domain Model) – 陆敏技 – 博客园已关闭评论
2019年7月6日 By mikel 分类: 架构设计

来源: 你在用什么思想编码:事务脚本 OR 面向对象? – 陆敏技 – 博客园

最近在公司内部做技术交流的时候,说起技能提升的问题,调研大家想要培训什么,结果大出我意料,很多人想要培训:面向对象编码。于是我抛出一个问题:你觉得我们现在的代码是面向对象的吗?有人回答:是,有人回答否。我对这个问题的回答是:语法上,是了,但是架构上或者思想上,不是。我们现在的大部分代码,如果要死扣一个名词的话,那就是:事务脚本。

 

1:最开始的事务脚本

在 Martin Fowler 的书中,存在一个典型的 应用场景,即“收入确认”(Revenue Recognition)。该“收入确认”的描述:

一家软件公司有3种产品,其售价策略分别为,第一种:交全款才能卖给你;第二种,付三分之一,就给你,60天后,再给1/3,90天后给完全部;第三种,付1/3,就给你,30天后给1/3,60天后给完。

但是,关于这个描述,我打算多啰嗦几句,而且个人觉的这个啰嗦非常之紧要,因为它影响到了我们的设计。以下是啰嗦的部分:

“收入确认”,在概念上,确实是产品的入账策略,实际上,Martin 的代码,也是这么去实现的,不同的产品有不同的入账策略。不过,数据库实现,RevenueRecognition 这个表记录的是“产品的某个合同根据产品类型所计算出来的:应该执行的入账日及金额”,即策略是跟着合同走的,而不是跟着产品走的。这很有意思,如果你精读此部分,这种矛盾就会一直纠结在你心头。同时,我们又不得不时刻提醒自己存在的这个需求。

现在,关于这个场景,如果我们理解了 产品 合同 RevenueRecognition 之间的关系,我们就很能理解了数据库是被设计成这样的:

image

其概念模型为如下:

image

好了,现在我们来看看什么是事务脚本,对的,就用代码来说话。在原文中, Martin 举了两个例子,但是精读之后,我打算将其颠个倒,把原文中的示例2讲在前头。因为示例2,很好的表达了什么才是作者或者译者眼中的“收入确认”,以及我眼中的“收入策略”。

第一个要实现的功能,即第一个事务脚本描述如下:

根据合同 ID,找到该合同,并根据合同类型得到应该在哪天收入多少钱,并插入数据库。

从该描述中,我们知道,这个脚本最应该发生在签订合同时。因为合同一旦签订,就应该记录什么时候应该收到客户端多少钱。代码如下:

class RecognitionService
{
dynamic dal = null;

// 计算哪天该入账多少并插入
public void CalculateRevenueRecognitions(long contactNumber)
{
DataSet contractDs = dal.FindContract(contactNumber);
double totalRevenue = (double)contractDs.Tables[0].Rows[0][“ID”];
DateTime dateSigned = (DateTime)contractDs.Tables[0].Rows[0][“DateSigned”];
string type = (string)contractDs.Tables[0].Rows[0][“Type”];
if(type == “S”)    // 电子表格类
{
// the SQL “INSERT INTO REVENUECONGNITIONS (CONTRACT,AMOUNT,RECOGNIZEDON) VALUES (?,?,?)”
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned);
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(60));
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(90));
}else if(type == “W”)    // 文字处理
{
dal.InsertRecognition(contactNumber, totalRevenue, dateSigned);
}else if(type == “D”)    // 数据库
{
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned);
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(30));
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(60));
}
}
}

 

第二个需求是:计算某合同在某个日期前的应该有的入账。

class RecognitionService
{
dynamic dal = null;

// 得到哪天前入账了多少
public double RecognizedRevenue(long contractNumber, DateTime asOf)
{
// the SQL “SELECT AMOUNT FROM REVENUECONGNITIONS WHERE CONTRACT=? AND RECOGNIZEDON <=?”;
DataSet ds = dal.FindRecognitionsFor(contractNumber, asOf);
double r = 0.0;
foreach(DataRow dr in ds.Tables[0].Rows)
{
r += (double)dr[“AMOUNT”];
}

return r;
}
}

从上面的代码,我们可以看出什么才是 事务脚本:

1:采用面向过程的方式组织业务逻辑;
2:没有或尽量少的实体类;
3:一个方法一件事情,故有大量业务类或方法;
4:能与行数据入口表数据入口很好协作;

 

2:事务脚本之变体

也许上面的代码多多少少让大家嗤之以鼻,认为现在很少会这样来写代码了。那么,我们来看看下面这段代码:

class RecognitionBll
{
dynamic dal = null;

// 计算哪天该入账多少并插入
public void CalculateRevenueRecognitions(long contactNumber)
{
List<Contact> contracts = dal.FindContract(contactNumber);
double totalRevenue = (double)contracts[0].Id;
DateTime dateSigned = (DateTime)contracts[0].DateSigned;
string type = (string)dal.FindContractType(contactNumber);
// 上面这行代码你还可能会写成
// string type = (string)dal.contracts[0].ProductType;
// 或者
// string type = (string)dal.contracts[0].Product.Type;
if(type == “S”)    // 电子表格类
{
// the SQL “INSERT INTO REVENUECONGNITIONS (CONTRACT,AMOUNT,RECOGNIZEDON) VALUES (?,?,?)”
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned);
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(60));
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(90));
}else if(type == “W”)    // 文字处理
{
dal.InsertRecognition(contactNumber, totalRevenue, dateSigned);
}else if(type == “D”)    // 数据库
{
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned);
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(30));
dal.InsertRecognition(contactNumber, totalRevenue / 3, dateSigned.AddDays(60));
}
}

// 得到哪天前入账了多少
public double RecognizedRevenue(long contractNumber, DateTime asOf)
{
// the sql “SELECT AMOUNT FROM REVENUECONGNITIONS WHERE CONTRACT=? AND RECOGNIZEDON <=?”;
List<RevenueRecognition> revenueRecognitions = dal.FindRecognitionsFor(contractNumber, asOf);
double r = 0.0;
foreach(RevenueRecognition rr in revenueRecognitions)
{
r += rr.Amount;
}

return r;
}
}

public class Product
{
public long Id;
public string Name;
public string Type;
}

public class Contact
{
public long Id;
public long ProductId;
public string ProductType;
public Product Product;
public double Revenue;
public DateTime DateSigned;
}

public class RevenueRecognition
{
public long ContactId;
public double Amount;
public double RevenuedOn;
}

在这个事务脚本的变种中,我们看到了所有人写过代码的影子:

1:有了实体类了,所以看上去貌似是面向对象编码了;

2:看到了 “三层架构” 了,即:实体层、DAL层、业务逻辑层等;

但是,它仍旧是 事务脚本 的!唯一不同的是,它光鲜的把 DataSet 变成了 List<Model> 了!

 

3:什么是面向对象的?

那么,什么是面向对象的编码,面向对象的一个很重要的点就是:“把事情交给最适合的类去做”,并且“你得在一个个业务类之间跳转,才能找出他们如何交互”。这确实是个不那么简单的话题,而本文的主旨也仅在于指出,如果我们的代码中还没有 工作单元 映射 缓存 延迟加载 等等概念,即便我们编码再熟练,也仅仅是在熟练的 面向过程编码。

你在用什么思想编码:事务脚本 OR 面向对象? – 陆敏技 – 博客园已关闭评论
2019年7月6日 By mikel 分类: C#

来源: 动态的加载类型 – Halower – 博客园

动态的加载类型

 

 

 

 

开篇先熟悉两个小概念:

早绑定:是指在编译时绑定对象的类型

晚绑定:是指在运行时才绑定对象的类型。

当然我们提到上面两个概念,肯定是为了引入今天的主题——利用反射实现晚绑定(也就是动态的加载类型,并调用它们)。

我暂时只是为了测试的方便先定义一个不能执行的程序集(Person.dll)无需写的完善,仅仅作为测试使用,之后我们在这个程序中调用它。

person.dll内部如下:

复制代码
 1  using System;
 2     public class Chinese
 3     {
 4         private string language;
 5         private string name;
 6         private int age;
 7         private string like;
 8         public string Like
 9         {
10             get { return like; }
11             set { like = value; }
12         } 
13         public string Language
14         {
15             get { return language; }
16             set { language = value; }
17         }
18         public string Name
19         {
20             get { return name; }
21             set { name = value; }
22         }
23         public int Age
24         {
25             get { return age; }
26             set { age = value; }
27         }
28         public virtual void Favourite()
29         {
30             Console.WriteLine(name+"今年"+age+岁"喜欢的运动是:"+this.like);
31         }
32         public Chinese()
33         { 
34         }
35         public Chiese(string name, int age)
36         {
37             this.name = name;
38             this.age = age;
39         }
40     }
复制代码

注意:当我们不用vs生成注意程序集的时候,一定要注意主程序运行时不能低于该程序集的运行时,默认的使用命令提示执行时使用的是最新的运行时:例如:我这里执行了:

之后执行主程序报错,于是更新了一下运行时版本就OK了。

如果我们这样定义:就会发生早绑定,因为编译时,编译器将从程序集中导入Chinese类。

Chiesep1=new Chiese(“小强”,18);

那么我们怎么实现晚绑定呢?

现在我们再看下开篇晚绑定的概念,也就是说我们这里实现的效果是:不会再元数据中嵌入对类型的引用,而是在运行是通过反射来实现。现在我们就开始动态加载类型和调用方法。先看一种方式:

  1. 使用Assembly类的Load方法来动态的来动态的加载指定的程序集。
  2. 使用Assembly类的GetType方法来动态的加载指定的类型。
  3. 通过Type类的InvokeMember方法来调用Type对象所表示的类型方法。

下面的代码演示如歌动态的加载程序集Person.dll,动态的加载Chinese类型,并调用Favourite方法:

复制代码
 1 using System;
 2 using System.IO;
 3 using System.Reflection;
 4 
 5 namespace 动态的加载类型
 6 {
 7     class Program
 8     {
 9         static void Main()
10         {
11             //加载程序集
12             string assemblyPath = Path.Combine(Environment.CurrentDirectory, "person.dll");
13             Assembly a = Assembly.LoadFrom(assemblyPath);
14            //获取指定的类型
15             Type t = a.GetType("Chinese");
16            //构造类型实例
17             Object[] args = new Object[] { "小强",18 };
18             Object obj = t.InvokeMember(null,
19                 BindingFlags.DeclaredOnly |                       //指定绑定类型
20                 BindingFlags.Public | BindingFlags.NonPublic |
21                 BindingFlags.Instance | BindingFlags.CreateInstance, null, null, args);
22             Console.WriteLine("新创建的类型: " + obj.GetType().ToString());
23             Console.WriteLine("================");
24             //给字段like赋值
25             t.InvokeMember("like",
26            BindingFlags.DeclaredOnly |
27            BindingFlags.Public | BindingFlags.NonPublic |
28            BindingFlags.Instance | BindingFlags.SetField, null, obj, new Object[] { "抓篮球" });
29             //调用方法
30             t.InvokeMember("Favourite", BindingFlags.DeclaredOnly |
31             BindingFlags.Public | BindingFlags.NonPublic |
32             BindingFlags.Instance | BindingFlags.InvokeMethod, null, obj, null);
33             Console.ReadKey();
34         }
35     }
36 }
复制代码

利用System.Activator类

承接上面的实例更简单的加载类型的方法,就是利用System.Activator类。

该类包含特定的方法,用以在本地或从远程创建对象类型,或获取对现有远程对象的引用。此类不能被继承。

步骤如下: 

1.加载程序集并调用GetType方法获取目标类型对象

2.调用Activator.CreateInstance(Type)使用指定类型的默认构造函数来创建该类型的实例,并通过Type对象的GetMethod方法来获取MethodInfo对象

3.使用MethodInfo对象的Invoke方法来动态的执行方法。

 

复制代码
 1 using System;
 2 using System.Reflection;
 3 
 4 namespace Test
 5 {
 6     class Program
 7     {
 8         static void Main()
 9         {
10             Assembly a = Assembly.Load("person.dll");
11             Type t = a.GetType("Chinese");
12             Object[] args = new Object[] { "小强", 18 };
13             object obj = Activator.CreateInstance(t, args);
14             MethodInfo mi = t.GetMethod("Favourite");
15             mi.Invoke(obj, null);
16             Console.ReadKey();
17         }
18     }
19 }
复制代码

执行效果图:

动态的加载类型 – Halower – 博客园已关闭评论
备案信息冀ICP 0007948