NopORM 教程:支持企业级SaaS应用的下一代ORM 框架

什么是 NopORM?

NopORM 是基于可逆计算理论设计的下一代 ORM 框架,它不仅提供了类似 JPA/Hibernate 的完整 ORM 功能,还内置了对 OLAP 分析查询的支持,实现了类似润乾 DQL 语言的多维查询能力。相比于传统 ORM 框架,NopORM 在保持易用性的同时,解决了企业级应用中的复杂查询、性能优化和系统扩展性等痛点。

核心特性概览

1. 统一的 SQL 管理

<!-- 在 sql-lib.xml 中统一管理 SQL/EQL/DQL -->
<sql-lib>
    <sqls>
        <sql name="findActiveUsers">
            select * from nop_auth_user where status = 1
        </sql>
        
        <eql name="findUserWithDepartment">
            select u, u.department.name 
            from NopAuthUser u 
            where u.department.status = 1
        </eql>
        
        <query name="userAnalysis">
            <sourceName>NopAuthUser</sourceName>
            <fields>
                <field name="department.name"/>
                <field name="status" aggFunc="count" alias="userCount"/>
            </fields>
            <groupBy>
                <field name="department.name"/>
            </groupBy>
        </query>
    </sqls>
</sql-lib>

2. 声明式数据访问

与 MyBatis 的命令式风格不同,NopORM 采用声明式数据访问:

// MyBatis - 需要显式调用 update
user.setName("newName");
userMapper.update(user);

// NopORM - 修改即更新
user.setName("newName");
// 自动检测变更,在 session.flush() 时生成优化后的 UPDATE 语句

3. 对象查询语言 (EQL)

EQL = SQL + AutoJoin,支持复杂的对象导航:

// 复杂关联自动转换为 JOIN 查询
select o from Order o 
where o.customer.address.city = 'Beijing' 
  and o.items.product.category.name = 'Electronics'

// 多对多关联查询
select b from Book b 
where b.authors.name like '张%'

快速开始

1. 添加依赖

<dependency>
    <groupId>io.github.entropy-cloud</groupId>
    <artifactId>nop-orm</artifactId>
    <version>2.0.0</version>
</dependency>

2. 配置数据源

nop:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test
    username: root
    password: 123456

3. 定义数据模型并生成实体

第一步:在模型文件中定义实体

/src/main/resources/_vfs/ 目录下创建 orm.xml 或使用 Excel 格式的 orm.xlsx

<!-- orm.xml -->
<orm x:schema="/nop/schema/orm/orm.xdef" xmlns:x="/nop/schema/xdsl.xdef">
    <entities>
        <entity name="app.NopAuthUser" tableName="nop_auth_user">
            <columns>
                <column name="sid" code="SID" stdDomain="id" primaryKey="true"/>
                <column name="user_name" code="USER_NAME" stdDomain="string" length="50"/>
                <column name="email" code="EMAIL" stdDomain="email" length="100"/>
            </columns>
            <relations>
                <to-one name="department" refEntityName="app.NopAuthDepartment"/>
                <to-many name="roles" refEntityName="app.NopAuthUserRole" collectionType="set"/>
            </relations>
        </entity>
    </entities>
</orm>

第二步:使用nop-cli生成代码

# 执行代码生成
java -jar nop-cli.jar gen model/demo.orm.xml "-o=."

第三步:使用生成的实体

// 生成的实体类位于 target/generated-sources 目录
// 实体类名如:NopAuthUser.java 等

// 创建实体实例
NopAuthUser user = new NopAuthUser();
user.setUserName("zhangsan");
user.setEmail("zhangsan@example.com");

4. 基本 CRUD 操作

@Inject
IDaoProvider daoProvider;

// 获取DAO实例
IEntityDao<NopAuthUser> dao = daoProvider.daoFor(NopAuthUser.class);
// 创建
NopAuthUser user = dao.newEntity();
user.setUserName("zhangsan");
user.setEmail("zhangsan@example.com");
dao.saveEntity(user);

// 查询
NopAuthUser user = dao.getEntityById("1");

NopAuthUser example = new NopAuthUser();
example.setStatus(1);
List<NopAuthUser> users = dao.findAllByExample(example);

// 复杂查询
QueryBean query = new QueryBean();
query.addFilter(and(
    eq(NopAuthUser.PROP_NAME_userName, "zhangsan"),
    gt(NopAuthUser.PROP_NAME_createTime, startDate)
)).limit(100);
List<NopAuthUser> users = dao.findPageByQuery(query);

// 更新 - 无需显式调用 update
user.setEmail("new@example.com");
// 自动在 session.flush() 时同步到数据库

// 删除
dao.deleteEntity(user);

