欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

一个类GraphQL的ORM数据访问框架发布

程序员文章站 2022-04-11 20:58:51
Zongsoft.Data 是一个类 GraphQL 风格的 ORM(Object/Relational Mapping) 数据访问框架。很高兴我们的 ORM 数据访问框架在历经两个 SaaS 产品的应用之后,今天正式宣布对外推广! ......

zongsoft.data 发布公告

很高兴我们的 orm 数据访问框架(zongsoft.data)在历经两个 saas 产品的应用之后,今天正式宣布对外推广!
这是一个类 graphql 风格的 orm(object/relational mapping) 数据访问框架。

又一个*?

在很长时间里,.net 阵营似乎一直缺乏一个被普遍使用的 orm 数据访问框架,从最早的原生 ado.net 到舶来品 ibatis.nethibernate.net,后来又经历了 linq for sql 与 entity framework 的混战,可能是因为 entity framework 早期版本的模糊定位和反复变更的设计导致了它失之霸主之位,进而造就了一段百舸争流、群雄共逐的战国时代。在历经漫长而反复的期待、失望、纠结和痛苦之后,我终于决定动手造一个*。

设计理念

在开始动手之前,先确定以下基本设计原则:

  • 数据库优先(database first)
  • 严格的 poco/pojo 支持
  • 映射模型与代码完全隔离
  • 禁止业务层出现 sql 和类 sql 代码

在一个业务系统中,数据结构及其关系毋庸置疑是最底层的基础性结构,数据库应由系统架构师或开发负责人进行仔细设计 no schema/weakly schema 的思潮是涂抹了蜂蜜的毒药),数据访问映射以数据库表结构关系为基石,在此之上业务层亦以概念映射模型为准绳,层级之间相互隔离。

领域模型实体避免通过注解 (标签) 来进行元数据定义,应确保严格符合 poco/pojo 范式。通过语义化的 schema 来声明访问的数据结构关系,禁止应用层的 sqllinq 式的类 sql 代码可降低业务层对数据层的依赖、提升代码可维护性外,还具备更加统一可控的便利性,并为数据访问引擎的实现提供了更大的优化空间和*度。

范例说明

下面通过三个的例子 (注:例子均基于 zongsoft.community 项目) 来佐证上面的部分设计理念,更多示例和阐述请参考 zongsoft.data 项目的 readme.md 文档和 zongsoft.community 项目的代码。

提示: 下面的范例均基于 zongsoft.community 开源项目,该项目是一个完整的论坛社区的后台程序。你可能需要预先阅读一下该项目的《数据库表结构设计》文档,以便更好的理解范例代码的业务逻辑。

示例一

导航查询及导航过滤

var forums = this.dataaccess.select<forum>(
    condition.equal("siteid", this.user.siteid) &
    condition.in("visibility", visibility.internal, visibility.public) |
    (
        condition.equal("visibility", visibility.specified) &
        condition.exists("users",
                  condition.equal("userid", this.user.userid) &
                  (
                      condition.equal("ismoderator", true) |
                      condition.notequal("permission", permission.none)
                  )
        )
    ),
    "*, mostrecentthread{threadid,title,creator{name,nickname,avatar}}"
);

上述数据访问的查询方法大致生成如下sql脚本:

select
    t.*,
    t1.threadid as 'mostrecentthread.threadid',
    t1.title as 'mostrecentthread.title',
    t1.creatorid as 'mostrecentthread.creatorid',
    t2.userid as 'mostrecentthread.creator.userid',
    t2.name as 'mostrecentthread.creator.name',
    t2.nickname as 'mostrecentthread.creator.nickname',
    t2.avatar as 'mostrecentthread.creator.avatar'
from forum t
    left join thread as t1 on
        t.mostrecentthreadid=t1.threadid
    left join userprofile as t2 on
        t1.creatorid=t2.userid
where
    t.siteid = @p1 and
    t.visibility in (@p2, @p3) or
    (
        t.visibility = @p4 and
        exists
        (
            select u.siteid, u.forumid, u.userid
            from forumuser u
            where u.siteid = t.siteid and
                  u.forumid = t.forumid and
                  u.userid = @p5 and
                  (
                      u.ismoderator = @p6 or
                      u.permission != @p7
                  )
        )
    );

上述示例通过 select 查询方法的 schema 参数 (即值为 *, mostrecentthread{threadid,title,creator{name,nickname,avatar}} 的参数) 从数据结构关系的层次指定了查询数据的形状,因而不再需要 sql 或类 sql 语法中 join 这样命令式的语法元素,它不光提供了更简洁且语义化的 api 访问方式,而且还给数据访问引擎底层提供了更大的优化空间和*度。

如果将 select 查询方法的 schema 参数值改为 *,moderators{*},mostrecentthread{threadid,title,creator{name,nickname,avatar}} 后,数据访问引擎会将查询内部分解为一对多的两条 sql 语句进行迭代执行,而这些都不需要业务层进行分拆处理,因而提升了效率并降低了业务层的复杂度。

注:schema 模式表达式通过 web api 提供给前端应用,将大大减少后端开发的工作量,提升前后端的工作效率。

