Scree- C# 纯净 ORM 框架

C# ORM/持久层框架

详细介绍

目录

一、Scree是为了解决什么问题?

二、Scree为了取代数据访问层是如何思考的?做了哪些事情?

三、现在开始使用scree

四、Scree的高阶能力

五、分布式环境下的scree

六、常见问题集锦

前言

2008年初,来上海不久的我,有幸加入了一家在沪上的.net领域还算有一定名气的公司。第一次接触到ORM,那是技术老大自行研发的。其实,它并不仅仅是ORM,而是基于ORM的一整套框架体系。当时的我就被震撼到了,对一套框架来说,核心并不在于代码,而在于它曾经面临什么样的问题,在于它解决问题的思维方式。随着研究和使用的深入,越发觉得它的思想在当时很超前的(这里就不多提了),同时,也发现了它的一些不足和过度设计的问题。于是,我萌生了写一套自己框架的想法。说干就干,这一干,从08年开始断断续续就好几年。迭代了很多版本,逐步完善了很多功能,也删了更多的功能。其后,项目经过几年在磁盘中的封印,又经过几年在实际生产中的应用,又经过N年文档懒癌的治疗,终于下定决心开源出来。我喜欢写代码,特别喜欢逻辑思考的过程,但一点也不喜欢写项目文档,何况还是教别人怎么使用这一堆代码的文档。

项目名scree(小石子)来源于大学期间,远在04年的故事,暂不多提了,说了太多废话,开始进入正题。再家里奶奶般的重复一遍,问题与解决问题的思想才是最重要的。

一、Scree是为了解决什么问题?

要回答这个问题,不得不提非ORM的开发模式,同时也不得不提三层架构。

三层与N层这里不展开,需要提及的是三层中的数据访问层不一定就是一层,也可以是两层、三层甚至N层。在非ORM开发模式下,通过在业务逻辑层编写SQL语句(很多时候也以视图或存储过程的形态存在),然后调用数据访问层而达到读写数据的目的。而基于ORM底层框架(这里需要加上底层这个定义,随着技术的发展、系统规模的膨胀三层框架任何一层在横向和纵向都有了大规模的层级扩展),主旨就是为了替换掉数据访问层同时变业务逻辑层的SQL操作方式为对象化操作方式,这也是scree的初衷。

对象化操作的好处就不用多说了,谁用谁知道。ORM的出现,将面向对象语言的系统构建过程全面的对象化了,不再留有SQL时代的缺憾。数据终于全部变成了一个个、一组组的对象,一线开发人员的精气神可以更专注于业务逻辑,更有效率的团队协作、更快速的数据迁移、更灵活的系统扩展成为可能。

二、Scree为了取代数据访问层是如何思考的?做了哪些事情?

1、自动生成表

既然祭出了ORM的旗帜,那么class与表、object与数据行的对应关系维护就是基本需求。先看如下一段代码:

public enum NewsType
{
    Military = 0,
    World = 1,
    Society = 2,
    Culture = 3,
    Travel = 4,
}
public class News : SRO
{
    [StringDataType(IsNullable = false, Length = 50)]
    public string Title { get; set; }

    [StringDataType(IsMaxLength = true)]
    public string Context { get; set; }

    public string Author { get; set; }

    public NewsType Type { get; set; }

    public int ReadingQuantity { get; set; }

    static News()
    {
        TimeStampService.RegisterIdFormat<News>("xw{0:yyMMdd}{1}");
    }
}
  • Scree会自动将继承自SRO的类生成为数据库中同名的表(不支持配置不同的表名,这不是一个技术问题,应该来说最初是有这个设计的,在再三考虑下,取消这个支持。少就是多,在至简的名义追求最大的可用才是最美的。对于很多不支持的功能,不是不能支持,而是经过慎重考虑不予支持,下同,不在赘述)。

  • 仅支持SQL Server,如果确实有其他数据库的需求,请自行修改Scree.DataBase.SQLServer。

  • 支持数据类型为int、bool、string、DateTime、枚举、decimal、long、byte[],分别对应数据库中的int、bit、nvarchar或text、datetime、int、decimal、bigint、image。

  • 可用通过对属性设置Attribute来指定字段类型详细信息,详见Scree.Attributes。

  • string默认为nvarchar(32),decimal默认为decimal(18,4)。

  • 系统只会自动为新class创建对应的表,如果是字段有修改,则需要人工修改数据库。

  • SRO基类中提供了5个默认属性Id、CreatedDate、LastAlterDate、Version、IsDeleted,也就是所有scree的数据对象都会自带这5个字段,用途后面还会详解。

