ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

数据库操作规范(学习笔记)

数据库操作规范(学习笔记) 数据库操作规范学习笔记本文档基于行业最佳实践覆盖数据访问层与事务管理的核心规范。适用于所有涉及数据持久化的后端开发场景。1. 数据访问层规范1.1 技术选型边界ORM 框架如 MyBatis-Plus与原生 SQL 并非互斥关系应根据场景选择最合适的方案场景推荐方案原因单表 CRUDORM 框架 APILambda Query/Update类型安全自动处理逻辑删除、租户过滤等横切关注点多表关联原生 SQL注解或 XMLJOIN、子查询、NOT EXISTS 等 ORM 无法优雅表达动态分页框架分页插件 统一分页请求体避免手动拼接 LIMIT/OFFSET批量操作框架批量方法分批提交避免单次 SQL 过长核心原则ORM 是单表神器多表关联老老实实写 SQL。两者互补不是替代关系。1.2 Lambda 查询规范单表首选优先使用类型安全的 Lambda API避免硬编码字段名字符串// ✅ 推荐类型安全字段改名时编译报错mapper.lambdaQuery().eq(Entity::getCode,code).ne(Entity::getId,id).count();// ❌ 避免硬编码字段名重构时容易遗漏mapper.selectCount(newQueryWrapperEntity().eq(code,code));动态条件拼接利用第一个 boolean 参数控制是否拼接避免繁琐的 if-elsemapper.lambdaQuery().eq(StringUtils.hasText(req.getCode()),Entity::getCode,req.getCode()).eq(req.getEnabled()!null,Entity::getEnabled,req.getEnabled()).list();嵌套 OR 条件当需要(A AND B) OR (C AND D)结构时使用nested嵌套mapper.query().and(outer-{for(inti0;iconditions.size();i){if(i0)outer.or();outer.nested(n-n.eq(field_a,conditions.get(i).getA()).eq(field_b,conditions.get(i).getB()));}}).list();1.3 原生 SQL 规范多表关联当涉及 JOIN、子查询等 ORM 无法表达的操作时使用原生 SQL注解方式适合简单、参数固定的查询Select( SELECT t1.id, t1.name, t2.code FROM table_a t1 LEFT JOIN table_b t2 ON t1.id t2.a_id WHERE t1.status #{status} AND t1.deleted 0 AND t2.deleted 0 ORDER BY t1.seq )ListResultDtoqueryWithJoin(Longstatus);XML 方式适合复杂、动态条件的查询selectidqueryWithConditionresultTypeResultDtoSELECT t1.id, t1.name FROM table_a t1wheret1.deleted 0iftestname ! null and name ! AND t1.name LIKE CONCAT(%, #{name}, %)/ififteststatus ! nullAND t1.status #{status}/if/where/select强制规则原生 SQL 不经过 ORM 的自动逻辑删除每张表必须手动加deleted 0使用参数占位符#{param}禁止拼接 SQL 字符串SQL 较长时使用 Text Block或 XML 保持可读性明确列出需要的字段禁止SELECT *1.4 SQL 放置位置决策指南当确定需要写原生 SQL 后还有一个选择注解Select还是 XML决策矩阵需要写 SQL ├── 参数固定、SQL ≤ 20 行 → Select 注解 Text Block ├── 需要动态条件if、choose→ XML Mapper ├── 需要复用 SQL 片段sql→ XML Mapper ├── 多数据库适配不同方言→ XML Mapper databaseId └── 单表操作 → 不需要 SQL用 ORM Lambda API三种方式对比维度ORM Lambda APISelect 注解XML Mapper适用场景单表 CRUD简单多表查询复杂/动态查询动态条件天然支持需script包裹很丑原生支持if/chooseSQL 复用不支持不支持sql片段复用可读性高链式调用中Text Block高语法高亮IDE 支持无 SQL 提示部分 IDE 支持SQL 插件完整支持多数据库适配框架自动处理不支持方言切换databaseId原生支持维护成本低中中多一个文件实际建议项目初期 / 小团队Select注解足够减少文件数量SQL 和方法挨着看SQL 超过 30 行或需要动态条件迁移到 XML不要硬撑在注解里需要多数据库兼容必须用 XML见 1.5 节同一 SQL 被多个方法复用用 XML 的sql片段提取1.5 多数据库适配规范当系统需要同时支持多种数据库如 MySQL 达梦 PostgreSQL时需要从DDL、SQL 方言、驱动配置三个层面处理。1.5.1 核心策略ORM 屏蔽 方言隔离┌─────────────────────────────────────────┐ │ Service / Business │ ├─────────────────────────────────────────┤ │ 单表操作 → ORM Lambda API无方言差异 │ │ 多表操作 → XML Mapper databaseId │ ├──────────┬──────────┬───────────────────┤ │ MySQL │ 达梦 │ PostgreSQL ... │ └──────────┴──────────┴───────────────────┘核心思路单表操作ORM 框架自动处理方言差异分页、转义等开发者无需关心多表 SQL通过 XML 的databaseId为不同数据库写不同版本的 SQLDDL各数据库独立维护建表脚本不试图写通用 DDL1.5.2 MyBatis databaseId 机制配置MybatisPlusConfig或MybatisSqlSessionFactoryBean注册DatabaseIdProviderBeanpublicDatabaseIdProviderdatabaseIdProvider(){VendorDatabaseIdProviderprovidernewVendorDatabaseIdProvider();PropertiespropsnewProperties();props.setProperty(MySQL,mysql);props.setProperty(DM DBMS,dm);// 达梦props.setProperty(PostgreSQL,pg);provider.setProperties(props);returnprovider;}XML 中使用databaseId区分方言!-- MySQL 版本 --selectidpageQuerydatabaseIdmysqlresultTypeResultDtoSELECT id, name FROM tb_xxx WHERE deleted 0 LIMIT #{offset}, #{size}/select!-- 达梦版本 --selectidpageQuerydatabaseIddmresultTypeResultDtoSELECT id, name FROM tb_xxx WHERE deleted 0 LIMIT #{size} OFFSET #{offset}/select!-- 无 databaseId 的版本兜底当 databaseId 不匹配时使用 --selectidpageQueryresultTypeResultDtoSELECT id, name FROM tb_xxx WHERE deleted 0/select匹配优先级databaseIdmysql 无 databaseId兜底1.5.3 常见方言差异与处理差异点MySQL达梦PostgreSQL分页LIMIT offset, sizeLIMIT size OFFSET offsetLIMIT size OFFSET offset字符串拼接CONCAT(a, b)CONCAT(a, b)或a || ba || b日期函数NOW()SYSDATENOW()自增主键AUTO_INCREMENTIDENTITY(1,1)SERIAL布尔类型TINYINT(1)BITBOOLEAN反引号/引号columncolumncolumnLIKE 通配符%/_%/_%/_处理原则能用 SQL 标准的语法如LIMIT size OFFSET offset、CONCAT()各数据库通用不需要分版本实在无法统一的如日期函数、自增主键用databaseId分版本写分页推荐直接用 ORM 框架的分页插件自动适配方言1.5.4 DDL 多数据库管理不要试图写一套通用 DDL各数据库独立维护resources/ └── db/ ├── mysql/ │ └── schema.sql -- MySQL 建表脚本 ├── dm/ │ └── schema.sql -- 达梦建表脚本 └── postgresql/ └── schema.sql -- PostgreSQL 建表脚本DDL 差异示例-- MySQLCREATETABLEtb_xxx(idBIGINTNOTNULLAUTO_INCREMENT,nameVARCHAR(128)NOTNULL,enabledTINYINTDEFAULT1,create_timeDATETIMEDEFAULTNULL,PRIMARYKEY(id))ENGINEInnoDBDEFAULTCHARSETutf8mb4;-- 达梦CREATETABLEtb_xxx(idBIGINTNOTNULLIDENTITY(1,1),nameVARCHAR(128)NOTNULL,enabledBITDEFAULT1,create_timeTIMESTAMPDEFAULTNULL,PRIMARYKEY(id));1.5.5 多数据库最佳实践清单实践说明ORM 优先单表操作全部走 ORM天然屏蔽方言差异SQL 标准化多表 SQL 尽量用标准语法减少databaseId分支独立 DDL各数据库独立维护建表脚本不写通用 DDLCI 双跑集成测试至少在两种数据库上各跑一遍方言常量通过databaseId或配置项获取当前数据库类型业务代码中不硬编码方言判断避免方言函数如IFNULL()(MySQL) vsNVL()(Oracle/达梦)统一用COALESCE()(SQL 标准)连接池配置各数据库使用对应的 Driver 和连接池配置通过 Spring Profile 切换1.5.6 Spring Profile 切换数据源# application-mysql.ymlspring:datasource:driver-class-name:com.mysql.cj.jdbc.Driverurl:jdbc:mysql://localhost:3306/mydb?useUnicodetrue# application-dm.ymlspring:datasource:driver-class-name:dm.jdbc.driver.DmDriverurl:jdbc:dm://localhost:5236/mydb启动时通过--spring.profiles.activemysql或dm切换无需改代码。1.6 分页查询规范列表查询必须分页禁止全表加载后内存过滤使用框架提供的统一分页机制避免手动拼接 LIMIT分页结果统一转换为 DTO不直接暴露 Entity// 标准分页查询publicPageResultXxxDtopage(PageRequestreq){PageEntitypagemapper.selectPage(toPage(req),buildQuery(req));returntoPageResult(page,XxxDto.class);}1.7 批量操作规范规则说明使用批量 APIinsertBatch()/updateBatch()避免循环单条操作控制批次大小默认 1000 条/批大数据量可自定义如 500事务内分批提交避免单次 SQL 过长导致锁表或内存溢出// ✅ 批量插入mapper.insertBatch(entityList,500);// ❌ 循环单条插入for(Entitye:entityList){mapper.insert(e);// N 次 DB 交互性能差}2. 事务管理规范2.1 事务注解策略操作类型注解说明读操作查询/分页Transactional(readOnly true)只读事务数据库可优化如不获取写锁、跳过 redo log写操作增/删/改Transactional默认回滚 RuntimeException复杂写操作Transactional(rollbackFor Exception.class)显式声明回滚所有异常防止 checked exception 绕过回滚2.2 核心规则规则一事务只加在 Service 层// ✅ 正确事务在 Service 层ServicepublicclassXxxService{Transactionalpublicvoidadd(XxxDtodto){...}}// ❌ 错误事务不应加在 Controller 或 Mapper 层RestControllerpublicclassXxxController{Transactional// 不该在这里publicRStringadd(RequestBodyXxxDtodto){...}}规则二写操作异常必须 throw不能用 return// ✅ 正确抛异常 → 事务回滚Transactionalpublicvoidadd(XxxDtodto){if(duplicate){thrownewBusinessException(编码已存在);// 事务正确回滚}mapper.insert(entity);}// ❌ 错误return 错误码 → 事务不回滚半截数据落库TransactionalpublicStringadd(XxxDtodto){if(duplicate){return编码已存在;// 事务不会回滚前面的写操作已落库}mapper.insert(entity);returnsuccess;}规则三禁止在 Controller 层 try-catch 吞掉异常异常应自然冒泡到全局异常处理器统一处理Controller 层捕获异常会导致事务失效和错误处理不一致。规则四事务方法内不做耗时操作// ✅ 正确事务只包裹数据库操作Transactionalpublicvoidadd(XxxDtodto){mapper.insert(entity);mapper.insertBatch(items);}// 事务外的耗时操作sendMqMessage(event);uploadFile(file);// ❌ 错误事务内包含远程调用和文件 I/OTransactionalpublicvoidadd(XxxDtodto){mapper.insert(entity);remoteService.call();// 长事务锁持有时间过长fileStorage.upload();// 长事务mapper.insertBatch(items);}2.3 事务失效场景场景原因解决方案同类方法调用this.methodA()调用Transactional的methodB()绕过代理注入自身代理或拆分到不同 Service异常被 catch异常在事务方法内被捕获Spring 感知不到异常不要 catch或 catch 后重新 throw非 public 方法Spring AOP 只代理 public 方法事务方法必须为 public异常类型不匹配默认只回滚 RuntimeExceptionchecked exception 不回滚加rollbackFor Exception.class数据库不支持事务如 MySQL MyISAM 引擎统一使用 InnoDB2.4 乐观锁并发更新场景必须使用乐观锁防止丢失更新问题// Entity 字段VersionprivateIntegerversion;// 更新时检查返回值introwsmapper.updateById(entity);if(rows0){thrownewBusinessException(数据已被他人修改请刷新后重试);}原理UPDATE 语句自动追加AND version #{version}若返回 0 行说明数据已被他人修改。2.5 事务传播行为了解传播行为含义使用频率REQUIRED默认有事务则加入无则新建最常用REQUIRES_NEW总是新建事务挂起当前事务日志记录不论主事务成功失败都要落库SUPPORTS有事务则加入无则非事务执行只读查询NOT_SUPPORTED总是非事务执行极少使用NESTED嵌套事务内层回滚不影响外层谨慎使用99% 的场景使用默认的REQUIRED即可不要过度设计事务传播。3. 安全规范3.1 逻辑删除所有删除操作使用逻辑删除deleted字段禁止物理 DELETEORM 框架自动追加AND deleted 0自定义 SQL 需手动添加deleted字段0 未删除1 已删除3.2 防注入使用 ORM 的 Wrapper API 或#{param}占位符禁止拼接 SQL 字符串如WHERE name name 分页排序的字段名需白名单校验不接受前端传入原始 SQL 片段3.3 数据隔离多租户场景查询时自动追加租户条件自定义 SQL 需手动添加权限隔离敏感数据查询需校验当前用户是否有权访问4. 性能规范4.1 查询优化规则说明避免SELECT *只查需要的字段减少网络传输和内存占用分页必加列表查询必须分页禁止全表查询后内存过滤索引覆盖高频查询条件字段必须建索引避免 N1关联数据用 JOIN 一次查出禁止循环内逐条查询区分度区分度低的字段如enabled不单独建索引4.2 写入优化规则说明批量写入多条插入使用批量 API避免循环单条 insert控制批次大批量数据分批处理建议 500~1000 条/批索引数量单表索引不超过 5 个避免写入时索引维护开销过大4.3 连接与资源不在代码中手动管理数据库连接由连接池托管事务方法只包含数据库操作远程调用、文件 I/O 放在事务外长事务必须避免持有锁时间过长会导致其他请求阻塞
返回列表