示例二

一对多的关联新增

// 构建待新增的实体对象
var forum = new
{
    siteid = this.user.siteid,
    groupid = 100,
    name = "xxxx",

    // 一对多的导航属性
    users = new forumuser[]
    {
      new forumuser { userid = 1001, ismoderator = true },
      new forumuser { userid = 1002, permission = permission.read },
      new forumuser { userid = 1003, permission = permission.write },
    }
}

// 执行数据新增操作
this.dataaccess.insert<forum>(forum, "*, users{*}");

上述数据访问的新增方法大致生成如下sql脚本:

/* 主表插入语句,执行一次 */
insert into forum (siteid,forumid,groupid,name,...) values (@p1,@p2,@p3,@p4,...);

/* 子表插入语句,执行多次 */
insert into forumuser (siteid,forumid,userid,permission,ismoderator) values (@p1,@p2,@p3,@p4,@p5);

上述示例通过 insert 新增方法的 schema 参数(即值为 *,user{*} 的参数)指定了新增数据的形状,由数据访问引擎根据映射定义自动处理底层的 sql 执行方式,确保业务层代码的简洁和更高的执行效率。

示例三

一对一和一对多的关联更新,对于“一对多”的导航属性,还能确保该属性值 (集合类型) 以 upsert 模式写入。

public bool approve(ulong threadid)
{
    //构建更新的条件
    var criteria =
        condition.equal(nameof(thread.threadid), threadid) &
        condition.equal(nameof(thread.approved), false) &
        condition.equal(nameof(thread.siteid), this.user.siteid) &
        condition.exists("forum.users",
            condition.equal(nameof(forum.forumuser.userid), this.user.userid) &
            condition.equal(nameof(forum.forumuser.ismoderator), true));

    //执行数据更新操作
    return this.dataaccess.update<thread>(new
    {
        approved = true,
        approvedtime = datetime.now,
        post = new
        {
            approved = true,
        }
    }, criteria, "*,post{approved}") > 0;
}

上述数据访问的更新方法大致生成如下sql脚本:

/* 以下代码为支持 output/returning 子句的数据库(如:sqlserver,oracle,postgresql) */

/* 根据更新的关联键创建临时表 */
create table #tmp
(
    postid bigint not null
);

/* 更新主表,并将更新的关联键输出到内存临时表 */
update t set
    t.[approved]=@p1,
    t.[approvedtime]=@p2
output deleted.postid into #tmp
from [community_thread] as t
    left join [community_forum] as t1 on /* forum */
        t1.[siteid]=t.[siteid] and
        t1.[forumid]=t.[forumid]
where
    t.[threadid]=@p3 and
    t.[approved]=@p4 and
    t.[siteid]=@p5 and exists (
        select [siteid],[forumid]
        from [community_forumuser]
        where [siteid]=t1.[siteid] and
              [forumid]=t1.[forumid] and
              [userid]=@p6 and
              [ismoderator]=@p7
    );

/* 更新关联表 */
update t set
    t.[approved]=@p1
from [community_post] as t
where exists (
    select [postid]
    from #tmp
    where [postid]=t.[postid]);

上述示例通过 update 更新方法的 schema 参数(即值为 *,post{approved} 的参数)指定了更新数据的形状,数据访问引擎将根据数据库类型生成高效的 sql 语句,对于业务层而言这一切都是无感的、透明的。

对于一对多的导航属性,数据访问引擎默认将以 upsert 模式处理子集的写入,关于 upsert 更多信息请参考 zongsoft.data 项目文档。

性能

我们希望提供最佳的综合性价比,对于一个 orm 数据访问引擎来说,性能的关注点主要 (不限) 有这些要素:

  1. 生成简洁高效的 sql 脚本,并尽可能利用特定数据库的最新 sql 语法;
  2. 数据查询结果的实体组装(populate)过程必须高效;
  3. 避免反射,有效的语法树缓存。

实现层面我们采用 emitting 动态编译技术对实体组装(populate)、数据参数绑定等进行预热处理,可查阅 datapopulator 等相关类的源码深入了解。

其他

得益于 “以声明方式来表达数据结构关系” 的语义化设计理念,相对于命令式设计而言,它使得程序意图更加聚焦,天然地对底层数据的表达和优化更加宽容与*。

更多详细内容 (譬如:读写分离、继承表、数据模式、映射文件、过滤器、验证器、类型转换、数据隔离) 请查阅相关文档。

支持赞助

我们欢迎并期待任何形式的推广支持!

如果你认同我们的设计理念请为这个项目点赞(star),如果你认为该项目很有用,并且希望支持它未来的发展,请给予必要的资金来支持它:

  1. 关注 zongsoft 微信公众号,对我们的文章进行打赏;
  2. 加入 zongsoft 知识星球圈,可以获得在线问答和技术支持;
  3. 如果您的企业需要现场技术支持与辅导,又或者需要开发新功能、即刻的错误修复等请给我。

一个类GraphQL的ORM数据访问框架发布


提醒: 本文可能会更新,请阅读原文:,以避免因内容陈旧而导致的谬误,同时亦有更好的阅读体验。