2、增删改查

对数据的基本操作,莫过于增删改查,下面演示如何通过操作对象方便的读写对应的数据。

  • 增加

    //如果直接new也是可以的,目前是等效的,建议统一使用CreateObject,未来可以利用CreateObject搞一些事情
    //News news = new News();
    News news = PersisterService.CreateObject();

    news.Title = “新闻标题”;
    news.Context = “新闻内容”;

    PersisterService.SaveObject(news);

  • 查询

    News obj = PersisterService.LoadObject(“新闻Id”);

  • 修改

    News obj = PersisterService.LoadObject(“新闻Id”, LoadType.DataBaseDirect);

    news.Title = “新的标题”;
    news.Context = “新的内容”;

    PersisterService.SaveObject(news);

  • 删除,仅支持逻辑删除,不会做物理删除,可以理解为删除本身也就是一种修改。逻辑删除的数据,通过框架查询时会自动屏蔽掉。

    News obj = PersisterService.LoadObject(“新闻Id”, LoadType.DataBaseDirect);
    news.IsDeleted = true;
    PersisterService.SaveObject(news);

本质上,增删改查只有两个动作:读和写。在scree中,单个对象读使用LoadObject,写使用SaveObject。在实际业务处理中,如果Load的对象是要用于修改后Save的,LoadObject的参数应该使用LoadType.DataBaseDirect(不从缓存加载的模式)。

3、读取一组对象

使用LoadObjects,可精确查找、可模糊查找、可排序。

internal static News[] GetNewsByType(NewsType type, LoadType loadType)
{
    List<IMyDbParameter> prams = new List<IMyDbParameter>();
    prams.Add(DbParameterProxy.Create("Type", SqlDbType.Int, (int)type));

    News[] objs = PersisterService.LoadObjects<News>("[Type]=@Type", prams.ToArray(), loadType);

    return objs;
}
internal static News[] GetNewsByAuthor(string author, LoadType loadType)
{
    List<IMyDbParameter> prams = new List<IMyDbParameter>();
    prams.Add(DbParameterProxy.Create("Author", SqlDbType.NVarChar, "%" + author + "%"));

    News[] objs = PersisterService.LoadObjects<News>("[Author] like @Author order by ReadingQuantity desc", 
    prams.ToArray(), loadType);

    return objs;
}

4、保存一组对象

默认为事务性保存。

News news = PersisterService.CreateObject<News>();

news.Title = "新闻标题";
news.Context = "新闻内容";

string remark = "增加新闻";
SystemLog systemLog = LogService.CreateSystemLog(SystemLogType.AddNews, typeof(News), news.Id, remark);

PersisterService.SaveObject(new SRO[] { news, systemLog });

5、自定义对象Id

新创建的对象,默认Id是Guid。

private string _id = Guid.NewGuid().ToString().Replace("-", "");

也可以自定义具有业务意义的Id(建议Id全局唯一。注意,是全局唯一,而不是单类型唯一)。Scree提供时间戳服务,可以确保Id全局唯一。

  • 自定义Id需要两步,首先注册Id的格式(这种注册格式大家应该很熟悉,就是格式化字符串的写法),引用时间戳服务提供的变量。

    public class News : SRO
    {
    static News()
    {
    TimeStampService.RegisterIdFormat(“xw{0:yyMMdd}{1}”);
    }
    }

  • 然后,给新创建的对象Id赋值(注意:Id是只读属性,只能使用SetId方法对新创建的对象赋值一次)。

    News news = PersisterService.CreateObject();
    news.SetId(TimeStampService.GetOneId());

RegisterIdFormat以及GetOneId高级使用可以详见代码注释。

6、对象与数据的映射原理

  • 先说读,前面提到读使用LoadObject

在框架内部,通过条件自动拼接出select的sql语句,从DB拉取到数据后对对象属性进行反射,逐一赋值。

  • 再说写,写统一使用SaveObject

Scree通过SRO基类的IsNew属性维护对象的新老状态。新创建的对象IsNew=true,而通过LoadObject拉取到的对象IsNew=false,框架通过判断IsNew,分别拼接出insert或update的sql。如果是一组对象同时save,则是循环前述过程。