高级特性详解

1. 扩展字段支持

无需修改数据库表结构即可添加扩展字段:

<orm>
  <x:post-extends>
    <orm-gen:DefaultPostExtends xpl:lib="/nop/orm/xlib/orm-gen.xlib"/>
  </x:post-extends>
  
  <entities>
	<!-- 在 orm.xml 模型中标记 use-ext-field -->
	<entity name="app.MyEntity" tableName="my_entity" tagSet="use-ext-field">
		<aliases>
			<alias name="extFieldA" propPath="extFields.fldA.string" type="String"/>
			<alias name="extFieldB" propPath="extFields.fldB.int" type="Integer"/>
		</aliases>
	</entity>
  </entities>
</orm>  

使用扩展字段与普通字段完全一致:

// Java 代码
entity.setExtFieldA("value");
String value = entity.getExtFieldA();

// EQL 查询
select o from MyEntity o where o.extFieldA = 'value' order by o.extFieldB

// GraphQL 查询
query {
    MyEntity_list {
        extFieldA
        extFieldB
    }
}

2. DQL 多维查询

面向 OLAP 的查询,简化复杂数据分析:

QueryBean query = new QueryBean();
query.setSourceName("NopAuthUser");
query.fields(
    mainField("department.name"),
    subField("roles", "roleId").count().alias("roleCount"),
    mainField("status")
);
query.addOrderField("department.name", true);

List<Map<String, Object>> result = ormTemplate.findListByQuery(query);

对应的 SQL 需要复杂的 JOIN 和子查询,而 DQL 自动处理:

  • 自动识别关联路径
  • 内存中的 Hash Join 优化
  • 支持分页和复杂过滤

3. 逻辑删除和审计

通过stdDomain标记特殊字段:

<entity name="app.MyEntity">
    <columns>
        <column name="delFlag" stdDomain="delFlag" ... />
        <column name="delVersion" stdDomain="delVersion" .../>
	<column name="updatedBy" stdDomain="updater" .../>
    </columns>
</entity>
// 自动处理逻辑删除
dao.deleteEntity(user); // 实际设置 deleted=1

// 自动审计字段  - 自动设置,无需手动调用
// entity.setUpdateBy(currentUser); // 自动设置
// entity.setUpdateTime(new Date()); // 自动设置

4. 多数据源支持

在实体定义中增加querySpace属性

<!-- 在orm.xml实体定义中配置querySpace -->
<entity name="app.NopAuthUser" tableName="nop_auth_user" querySpace="main">
    <!-- 实体定义 -->
</entity>

<entity name="app.ReportEntity" tableName="report_data" querySpace="report">
    <!-- 实体定义 -->
</entity>

在ioc配置中增加数据源定义

<!-- 配置多个数据源 -->
<bean id="nopDataSource_main" class="com.zaxxer.hikari.HikariDataSource">
    <!-- 主数据源配置 -->
</bean>

<bean id="nopDataSource_report" class="com.zaxxer.hikari.HikariDataSource">
    <!-- 报表数据源配置 -->
</bean>

访问实体时会自动使用不同的querySpace

5. 字段级安全控制

<!-- 字段掩码配置 -->
<prop name="creditCard" ui:maskPattern="6*4">
    <schema type="String"/>
</prop>

<!-- 字段加密 -->
<column name="secret_data" stdDomain="encrypted"/>

6. 实体变更拦截器

/{moduleId}/orm/app.orm-interceptor.xml中监听每个实体的pre-update/pre-save等事件。

<interceptor x:schema="/nop/schema/orm/orm-interceptor.xdef" 
             xmlns:x="/nop/schema/xdsl.xdef"
             xmlns:c="c">
    
    <!-- 用户实体的拦截逻辑 -->
    <entity name="app.NopAuthUser">
        <pre-update id="trackUserChange">
            <source>
                <!-- 记录用户信息变更 -->
                <c:if test="${entity.orm_propDirtyByName('email')}">
                    <log:info message="用户邮箱变更: userId=${entity.sid}, 
                        oldEmail=${entity.orm_propOldValueByName('email')}, newEmail=${entity.email}" />
                </c:if>
                
                <c:if test="${entity.orm_propDirtyByName('status')}">
                    <log:info message="用户状态变更: userId=${entity.sid}, 
                        oldStatus=${entity.orm_propOldValueByName('status')}, newStatus=${entity.status}" />
                    
				   <c:script>
                     const notifyService = inject('userNotifyService');
                     notifyService.sendStatusChangeNotify(entity, 
                         entity.orm_propOldValueByName('status'), entity.status);
                   </c:script>
                </c:if>
            </source>
        </pre-update>
    </entity>
</interceptor>

与主流 ORM 框架对比

与 MyBatis 的对比

特性 MyBatis NopORM
数据访问模式 命令式 声明式
缓存机制 二级缓存 Session 级一级缓存 + 业务缓存
关联查询 手动配置 ResultMap 自动对象导航
动态 SQL XML 标签有限 XPL 模板语言,支持自定义标签
扩展性 有限 Delta 定制,无需修改源码
OLAP 支持 需要手动编写复杂 SQL 内置 DQL 多维查询

MyBatis 示例:

// 需要显式调用 update
user.setName("newName");
userMapper.update(user);

// 复杂查询需要编写 XML
public interface UserMapper {
    List<User> findComplexUsers(@Param("filter") UserFilter filter);
}

NopORM 示例:

// 声明式,修改自动跟踪
user.setName("newName");

// 复杂查询统一管理
List<User> users = sqlLibManager.invoke("findComplexUsers", params);

与 JPA/Hibernate 的对比

特性 JPA/Hibernate NopORM
实体定义 注解或XML配置 模型文件 + 代码生成
代码生成 运行时AOP增强 编译时一次性生成,性能更好
查询语言 JPQL/HQL,功能有限 EQL = SQL + AutoJoin,支持完整 SQL 语法
关联查询 受限的 JOIN 语法 任意表之间的 JOIN,支持复杂关联条件
动态 SQL 需要 Criteria API,代码冗长 统一的 sql-lib 管理,支持 XPL 模板语言
扩展机制 有限的拦截器机制 Delta 定制,无需修改源码即可扩展功能
OLAP 支持 需要原生 SQL 或第三方库 内置 DQL 多维查询语言

JPA/Hibernate 复杂查询:

// Criteria API 冗长难维护
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
List<Predicate> predicates = new ArrayList<>();

if (StringUtils.hasText(name)) {
    predicates.add(cb.like(root.get("name"), "%" + name + "%"));
}
// ... 更多条件
query.where(predicates.toArray(new Predicate[0]));

NopORM 复杂查询:

<!-- sql-lib 中声明式管理 -->
<eql name="findUsers">
    <source>
        select u from User u 
        where 1=1
        <c:if test="${!_.isEmpty(name)}">
            and u.name like ${'%' + name + '%'}
        </c:if>
        <app:CustomFilter/> <!-- 自定义标签复用逻辑 -->
    </source>
</eql>

与 Spring Data JPA 的对比

特性 Spring Data JPA NopORM
开发范式 Repository 抽象,方法名派生查询 统一的 DAO 模式 + 声明式查询
实体生成 手工编写或Lombok注解 模型驱动,自动代码生成
查询构建 方法名约定、@Query 注解、Specification QueryBean、EQL、sql-lib 统一管理
动态查询 Specification API 复杂冗长 QueryBean 类型安全,模板动态生成
关联处理 懒加载 + N+1 问题常见 批量属性加载,自动优化关联查询
扩展机制 自定义 Repository 实现 Delta 定制,无需代码修改

Spring Data JPA Repository:

public interface UserRepository extends JpaRepository<User, Long> {
    // 方法名派生查询
    List<User> findByNameAndStatus(String name, Integer status);
    
    // 复杂查询需要 Specification
    Specification<User> hasRole(String roleName) {
        return (root, query, cb) -> 
            cb.isMember(roleName, root.get("roles"));
    }
}

NopORM 统一 DAO:

// 统一的 DAO 接口
IEntityDao<User> userDao = daoProvider.daoFor(User.class);

// 简单查询
User example = userDao.newEntity();
example.setStatus(1);
List<User> users = userDao.findAllByExample(example);

// 复杂查询
QueryBean query = new QueryBean();
query.addFilter(and(
    eq(User.PROP_NAME_status, 1),
    like(User.PROP_NAME_department + ".name", "IT%")
)).limit(100);
List<User> users = userDao.findPageByQuery(query);

性能优化特性

1. 智能 Session 管理

// 延迟获取数据库连接
@SingleSession
@Transactional
public void businessMethod() {
    // 先执行非数据库操作
    processBusinessLogic();
    
    // 需要时才获取连接
    User user = dao.getEntityById("1");
    // 自动在方法结束时 flush
}

2. 批量操作优化

// 自动 JDBC Batch
for (int i = 0; i < 1000; i++) {
    User user = dao.newEntity();
    user.setName("user" + i);
    dao.saveEntity(user);
}
// 自动合并为 Batch INSERT

3. 关联属性批量加载

List<User> users = dao.findAll();
// 一次性加载所有关联属性,避免 N+1 查询
dao.batchLoadProperties(users, Arrays.asList(
    "department", 
    "orders", 
    "orders.items",
    "department.company"
));