7、对象版本

Scree通过SRO基类的Version属性提供对象版本维护的能力。这项能力非常重要,建议无论是否使用ORM,无论使用什么样的框架与开发模式,都应该对对象(或者说行数据)增加版本号。

  • 每一个新创建的对象其Version默认为0(第一次insert进入数据库中,Version维持为0)

  • 对象每一次update操作后,其Version自动+1

  • 对象update sql会自动加上当前Version的where条件,以保证不会出现数据污染

8、CreatedDate与LastAlterDate

CreatedDate是对象第一次创建的时间,永不再变化。LastAlterDate是对象最后一次被修改的时间,每一次update都会改变。

9、充血模型与贫血模型

推荐使用贫血模型。这方面的争论太多了,个人认为理论和工程是两码事,让理论的归理论,工程的归工程吧。简单、层次结构清楚、工程师易于理解和使用在工程实际中太重要了。

三、现在开始使用scree

启动scree框架,只需要一行代码,在Global.asax中

protected void Application_Start(object sender, EventArgs e)
{
    ServiceRoot.Init();
}

在Test文件夹中,提供了两个示例项目:

  • SimpleExample,简单示例,一般小型应用(单服务器或少量服务器或单表数据量在百万内),使用基本用法即可满足需求了

  • AdvancedExample,高级示例

启动scree框架之前,需要有两个步骤,下面以SimpleExample为例。

1、引用程序集

  • Scree.Attributes

  • Scree.Cache

  • Scree.Common

  • Scree.Core.IoC

  • Scree.DataBase

  • Scree.Lock

  • Scree.Log

  • Scree.Persister

  • Scree.Syn

2、添加配置文件

在应用程序的根目录新建名为config的文件夹,并添加文件

  • log4net.config

  • mapping.config

  • root.config

  • storage.config

配置文件的内容可以详见示例项目。

示例项目中dbbak文件夹中是对应的数据库备份,初次观摩scree框架,建议直接使用备份数据库,以利于快速上手。

四、Scree的高阶能力

1、视图支持

虽然,我一般不建议使用视图,但某些情境下,视图确实还有积极的意义。在这里,可以通过一个窍门的方式来读取视图的数据,把视图当成一张表即可。前面说到,Scree会自动将继承自SRO的类映射成DB中同名的表。例如对于视图vwNewsForUser,可以定义类型:

public class vwNewsForUser : SRO
{

}

读取视图的数据同样可以使用到LoadObject或LoadObjects,也就是说在读上面等同于数据对象。

vwNewsForUser obj = PersisterService.LoadObject<vwNewsForUser>("视图数据Id");

显然,视图中必须存在SRO基类默认的5个字段。不过,这并不是问题,视图总归是从数据对象表之间关联而来,必然会有一张表是关联关系中的核心表,视图5个字段就使用该对象的即可。

2、缓存

框架为对象或对象数组提供本地缓存功能,只需要配置即可。

  • 第一步,root.config中需要配置启用缓存服务,如下:

  • 第二步,在cache.config中配置指定类型的缓存参数,单个对象或一组对象都可以,示例如下:


LoadObject或LoadObjects均有LoadType的参数,默认优先读取缓存数据。框架未提供分布式缓存注入功能,如果需要使用MC或Redis等,需自行修改Scree.Cache.CacheService。

3、BeforeSave和AfterSave

  • 对于单个对象,在持久化之前和之后均可以搞一些事情。

    public class SystemLog : SRO
    {
    protected override void BeforeSave()
    {
    //可以在这里搞一些事情
    }

    protected override void AfterSave()
    {
        //可以在这里搞一些事情
    }
    

    }

  • 在调用PersisterService.SaveObject持久化单个(或一组对象)的之前和之后也可以搞一些事情。PersisterService提供了注入接口。

    public delegate void BeforeSave(SRO[] objs);
    public delegate void AfterSave(SRO[] objs);

    void RegisterBeforeSaveMothed(BeforeSave beforeSave);
    void RegisterAfterSaveMothed(AfterSave afterSave);

4、分库

  • storage.config用于配置每一个数据库的连接信息,例如:


    127.0.0.1
    AdvancedExample
    dbname
    dbpassword
    true
    60
    1
    100

推荐源码