架构理念差异

传统 ORM 框架

  • 命令式编程:需要显式调用 save/update 方法
  • 配置分散:注解、XML、代码配置混杂
  • 有限扩展:修改需要重新编译部署
  • 查询复杂:复杂查询需要编写冗长代码

NopORM 基于可逆计算理论

  • 声明式编程:修改自动跟踪,无需显式 save
  • 模型驱动:ORM 数据模型作为唯一事实源生成前后端完整代码
  • 差量定制:定制化开发无需修改原有代码
  • 统一抽象:SQL、EQL、DQL 统一管理

实际应用场景

1. 电商订单分析

QueryBean query = new QueryBean();
query.setSourceName("Order");
query.fields(
    mainField("createTime", "day"),  // 按天分组
    mainField("customer.region"),
    subField("items", "amount").sum().alias("totalAmount"),
    subField("items", "id").count().alias("orderCount")
);
query.addFilter(gt("createTime", startDate));

List<Map<String, Object>> salesReport = ormTemplate.findListByQuery(query);

2. 权限管理系统

// 查找有特定权限的用户
select u from NopAuthUser u 
where u.roles.permissions.resource.name = '/api/admin'
  and u.department.name = '技术部'

// 自动处理逻辑删除、数据权限过滤

3. 实体修改状态跟踪

// 判断字段是否被修改
if (entity.orm_propDirty(propId)) {
    Object oldValue = entity.orm_propOldValue(propId);
    Object newValue = entity.orm_propValue(propId);
}

// 获取所有修改的字段
Map<String, Object> dirtyOldValues = entity.orm_dirtyOldValues();
Map<String, Object> dirtyNewValues = entity.orm_dirtyNewValues();

4. 启用租户

<entity name="app.BizOrder" tableName="biz_order" tenantProp="tenantId" useTenant="true" >
    <columns>
        <column name="sid" code="SID" stdDomain="id" primaryKey="true"/>
        <column name="tenantId" code="TENANT_ID" stdDomain="string" length="50" notNull="true"/>
	...
    </columns>
</entity>    

5. 分库分表支持

NopORM支持按照shardProp的值确定分库以及分表:

<entity name="app.ShardEntity" tableName="shard_table" shardProp="tenantId">
    <!-- 实体定义 -->
</entity>
// 实现IShardSelector接口
public class MyShardSelector implements IShardSelector {
    public ShardSelection selectShard(String entityName, String shardProp, Object shardValue) {
        // 根据分片值选择数据源和表名
        return new ShardSelection("querySpace_" + shardValue, "shard_" + shardValue);
    }
}
### 迁移建议

**从 MyBatis 迁移:**
- 保持现有的 SQL 知识,在 sql-lib 中统一管理
- 逐步将命令式代码改为声明式数据访问
- 利用 EQL 简化复杂的关联查询

**从 JPA 迁移:**
- 将 JPQL 查询转换为更强大的 EQL
- 利用批量加载解决 N+1 问题
- 使用 Delta 定制替代复杂的拦截器配置

**从 Spring Data JPA 迁移:**
- 将 Repository 转换为统一的 DAO + sql-lib
- 用 QueryBean 替代复杂的 Specification
- 享受自动 GraphQL API 生成的好处

## 总结

NopORM 通过创新的设计解决了传统 ORM 在企业级应用中的痛点:

1. **模型驱动开发** - 在模型文件中定义数据结构,自动生成实体代码
2. **编译时优化** - 预生成所有代码,避免运行时AOP开销,性能更好
3. **声明式编程模型** - 修改即更新,减少样板代码
4. **统一查询抽象** - SQL、EQL、DQL 统一管理,适应不同场景
5. **无侵入扩展** - Delta 定制实现模型扩展,提升可维护性
6. **性能优化** - 自动 Batch、延迟加载、内存 Join 等
7. **企业级特性** - 多租户、逻辑删除、审计、分库分表等开箱即用

对于需要处理复杂业务逻辑和数据分析的企业应用,NopORM 提供了比传统 ORM 更强大和灵活的解决方案。其基于可逆计算理论的设计理念,使得系统能够在不断演进的过程中保持架构的清晰和可维护性,特别适合快速发展的互联网企业和需要高度定制化的企业应用场景。

## 了解更多

- [官方文档](https://gitee.com/canonical-entropy/nop-entropy)
- [示例项目](https://gitee.com/canonical-entropy/nop-entropy/nop-demo/nop-orm-demo)
- [视频教程](https://space.bilibili.com/3493261219990250)
- [社区交流](https://gitee.com/canonical-entropy/nop-entropy/issues)