Java开发经验——数据库开发经验

摘要

本文主要介绍了Java开发中的数据库操作规范,包括数据库建表规范、索引规约、SQL规范和ORM规约。强调了在数据库设计和操作中应遵循的最佳实践,如字段命名、数据类型选择、索引创建、SQL语句编写和ORM映射,旨在提高数据库操作的性能和安全性。

1. 数据库建表规范

【强制】表达是与否概念的字段,必须使用is_xxx的方式命名,数据类型的是unsigned tinyint(1表示是,0表示否)。说明:任何字段如果为非负数,必须是unsigned。

数据类型的语义约束

  • 明确非负性对于明确只接受非负数的字段,将其定义为 unsigned 可以从数据库层面明确约束该字段不能存储负值。例如,ageis_xxx 这种字段不应出现负值,使用 unsigned 提高了数据约束能力。
  • 防止错误数据若定义为有符号类型(signed),可能因代码或外部输入错误而导致负数插入,而 unsigned 则能直接避免此类问题。

节省存储空间

  • TINYINT 是 1 字节的类型,取值范围:
  • signed-128127
  • unsigned0255

在明确字段非负的情况下,使用 unsigned 可以充分利用数值范围。例如:布尔型的 TINYINT(1) 只需要 01 两个值,使用 unsigned 更贴合业务需求,且减少浪费。

性能优化

  • 索引和比较效率更高:数据库在处理 unsigned 数据时会省略对负值的检查和处理逻辑,能够略微提升查询和比较的效率。
  • 存储和传输效率:尽管单个字段的差异可能不明显,但在高并发或大数据量场景下,unsigned 的节省会带来整体性能优化。

注意:POJO类中的任何布尔类型的变量,都不要加is前缀,所以,需要在<resultMap>设置从isx到Xx的映射关系。数据库表示是与否的值,使用tinyint类型,坚持isxx的命名方式是为了明确其取值含义与取值范围。

正例:表达逻辑删除的字段名is deleted,1表示删除,0表示未删除。

CREATE TABLE credit_risk_check (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id INT UNSIGNED NOT NULL,              -- 用户ID
    is_blacklisted TINYINT(1) UNSIGNED NOT NULL DEFAULT 0, -- 是否加入黑名单
    is_fraudulent TINYINT(1) UNSIGNED NOT NULL DEFAULT 0   -- 是否疑似欺诈
);

CREATE TABLE users (
    user_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    is_active TINYINT(1) UNSIGNED NOT NULL DEFAULT 0, -- 是否激活用户
    is_deleted TINYINT(1) UNSIGNED NOT NULL DEFAULT 0  -- 是否已删除
);

【强制】强制表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。

说明:MySQL在Vindows下不区分大小写,但在Liux下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。

正例:aliyun admin,rdc_config,level_3name

反例:AliyunAdmin,rdcConfig,level_3_name

【强制】表名不使用复数名词。

说明: 表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。

【强制】禁用保留字,如desc、range、match、delayed等,请参考MySQL官方保留字。

【强制】主键索引名为pk_字段名; 唯一索引名为uk_字段名; 普通索引名则为idx_字段名

说明: pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的简称。

主键索引(Primary Key Index)

特点

  • 唯一性约束:主键索引保证表中每一行的主键值是唯一的,且不能为空。
  • 自动创建索引定义主键时,数据库会自动为主键字段创建一个唯一的聚簇索引(如 MySQL 的 InnoDB 引擎)。
  • 聚簇索引:在支持聚簇索引的引擎中,表的数据存储会按照主键索引的顺序组织。
  • 每张表仅能有一个主键

使用场景

  • 用于标识表中每一行记录的唯一标识符(例如 user_id 或订单号 order_id)。
  • 通常设计为自增字段或全局唯一标识符(UUID)。
CREATE TABLE users (
    user_id INT UNSIGNED AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    PRIMARY KEY (user_id)  -- 主键索引
);

唯一索引(Unique Key Index)

特点

  • 唯一性约束:唯一索引确保字段值在整个表中唯一,但允许 NULL 值(多个 NULL 被认为是不同的)。
  • 支持多个字段:一个表可以有多个唯一索引。

使用场景

  • 用于约束一些字段的唯一性,例如邮箱地址、用户名或身份证号。
  • 避免重复数据输入,保证数据一致性。

示例

CREATE TABLE users (
  email VARCHAR(255) NOT NULL,
  phone_number VARCHAR(15) NOT NULL,
  CONSTRAINT uk_email UNIQUE (email), -- 唯一索引
  CONSTRAINT uk_phone_number UNIQUE (phone_number) -- 唯一索引
);

普通索引(Normal Index)

特点

  • 无约束限制:普通索引仅用于加速查询性能,对字段的唯一性或非空没有限制。
  • 支持重复值:普通索引的字段可以有重复值,也可以为 NULL
  • 支持组合索引:普通索引可以包含一个或多个字段。

使用场景

  • 用于优化查询频繁的字段,特别是非唯一字段,如用户名、创建时间等。
  • 用于加速排序或范围查询。

示例

CREATE TABLE users (
  username VARCHAR(50) NOT NULL,
  created_at DATETIME NOT NULL,
  INDEX idx_username (username),       -- 普通索引
  INDEX idx_created_at (created_at)    -- 普通索引
);

属性

主键索引

唯一索引

普通索引

唯一性约束

是否允许 NULL

是(允许多个 NULL)

数量限制

每张表只能有一个

一张表可以有多个

一张表可以有多个

功能

标识每行记录的唯一性

限制字段值的唯一性

提高查询效率

是否自动创建

是(创建主键时自动生成)

否(需手动创建)

否(需手动创建)

是否影响物理存储

是(聚簇索引在 InnoDB 中会影响存储)

典型使用场景

自增 ID、全局唯一标识符

邮箱、身份证号、用户名

高查询频率的普通字段

【强制】 小数类型为decimal,禁止使用float和double
说明: 在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的 结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。或者使用使用字符类型的存储。

CREATE TABLE users (
    user_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 主键
    email VARCHAR(255) NOT NULL,                    -- 邮箱(唯一)
    phone_number VARCHAR(15) NOT NULL,              -- 手机号(唯一)
    username VARCHAR(50) NOT NULL,                  -- 用户名
    created_at DATETIME NOT NULL,                   -- 创建时间
    updated_at DATETIME NOT NULL,                   -- 更新时间

    -- 主键索引
    CONSTRAINT pk_user_id PRIMARY KEY (user_id),

    -- 唯一索引
    CONSTRAINT uk_email UNIQUE (email),
    CONSTRAINT uk_phone_number UNIQUE (phone_number),

    -- 普通索引
    INDEX idx_username (username),
    INDEX idx_created_at (created_at)
);

【强制】如果存储的字符串长度几乎相等,使用char定长字符串类型。

【强制】varchar是可变长字符串,不预先分配存储空间,长度不要超过5000,如果存储长度 大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。

【强制】表必备三字段:id,create_time,update_time,version,staus,

说明:其中id必为主键,类型为bigint unsigned、单表时自增、步长为1。create time,update time的类型均为datetime类型,前者现在时表示主动式创建,后者过去分词表示被动式更新。

【推荐】表的命名最好是遵循“业务名称_表的作用”。

正例:alipay_task / force_project / trade_config

【推荐】库名与应用名称尽量一致。

【推荐】如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释。

【推荐】字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:

  1. 不是频繁修改的字段。
  2. 不是唯一索引的字段。
  3. 不是 varchar 超长字段,更不能是 text 字段。

正例: 各业务线经常冗余存储商品名称,避免查询时需要调用 IC 服务获取。

【推荐】单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。

说明: 如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。

【参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。

正例:无符号值可以避免误存负数,且扩大了表示范围。

2. 数据库索引规约

【强制】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。

说明: 不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的; 另外, 即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律(“凡是可能出错的地方,迟早会出错”),必然有脏数据产生

组合唯一索引: 假设有一个电商订单表 orders,需要保证同一用户(user_id)在同一时间(order_date)只能有一个特定商品(product_id)的订单。此时可以为这三个字段建立组合唯一索引

CREATE TABLE orders (
    order_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 主键
    user_id INT UNSIGNED NOT NULL,                    -- 用户ID
    product_id INT UNSIGNED NOT NULL,                 -- 商品ID
    order_date DATE NOT NULL,                         -- 下单日期
    quantity INT UNSIGNED NOT NULL DEFAULT 1,         -- 购买数量
    total_price DECIMAL(10, 2) NOT NULL,              -- 总价

    -- 组合唯一索引:确保 user_id、product_id 和 order_date 的组合唯一
    CONSTRAINT uk_user_product_date UNIQUE (user_id, product_id, order_date)
);

防止重复数据: 假设没有唯一索引,可能会由于程序逻辑错误,导致以下重复数据插入:

INSERT INTO orders (user_id, product_id, order_date, quantity, total_price)
VALUES (1, 101, '2024-12-21', 1, 100.00);

INSERT INTO orders (user_id, product_id, order_date, quantity, total_price)
VALUES (1, 101, '2024-12-21', 1, 100.00); -- 重复数据

通过唯一索引,数据库会直接报错,避免数据重复:

Error: Duplicate entry '1-101-2024-12-21' for key 'uk_user_product_date'

提高查询效率:当查询某用户某天的特定商品订单时,唯一索引能直接命中:

SELECT * FROM orders WHERE user_id = 1 AND product_id = 101 AND order_date = '2024-12-21';

由于是唯一索引,查询效率更高。

【强制】在varchar字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据 实际文本区分度决定索引长度。

说明: 索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为20的索引,区分度会高达 90% 。以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。

【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。 (查询一定要使用索引,不能走全表扫描)。

说明: 索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。

索引失效:使用 LIKE '%keyword'LIKE '%keyword%' 时,查询会失去对索引的利用,触发全表扫描。例如:LIKE '%abc'LIKE '%abc%',因为无法预判字符串前缀,索引无法帮助快速定位匹配的数据。

查询性能低下:在数据量较大的情况下,全表扫描会占用大量资源,导致查询效率下降。

资源浪费:全模糊查询会浪费 CPU 和 IO 资源,影响其他查询操作,导致系统性能整体下降。

前缀匹配查询

  • 使用 LIKE 'keyword%'= 来保证索引的利用。
  • 这种方式可以让查询条件基于索引进行匹配,而不是全表扫描。

借助全文检索引擎:使用全文搜索引擎(如 ElasticsearchSphinxMySQL FULLTEXT 索引),适合处理全文模糊匹配的场景。

自定义关键字拆分:如果必须支持模糊查询,可以通过应用层将字符串拆分为前缀、后缀等分段存储,使用组合索引来优化查询。

【推荐】如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能(注意最左匹配原则)。

正例: where a=? and b=? order by c; 索引:a_b_c 
反例: 索引如果存在范围查询,那么索引有序性无法利用,如:WHERE a>10 ORDER BY b; 索引 a_b 无 法排序。 

【推荐】利用覆盖索引来进行查询操作,避免回表。(就是的使用索引中就查询需要的值)
说明: 如果一本书需要知道第 11 章是什么标题,会翻开第 11 章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。

正例: 能够建立索引的种类分为主键索引、唯一索引、普通索引三种,而覆盖索引只是一种查询的一种效 果,用 explain 的结果,extra 列会出现:using index。

  • 减少 IO 操作:不需要回表,直接从索引中获取所需字段,减少磁盘 IO 和内存开销。
  • 提高查询效率:索引通常存储在内存中,查询性能比直接访问表记录快。
  • 优化性能:特别是在大表中,可以显著降低查询延迟。
CREATE TABLE users (
    user_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, -- 主键
    username VARCHAR(50) NOT NULL,                  -- 用户名
    email VARCHAR(100) NOT NULL,                    -- 邮箱
    created_at DATETIME NOT NULL                    -- 创建时间
);

错误示例:没有覆盖索引 如果查询以下语句:

SELECT user_id, username FROM users WHERE username = 'JohnDoe';

如果只有以下索引:

CREATE INDEX idx_username ON users (username);

查询时执行流程:

  • 索引 idx_username 用于定位到符合条件的记录。
  • 然后需要回表(通过主键找到完整的记录),再获取 user_idusername 的值。

正确示例:利用覆盖索引,为了避免回表,调整索引结构,使其包含查询所需的所有字段:

CREATE INDEX idx_username_user_id ON users (username, user_id);

此时执行:

SELECT user_id, username FROM users WHERE username = 'JohnDoe';

查询时直接利用覆盖索引 idx_username_user_id,不需要回表即可获得 user_idusername 的值。

实现多字段覆盖索引,如果需要查询多个字段,例如:

SELECT user_id, username, email FROM users WHERE username = 'JohnDoe';

为了避免回表,可以创建以下覆盖索引:

CREATE INDEX idx_username_cover ON users (username, user_id, email);

此时查询所需的 usernameuser_idemail 都被索引覆盖,查询直接从索引中获取数据,无需回表。

【推荐】利用延迟关联或者子查询优化超多分页场景(防止慢查询)。

说明: MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。

正例: 先快速定位需要获取的 id 段,然后再关联: 							
SELECT t1.* FROM 表 1 as t1, (select id from 表 1 where 条件 LIMIT 100000,20 ) as t2 where t1.id=t2.id。

【推荐】SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 consts 最好。

说明:

  1. consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
  2. ref 指的是使用普通的索引(normal index)。
  3. range 对索引进行范围检索。

反例: explain 表的结果,type=index,索引物理文件全扫描,速度非常慢,这个 index 级别比较 range 还低,与全表扫描是小巫见大巫。

【推荐】建组合索引的时候,区分度最高的在最左边(最左索引原则)。
正例: 如果 where a=? and b=?,a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即可。

说明: 存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where c>? and d=? 那么即使 c 的区分度更高,也必须把 d 放在索引的最前列,即建立组合索引 idx_d_c。

【推荐】防止因字段类型不同造成的隐式转换,导致索引失效(防止索引失效)

【参考】创建索引时避免有如下极端误解(表索引能够覆盖到底所有的查询要求即可,不宜多,不宜少):

  1. 索引宁滥勿缺。认为一个查询就需要建一个索引。
  2. 吝啬索引的创建。认为索引会消耗空间、严重拖慢记录的更新以及行的新增速度。
  3. 抵制惟一索引。认为惟一索引一律需要在应用层通过“先查后插”方式解决。

3. 数据库SQL规范

【强制】不要使用count(列名)或count(常量)来替代count(*),count(*)是SQL92定义的标 准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。

说明: count(*)会统计值为 NULL 的行,而count(列名)不会统计此列为 NULL值的行。

【强制】count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1, col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。

【强制】当某一列的值全是NULL时,count(col)的返回结果为0,但sum(col)的返回结果为 NULL,因此使用 sum()时需注意 NPE 问题。

正例:  可以使用如下方式来避免 sum 的 NPE 问题: SELECT IFNULL(SUM(column), 0) FROM table; 

【强制】使用ISNULL()来判断是否为NULL值。

说明: NULL 与任何值的直接比较都为 NULL。

  • NULL<>NULL 的返回结果是 NULL,而不是 false。
  • NULL=NULL 的返回结果是 NULL,而不是 true。
  • NULL<>1 的返回结果是 NULL,而不是 true。

反例: 在 SQL 语句中,如果在 null 前换行,影响可读性。select * from table where column1 is null and column3 is not null; 而`ISNULL(column)`是一个整体,简洁易懂。从性能数据上分析,`ISNULL(column)` 执行效率更快一些。

在 SQL 中,ISNULL() 是一个函数,常用于判断一个值是否为 NULL,并且可以替代 NULL 值为指定的值。不同的数据库管理系统(DBMS)在处理 NULL 值时可能会有所不同。ISNULL() 在某些 DBMS 中如 SQL Server 使用广泛,但在 MySQL 中常用 IFNULL()COALESCE() 来替代。

MySQL 使用 IFNULL(): 在 MySQL 中,IFNULL() 用于判断一个表达式是否为 NULL,如果是,返回替代值。

IFNULL(expression, replacement_value)

使用 IFNULL() 判断 NULL

如果 emailNULL,返回 'noemail@example.com'

SELECT IFNULL(email, 'noemail@example.com') AS user_email FROM users;

使用 IFNULL() 判断 NULL 与普通字段查询

如果 ageNULL,返回 'Unknown'

SELECT name, IFNULL(CAST(age AS CHAR), 'Unknown') AS user_age FROM users;

PostgreSQL 使用 COALESCE(): 在 PostgreSQL 和其他一些 SQL 数据库中,COALESCE() 函数用于返回第一个非 NULL 的值。它可以用于判断 NULL 值。

SQL Server 使用 ISNULL():ISNULL() 函数在 SQL Server 中常用来判断字段值是否为 NULL,并且可以为 NULL 值指定一个替代值。

ISNULL(expression, replacement_value)

使用 ISNULL() 判断 NULL

假设我们有一个 users 表,查询用户的 email,如果 emailNULL,则返回。

SELECT ISNULL(email, 'noemail@example.com') AS user_email FROM users;

使用 ISNULL() 判断 NULL 与普通字段查询。

如果 age 字段为 NULL,返回 'Unknown',否则返回实际年龄:

SELECT name, ISNULL(CAST(age AS VARCHAR), 'Unknown') AS user_age FROM users;

使用 IS NULLIS NOT NULL 处理 NULL 值时的注意点

  • 在 SQL 中,NULL 不等于任何值,包括 NULL 本身。所以,不能使用 =!= 来进行 NULL 值的比较。
  • 必须使用 IS NULLIS NOT NULL 来明确判断字段是否为 NULL
SELECT product_id, product_name, price FROM products WHERE price IS NOT NULL;

【强制】代码中写分页查询逻辑时若count为0应直接返回,避免执行后面的分页语句

import java.sql.*;

public class PaginationExample {

    // 获取分页数据
    public static PaginationResult getPaginatedData(int page, int perPage) throws SQLException {
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/your_database", "username", "password");

        // 第一步:查询总记录数
        String countQuery = "SELECT COUNT(*) FROM users";
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery(countQuery);

        // 获取总记录数
        resultSet.next();
        int totalCount = resultSet.getInt(1);

        // 如果总记录数为 0,直接返回空结果
        if (totalCount == 0) {
            return new PaginationResult(0, 0, 0, null);
        }

        // 第二步:执行分页查询
        String paginatedQuery = "SELECT * FROM users LIMIT ? OFFSET ?";
        PreparedStatement preparedStatement = connection.prepareStatement(paginatedQuery);
        preparedStatement.setInt(1, perPage);
        preparedStatement.setInt(2, (page - 1) * perPage);
        ResultSet paginatedResult = preparedStatement.executeQuery();

        // 获取分页数据
        ResultSetMetaData metaData = paginatedResult.getMetaData();
        int columnCount = metaData.getColumnCount();
        List<String> users = new ArrayList<>();
        while (paginatedResult.next()) {
            StringBuilder row = new StringBuilder();
            for (int i = 1; i <= columnCount; i++) {
                row.append(paginatedResult.getString(i)).append(" ");
            }
            users.add(row.toString());
        }

        // 返回分页结果
        return new PaginationResult(totalCount, page, perPage, users);
    }

    public static void main(String[] args) {
        try {
            PaginationResult result = getPaginatedData(1, 10);
            System.out.println(result);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

class PaginationResult {
    private int totalCount;
    private int page;
    private int perPage;
    private List<String> data;

    public PaginationResult(int totalCount, int page, int perPage, List<String> data) {
        this.totalCount = totalCount;
        this.page = page;
        this.perPage = perPage;
        this.data = data;
    }

    @Override
    public String toString() {
        return "PaginationResult{" +
        "totalCount=" + totalCount +
        ", page=" + page +
        ", perPage=" + perPage +
        ", data=" + data +
        '}';
    }
}

无论使用 JDBC 还是 Spring Data JPA,关键的思路是通过先查询总记录数 (count) 来判断是否有数据,如果没有数据(count == 0),直接返回空结果,从而避免进行不必要的分页查询操作。

【强制】不得使用外键与级联,一切外键概念必须在应用层解决。

说明: (概念解释)学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学 生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机 低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库 的插入速度。

为什么禁止使用外键与级联?

性能问题

  • 外键约束会影响插入和删除操作的速度:在数据库中,每次插入、删除或更新数据时,数据库必须检查外键约束是否满足。如果有多个表的外键关系,操作时需要多次检查约束,导致性能下降。
  • 级联操作:如级联删除或更新,会导致多个表的数据修改。虽然这在某些场景下有用,但在高并发环境下,数据库需要保证事务的完整性和一致性,可能造成锁竞争和性能瓶颈。

可扩展性和移植性问题

  • 跨数据库系统迁移困难:不同数据库系统的外键实现和支持程度不同。某些数据库可能不支持复杂的外键约束或有不同的语法和行为,迁移数据时需要额外的适配工作。
  • 服务解耦:使用外键和级联操作使得数据库间的耦合变得更加紧密,这在微服务架构中尤为不可取。每个微服务应当拥有自己的数据存储,并在应用层处理数据一致性,而不是依赖数据库约束来实现。

数据库的复杂性

  • 数据库设计复杂:外键和级联增加了数据库结构的复杂性,特别是当有多张表的外键依赖关系时,维护起来较为麻烦。数据库迁移、升级、数据恢复等操作可能会变得复杂。

假设有一个 orders 表,它引用了 users 表的用户 ID,而你在删除一个用户时需要删除与该用户相关的订单。因为没有外键约束,你必须在应用层执行以下步骤:

# 示例:使用 Python 和 SQLAlchemy 模拟外键删除约束

from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from models import User, Order

# 创建数据库连接和会话
engine = create_engine('mysql://username:password@localhost/dbname')
Session = sessionmaker(bind=engine)
session = Session()

# 获取用户 ID
user_id = 123

# 删除该用户相关的所有订单
orders_to_delete = session.query(Order).filter(Order.user_id == user_id).all()
for order in orders_to_delete:
    session.delete(order)

# 删除用户
user_to_delete = session.query(User).filter(User.id == user_id).first()
session.delete(user_to_delete)

# 提交事务
session.commit()

操作流程

  1. 查找相关订单:在删除用户之前,先查找与该用户相关的所有订单。
  2. 删除订单:删除相关的订单记录。
  3. 删除用户:最后删除用户。

通过应用层的逻辑来模拟外键约束,确保删除顺序正确,避免因外键关系导致的数据不一致。

【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。

【强制】数据订正(特别是删除或修改记录操作)时,要先select,避免出现误删除,确认无误才能执行更新语句。

  • 避免误删除:如果没有先执行 SELECT 确认数据,可能会因为条件设置错误(例如条件漏写、条件错误)导致删除了不该删除的记录,造成无法挽回的损失。
  • 避免误修改:在修改数据之前,先查看哪些记录会被修改,确保目标记录正确。避免误修改不应修改的字段,导致数据错误。
  • 提高操作的透明度:通过先查询,操作人员可以清楚知道将要删除或修改的数据,确保没有遗漏或错误。
  • 备份保护:在进行删除或批量更新时,先 SELECT 确认可以帮助快速备份需要更新的数据或记录(例如,通过导出)。

其他注意事项

  1. 使用事务(Transaction):如果操作涉及多个步骤(如查询、删除、更新),使用事务确保操作的原子性。如果在某个步骤出现问题,可以回滚事务。
BEGIN;

-- 确认查询
SELECT * FROM users WHERE age < 18;

-- 删除操作
DELETE FROM users WHERE age < 18;

COMMIT;  -- 提交事务
  1. 使用 LIMIT 防止误操作大规模数据:在删除或更新时,使用 LIMIT 限制一次删除或更新的记录数,这样可以防止不小心操作过多记录。示例:
DELETE FROM users WHERE age < 18 LIMIT 100;  -- 删除前100条记录
  1. 备份数据:在执行批量删除或更新前,最好先备份受影响的数据,以便出现错误时可以恢复。例如,你可以导出受影响的记录:
SELECT * FROM users WHERE age < 18 INTO OUTFILE '/path/to/backup.csv' FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n';
  1. 审计日志:在执行修改或删除时,记录操作日志。确保所有删除和更新操作都有审计记录,便于后期追溯。
  • 避免误删除或误修改:执行删除或修改操作时,一定要先进行 SELECT 查询,确认符合条件的记录是正确的。
  • 审慎操作:使用事务、备份数据以及添加确认机制(如脚本提示)来保护数据安全。
  • 确保数据的一致性和完整性:即使在高效的自动化流程中,也应保证每一步的安全确认,减少人为错误的发生。

【强制】对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或 表名)进行限定。

说明: 对多表进行查询记录、更新记录、删除记录时,如果对操作列没有限定表的别名(或表名),并且 操作列在多个表中存在时,就会抛异常。

正例: select t1.name from table_first as t1 , table_second as t2 where t1.id=t2.id; 

反例: 在某业务中,由于多表关联查询语句没有加表的别名(或表名)的限制,正常运行两年后,最近在某个表中增加一个同名字段,在预发布环境做数据库变更后,线上查询语句出现出 1052 异常:Column 'name' in field list is ambiguous。 

表示查询中存在 重复的列名,即在查询的结果集中,多个表或子查询中存在同名的列,MySQL 无法确定应该使用哪一个列。

这种情况通常发生在 联接查询 (JOIN) 中,当多个表中存在相同列名(如 name)时,MySQL 无法确定使用哪个表的 name 列。

联接查询中的列名冲突: 假设有两个表 usersorders,都包含 name 列,下面是一个错误的查询:

SELECT name FROM users JOIN orders ON users.id = orders.user_id;

这个查询会抛出 1052 错误,因为 name 列在两个表中都有,而没有明确指定从哪个表获取。

使用表名或表别名

SELECT users.name FROM users JOIN orders ON users.id = orders.user_id;

或者使用表别名来简化查询:

使用表别名

SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id;

在这个查询中,使用了表别名 uo 来明确指定 name 列是从 users 表中获取的。

多个表中有相同列名

假设查询中涉及到多个表,并且这些表中有重复的列名。以下是一个可能的查询:

SELECT name, email
FROM users u
JOIN employees e ON u.id = e.user_id;

如果 usersemployees 表中都有 name 列,则会抛出 1052 错误。解决方法同样是明确指定列的来源:

SELECT u.name AS user_name, e.name AS employee_name, u.email
FROM users u
JOIN employees e ON u.id = e.user_id;

在这个查询中,name 列被明确指定来自 users 表(u.name)和 employees 表(e.name),避免了列名冲突。

【推荐】SQL 语句中表的别名前加 as,并且以 t1、t2、t3、...的顺序依次命名。

说明: 别名可以是表的简称,或者是依照表在 SQL 语句中出现的顺序,以 t1、t2、t3 的方式命名。别名前加 as 使别名更容易识别。

正例: select t1.name from table_first as t1, table_second as t2 where t1.id=t2.id; 

【推荐】in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控 制在 1000 个之内。

  • SQL 查询优化和性能瓶颈执行计划的复杂性:当 IN 后面的集合非常大时,数据库的查询优化器可能会生成较为复杂的执行计划,这可能导致查询的执行效率变低,特别是当涉及到大量数据检索时。数据库需要逐个比对每个 IN 列表中的值,与表中的列进行匹配,如果集合元素过多,可能会影响查询的性能。
  • 索引失效无法利用索引优化:当使用 IN 列表时,数据库通常会试图将 IN 列表中的多个值视为多个 OR 条件来处理。但如果 IN 后面的元素数量过多,可能会导致查询优化器无法有效使用索引,进而导致全表扫描,性能大幅下降。尤其是在一些复杂的查询中,或者当查询条件的列没有合适的索引时,IN 的性能会受到影响。
  • 数据库优化器处理:不同的数据库引擎(例如 MySQL、PostgreSQL、Oracle 等)在处理大量元素的 IN 操作时,可能会遇到内部的性能限制。对于 IN 列表非常大的情况,数据库可能会尝试将其转化为多个小的查询来执行,这会增加额外的查询开销。
  • 参数数量限制:许多数据库系统对 SQL 查询中的参数数量有限制。例如,MySQL 中单个 IN 列表的最大元素数限制是 1000。当超过该限制时,会抛出错误,导致 SQL 无法执行。
  • 内存消耗和优化问题:当 IN 列表包含大量元素时,数据库需要将这些值加载到内存中进行比较,这将消耗更多的内存资源,尤其在高并发、大数据量的情况下,可能导致内存不足,进一步影响查询性能。

in 使用总结

  • IN 的元素数量应控制在 1000 个以内,主要是为了避免性能瓶颈、内存消耗过大以及索引失效等问题。
  • 如果 IN 列表中的元素过多,可以考虑分批查询使用临时表替代方案(如 JOINEXISTS,这些方法能够提高查询效率,减少数据库的负担。
  • 合理设计索引,并避免在高并发、数据量大的情况下频繁使用大型的 IN 查询。

这些做法能够帮助避免在处理大规模查询时因 IN 操作导致的性能问题。

【强制】MySQL 中,推荐使用 utf8mb4 编码 而不是 utf8 编码。

MySQL 中,推荐使用 utf8mb4 编码 而不是 utf8 编码,特别是在需要处理多种语言和特殊字符(如表情符号、一些罕见的Unicode字符等)时。

utf8utf8mb4 的区别

  • utf8 是 MySQL 的早期实现,它只支持 最多 3 个字节的字符,即 U+0000 到 U+FFFF 范围的 Unicode 字符。它无法存储 4 字节的字符。
  • utf8mb4 是 MySQL 后来引入的完整 Unicode 编码,它支持 1 到 4 个字节 的字符,即可以存储所有 Unicode 字符,包括 4 字节字符(如表情符号、部分特殊字符等)。utf8mb4 实际上是对 utf8 的扩展。

为什么选择 utf8mb4 而不是 utf8

  • utf8 的局限性:
    • utf8 并不是一个真正的 UTF-8 实现,它实际上只能处理 Unicode 的 1 到 3 字节 范围内的字符。这意味着它不能处理 4 字节字符,如 表情符号(emoji)、某些语言的特殊字符(例如某些少数民族字符),以及其他 4 字节的 Unicode 字符。
    • utf8 编码下,插入这些超出 3 字节的字符时,MySQL 会报错或丢失数据。
  • utf8mb4 的优势:
    • utf8mb4 能够支持 Unicode 的 1 到 4 字节 范围,完全兼容所有字符集,并能正确处理所有 Unicode 字符,包括表情符号(emoji)、历史符号、少数民族语言字符等。
    • 因为 utf8mb4 能支持 4 字节字符,它是现代应用和多语言环境下的最佳选择。

使用 utf8mb4 编码的理由

  • 兼容性:
    • utf8mb4 是完整的 Unicode 实现,能够支持所有语言的字符和符号。如果你期望支持表情符号或其他 Unicode 扩展字符,必须使用 utf8mb4
    • 如果你的数据库中包含 emoji 或某些特殊符号,而你使用了 utf8 编码,MySQL 会抛出错误或者数据会被截断。
  • 无字符损失:
    • 使用 utf8mb4 能确保字符的存储不被丢失或错误地转换,特别是在国际化应用中,确保所有字符都可以正确保存。
  • utf8mb4 是向下兼容的:
    • utf8mb4 完全兼容 utf8,也就是说,如果数据库或表使用了 utf8mb4,它可以存储 utf8 能存储的所有字符,但反过来就不行。换句话说,utf8mb4utf8 的超集。

如何在 MySQL 中使用 utf8mb4

创建数据库时使用 utf8mb4 编码:

CREATE DATABASE my_database CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

创建表时使用 utf8mb4 编码:

CREATE TABLE my_table (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  description TEXT
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

修改已有表的字符集为 utf8mb4

如果你的表已经使用 utf8 编码,可以通过以下命令将字符集转换为 utf8mb4

ALTER TABLE my_table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

修改已有列的字符集为 utf8mb4

ALTER TABLE my_table MODIFY COLUMN name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

13.【参考】TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但TRUNCATE无事务且不触发 trigger,有可能造成事故,故不建议在开发代码中使用此语句。

说明: TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同。

4. ORM规约

【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。

说明:

  1. 增加查询分析器解析成本
  2. 增减字段容易与 resultMap 配置不一致
  3. 无用字段增加网络 消耗,尤其是 text 类型的字段。

假设我们需要查询 usersorders 两个表的数据,可以使用 JOIN 操作,并且避免使用 * 来避免字段冲突。

<!-- 正确示例:明确指定需要查询的字段,避免字段冲突 -->

<select id="selectUserOrders" resultMap="userOrderResultMap">
    SELECT 
        u.id AS user_id, 
        u.username, 
        u.email, 
        o.id AS order_id, 
        o.total_price 
    FROM users u
    JOIN orders o ON u.id = o.user_id;
</select>

在这个例子中,我们通过 AS 为表 usersorders 中同名的 id 字段添加别名(user_idorder_id),避免了字段冲突。这样做可以确保查询结果的字段名称清晰且不重复。

定义 resultMap 映射:

为了将查询结果正确映射到 Java 对象,我们需要定义 resultMap 来映射字段到对象属性。例如,我们有 UserOrder 类,分别表示 usersorders 表。

<resultMap id="userOrderResultMap" type="UserOrderDTO">
  <result property="userId" column="user_id"/>
  <result property="username" column="username"/>
  <result property="email" column="email"/>
  <result property="orderId" column="order_id"/>
  <result property="totalPrice" column="total_price"/>
</resultMap>

这里,UserOrderDTO 是我们查询结果的封装对象。字段 user_id 映射到 userIdusername 映射到 username 等。通过这种方式,我们可以确保查询结果按照我们定义的字段正确映射到 Java 对象。

public class UserOrderDTO {
    private int userId;
    private String username;
    private String email;
    private int orderId;
    private BigDecimal totalPrice;

    // Getters and Setters
}

使用 sql 标签来引用公共 SQL 片段

首先,我们可以在 MyBatis 的 XML 配置文件中定义一个公共的 SQL 片段,比如查询 users 表的基本字段。使用 sql 标签,可以在多个查询中复用这个片段。

<!-- 定义公共的 SQL 片段 -->
<sql id="userColumns">
    id, username, email
</sql>

接下来,我们可以在查询中引用这个 SQL 片段,避免重复书写这些字段。

<!-- 查询所有用户的基本信息 -->
<select id="selectUsers" resultType="User">
    SELECT 
        <include refid="userColumns"/>
    FROM users;
</select>

复用 SQL 片段(多次引用):如果需要查询其他表的数据,或者想要使用相同的列来进行其他操作,我们仍然可以引用同一个 SQL 片段。

<!-- 查询所有订单的用户信息 -->
<select id="selectUserOrders" resultType="UserOrderDTO">
    SELECT 
        <include refid="userColumns"/>, o.id AS order_id, o.total_price 
    FROM users u
    JOIN orders o ON u.id = o.user_id;
</select>

使用 sql 标签来处理复杂的查询条件: 如果查询条件复杂,常常需要复用某些 SQL 片段,可以使用 sql 标签来定义条件部分,方便引用。

使用 sql 标签来处理复杂的查询条件: 如果查询条件复杂,常常需要复用某些 SQL 片段,可以使用 sql 标签来定义条件部分,方便引用。

<!-- 定义一个查询条件 -->
<sql id="userWhereCondition">
    WHERE status = #{status} AND created_at > #{createdAt}
</sql>

<!-- 查询特定状态的用户 -->
<select id="selectUsersByStatus" resultType="User">
    SELECT 
        <include refid="userColumns"/>
    FROM users
    <include refid="userWhereCondition"/>
</select>

动态 SQL 片段: 对于动态查询条件,可以结合 MyBatis 的动态 SQL 功能,如 ifwheretrim 等标签来生成灵活的查询条件。

<!-- 动态查询条件 -->
<select id="selectUsersByDynamicConditions" resultType="User">
    SELECT 
        <include refid="userColumns"/>
    FROM users
    <where>
        <if test="username != null">
            AND username = #{username}
        </if>
        <if test="email != null">
            AND email = #{email}
        </if>
    </where>
</select>

在这个例子中,我们使用了 <where> 标签来动态生成 SQL 条件,只有在参数不为 null 时才会添加对应的条件。

【强制】POJO类的布尔属性不能加is,而数据库字段必须加is_,要求在resultMap中进行 字段与属性之间的映射。
说明: 参见定义 POJO 类以及数据库字段定义规定,在 sql.xml 增加映射,是必须的。

假设你有一个 users 表,表中有一个布尔字段 is_active,表示用户是否激活。POJO 类中定义的布尔属性是 active,没有 is 前缀。我们需要在 resultMap 中明确将 is_active 字段映射到 active 属性。

数据库表结构:

users 表:

Column

Type

id

INT

username

VARCHAR

email

VARCHAR

is_active

TINYINT(1)

POJO 类(User 类)

在 POJO 类中,布尔类型的属性不加 is 前缀,直接使用 active 属性名。

public class User {
    private int id;
    private String username;
    private String email;
    private boolean active; // 布尔属性名不加 "is"

    // Getters and Setters
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }
}

resultMap 映射配置

在 MyBatis 的 XML 映射文件中,我们需要显式地使用 resultMap 来将数据库的 is_active 字段映射到 User 类的 active 属性。

<resultMap id="userResultMap" type="User">
  <id property="id" column="id"/>
  <result property="username" column="username"/>
  <result property="email" column="email"/>
  <!-- 映射数据库字段 is_active 到 POJO 类的 active 属性 -->
  <result property="active" column="is_active"/>
</resultMap>

<select id="selectUserById" resultMap="userResultMap">
  SELECT id, username, email, is_active
  FROM users
  WHERE id = #{id};
</select>

【强制】不要用resultClass当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义<resultMap>; 反过来,每一个表也必然有一个<resultMap>与之对应。

说明:配置映射关系,使字段与DO类解耦,方便维护。

为什么推荐使用 resultMap

  1. 明确的映射关系:使用 resultMap 明确指出数据库列和 Java 对象属性之间的关系,避免了 MyBatis 自动映射时可能出现的歧义,尤其是在属性名与列名不一致的情况下。
  2. 字段别名和处理resultMap 可以定义字段的别名,将数据库字段映射到 Java 类中的属性名,即使字段名不一致,也能做到灵活处理。
  3. 增强的映射控制resultMap 提供了更多的配置选项,如复杂映射(例如嵌套对象映射),以及对字段的额外处理(如日期格式化等)。
  4. 提升代码可读性与可维护性:通过显式的映射,可以提高代码的可读性,其他开发者在阅读代码时,能明确知道字段和属性之间的关系。

假设我们有一个 users 表和一个 User 类,users 表包含 idusernameemailpassword 等字段,User 类有对应的属性,但我们不直接使用 resultClass,而是通过 resultMap 显式定义映射关系。

表结构(users):

Column

Type

id

INT

username

VARCHAR

email

VARCHAR

password

VARCHAR

public class User {
    private int id;
    private String username;
    private String email;
    private String password;

    // Getters and Setters
}

使用 resultMap 进行映射: 即使 User 类的属性和 users 表的字段一一对应,依然推荐使用 resultMap 进行明确映射。

<resultMap id="userResultMap" type="User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="email" column="email"/>
    <result property="password" column="password"/>
</resultMap>

<select id="selectUserById" resultMap="userResultMap">
    SELECT id, username, email, password
    FROM users
    WHERE id = #{id};
</select>

复杂类型映射(嵌套映射)

如果表中有外键关系,或者你需要返回嵌套对象(比如 User 类中的 Address 类属性),则可以使用 resultMap 定义嵌套映射。例如,假设 users 表有一个外键 address_id,引用 addresses 表。

public class Address {
    private int id;
    private String street;
    private String city;

    // Getters and Setters
}

users 表结构更新:

Column

Type

id

INT

username

VARCHAR

email

VARCHAR

password

VARCHAR

address_id

INT

Address 表结构:

Column

Type

id

INT

street

VARCHAR

city

VARCHAR

定义 resultMap 和嵌套映射:

<resultMap id="addressResultMap" type="Address">
    <id property="id" column="id"/>
    <result property="street" column="street"/>
    <result property="city" column="city"/>
</resultMap>

<resultMap id="userResultMap" type="User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="email" column="email"/>
    <result property="password" column="password"/>
    <!-- 嵌套映射 -->
    <association property="address" javaType="Address" resultMap="addressResultMap"/>
</resultMap>

<select id="selectUserWithAddressById" resultMap="userResultMap">
    SELECT u.id, u.username, u.email, u.password, a.id AS address_id, a.street, a.city
    FROM users u
    LEFT JOIN addresses a ON u.address_id = a.id
    WHERE u.id = #{id};
</select>

在这个例子中:

  • 嵌套映射: 我们通过 <association> 标签将 users 表中的 address_id 字段映射到 User 类中的 Address 对象。这意味着,当查询 users 表时,MyBatis 会使用 addressResultMapaddress_id 映射到 Address 类的相关属性。
  • <association> 标签: 用于处理嵌套的映射关系,指定嵌套对象的 resultMap 和映射的属性。

使用 resultMap 进行多表查询

假设我们还需要查询多个表(比如 orders 表),也可以使用类似的方式来映射结果。

查询用户和订单信息:

<resultMap id="orderResultMap" type="Order">
    <id property="id" column="id"/>
    <result property="totalPrice" column="total_price"/>
    <result property="userId" column="user_id"/>
</resultMap>

<select id="selectUserWithOrdersById" resultMap="userResultMap">
    SELECT 
        u.id AS user_id, u.username, u.email, u.password,
        o.id AS order_id, o.total_price
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE u.id = #{id};
</select>

在这个查询中,我们使用 LEFT JOINusers 表和 orders 表连接,并将 orders 表的字段映射到 Order 类。通过 resultMap,我们将查询结果映射到 UserOrder 对象。

总结

  • 即使字段和属性一一对应,也应该使用 resultMap。这种做法明确了映射关系,提升了代码的可读性和可维护性。
  • resultMap 提供了更多的灵活性,可以应对字段名不一致、嵌套对象、复杂查询等复杂情况。
  • 嵌套映射:使用 <association> 标签可以实现多表查询时的对象嵌套映射。
  • 避免使用 resultClass:直接使用 resultClass 可能会导致自动映射的问题,resultMap 让你更加精确地控制字段到属性的映射。

insert 标签插入组合对象

在 MyBatis 中,如果你需要插入一个组合对象(如包含多个属性或嵌套对象的对象),你可以使用 insert 标签来实现。插入操作通常会使用 parameterType 来指定接收传入参数的 Java 类,然后在 SQL 语句中通过占位符来插入对象的属性值。

数据库表结构

假设我们有两个表:usersaddresses

users 表:

Column

Type

id

INT

username

VARCHAR

email

VARCHAR

address_id

INT

addresses 表:

Column

Type

id

INT

street

VARCHAR

city

VARCHAR

Java 类

我们定义了 User 类,它包含 Address 类的引用,表示一个组合对象。

public class User {
    private int id;
    private String username;
    private String email;
    private Address address; // Address 类是嵌套的对象

    // Getters and Setters
}

public class Address {
    private int id;
    private String street;
    private String city;

    // Getters and Setters
}

insert 标签的配置

假设你要插入一个 User 对象,并且 User 对象中包含一个 Address 对象。你需要在 insert 标签中编写插入语句,同时使用 parameterType 来指明参数类型是 User

<insert id="insertUser" parameterType="User">
    <!-- 插入到 users 表 -->
    INSERT INTO users (username, email, address_id)
    VALUES (#{username}, #{email}, 
        (SELECT id FROM addresses WHERE street = #{address.street} AND city = #{address.city}));
</insert>

多表插入的情况

有时你可能需要先插入一个表(例如 addresses 表),然后将插入后的 id 作为外键插入到其他表(例如 users 表)。这是一个典型的多表插入场景。

假设我们需要先插入 address,然后将其 id 插入到 users 表中:

<insert id="insertAddressAndUser" parameterType="User">
    <!-- 插入地址 -->
    INSERT INTO addresses (street, city) 
    VALUES (#{address.street}, #{address.city});
    
    <!-- 获取插入后的 address_id -->
    <selectKey keyProperty="address.id" resultType="int" order="AFTER">
        SELECT LAST_INSERT_ID();
    </selectKey>

    <!-- 插入用户 -->
    INSERT INTO users (username, email, address_id) 
    VALUES (#{username}, #{email}, #{address.id});
</insert>

解析:

  • <selectKey> 标签: 用于获取刚插入的 addressid,并将其设置到 User 对象的 address.id 属性中。
  • LAST_INSERT_ID(): 用来获取上一次插入操作的自动生成的 ID。这里,我们通过 SELECT LAST_INSERT_ID() 来获取 addressid
  • 多表插入: 第一步插入 address,第二步通过获取插入后的 address.id 来将其插入到 users 表的 address_id 字段。

【强制】sql.xml 配置参数使用:#{},#param# 不要使用${} 此种方式容易出现 SQL 注入。

使用 #{} 传递参数 (推荐做法)

#{} 适用于所有的参数绑定,它能够安全地处理各种数据类型。

<select id="selectUserById" resultType="User">
    SELECT id, username, email
    FROM users
    WHERE id = #{id}
</select>

解释:使用 #{id},MyBatis 会将 id 作为一个预处理语句的参数,安全地传递给 SQL 引擎,防止 SQL 注入。

// 假设我们有一个 SqlSession 实例
SqlSession sqlSession = sqlSessionFactory.openSession();

// 查询用户
User user = sqlSession.selectOne("selectUserById", 1);

使用 ${} 存在 SQL 注入风险(避免使用)

#{} 是安全的,而 ${} 直接将参数嵌入到 SQL 语句中,容易导致 SQL 注入攻击。使用 ${} 只有在动态 SQL 中必须拼接表名、列名、排序字段等的时候才可以使用,但这类情况也需要小心处理。

<!-- 错误示范:直接拼接 SQL 语句 -->
<select id="selectUserByColumn" resultType="User">
    SELECT id, username, email
    FROM users
    WHERE ${columnName} = #{value}
</select>

在这个例子中,${columnName} 会将传入的 columnName 直接替换到 SQL 语句中,如果 columnName 的值来自用户输入,攻击者就可以利用这个特性进行 SQL 注入。

【强制】iBATIS 自带的 queryForList(String statementName,int start,int size)不推荐使用。

说明: 其实现方式是在数据库取到 statementName 对应的 SQL 语句的所有记录,再通过 subList 取 start,size 的子集合。
正例: Map<String, Object> map = new HashMap<>(16); map.put("start", start); map.put("size", size);

【强制】不允许直接拿HashMap与Hashtable作为查询结果集的输出。

反例: 某同学为避免写一个<resultMap>xxx</resultMap>,直接使用 HashTable 来接收数据库返回结 果,结果出现日常是把 bigint 转成 Long 值,而线上由于数据库版本不一样,解析成 BigInteger,导致线 上问题。

问题原因:

  1. 类型不明确HashMapHashtable 的键和值是 Object 类型,无法在编译时进行类型检查,容易发生类型转换错误。
  2. 代码可读性差:直接使用 HashMapHashtable 返回结果时,不容易理解字段含义,也不方便重构和维护。
  3. 容易出错:如果查询结果的字段有变动,无法通过类型安全检查及时发现。

因此,推荐使用强类型的 POJO 类来映射查询结果,而不是直接使用 HashMapHashtable

不推荐的方式(使用 HashMap

<select id="selectUserById" resultType="java.util.HashMap">
  SELECT id, username, email
  FROM users
  WHERE id = #{id}
</select>

问题原因:

  • 查询结果直接映射到 HashMap,其中键是字段名(例如:idusernameemail),值是字段对应的值。HashMap 本身没有类型信息,这样做容易让程序员在处理数据时出错。
  • 如果数据库字段发生变化,或者字段的名字与 POJO 类属性不同,可能会导致错误。

推荐的方式(使用 POJO 类)

正确的做法是使用 POJO 类(即普通 Java 对象)来映射查询结果,这样可以确保类型安全、提高代码可读性,并且在字段变化时也能更容易地进行维护。

创建 POJO 类

假设我们查询的是用户信息,可以创建一个 User 类。

@Data
public class User {
    private int id;
    private String username;
    private String email;
}

使用 resultTyperesultMap 映射 POJO 类

在 MyBatis 的 XML 配置文件中,我们可以使用 resultTyperesultMap 将查询结果映射到 User 类。

方法一:使用 resultType

<select id="selectUserById" resultType="User">
  SELECT id, username, email
  FROM users
  WHERE id = #{id}
</select>

方法二:使用 resultMap(更灵活的方式)

使用 resultMap 更加灵活,尤其是当数据库字段与 POJO 属性不完全一致时,或者当我们需要处理复杂的映射关系时。

<resultMap id="userResultMap" type="User">
  <id property="id" column="id"/>
  <result property="username" column="username"/>
  <result property="email" column="email"/>
</resultMap>

<select id="selectUserById" resultMap="userResultMap">
  SELECT id, username, email
  FROM users
  WHERE id = #{id}
</select>

查询并使用结果

在 Java 代码中,通过 SqlSession 执行查询操作,MyBatis 会将结果映射到 User 对象中。

SqlSession sqlSession = sqlSessionFactory.openSession();
User user = sqlSession.selectOne("selectUserById", 1);

// 输出查询结果
System.out.println("User ID: " + user.getId());
System.out.println("Username: " + user.getUsername());
System.out.println("Email: " + user.getEmail());

总结与最佳实践:

  1. 避免使用 HashMapHashtable 作为查询结果:直接将查询结果映射到 HashMapHashtable 会丧失类型安全性,导致代码可维护性差,容易出错。
  2. 使用 POJO 类来接收查询结果:通过强类型的 POJO 类进行映射,可以确保类型安全和更好的代码可读性。
  3. 使用 resultMapresultType 映射查询结果:当数据库字段与 POJO 类属性一致时,可以直接使用 resultType;如果需要更复杂的字段映射,则使用 resultMap 来显式指定字段与属性的映射关系。
  4. 类型安全的好处
    • 编译时检查:使用 POJO 类可以让编译器检查类型,避免运行时发生类型转换错误。
    • 代码可读性和可维护性:POJO 类使得字段更具语义,代码更容易理解和维护。
    • 数据库字段变化的可控性:当数据库字段发生变化时,修改 POJO 类中的属性名或者添加新字段可以更容易地保持同步。

【强制】更新数据表记录时,必须同时更新记录对应的update_time字段值为当前时间。

在数据库表中更新记录时,通常会要求同时更新 update_time 字段,以记录数据的最后更新时间。这是一个常见的做法,可以通过触发器、应用逻辑或者在 SQL 语句中显式设置来实现。

使用 SQL 语句进行更新

假设我们有一个 users 表,表结构如下:

Column

Type

id

INT

username

VARCHAR

email

VARCHAR

update_time

DATETIME

每次更新用户记录时,我们希望同时更新 update_time 字段为当前时间。

更新 SQL 语句

在 MyBatis 中,我们可以通过 UPDATE 语句来更新记录,并且使用 NOW()(MySQL 的当前时间函数)来更新 update_time 字段。

<update id="updateUser" parameterType="User">
  UPDATE users
  SET username = #{username},
  email = #{email},
  update_time = NOW()
  WHERE id = #{id}
</update>

解释:

  • usernameemail 是要更新的字段。
  • update_time = NOW() 会将 update_time 字段的值设置为当前时间。
  • WHERE id = #{id} 用于指定更新的记录。

使用触发器(Trigger)

如果你希望在数据库层面自动更新 update_time 字段,可以使用数据库触发器来实现。在每次更新某个记录时,触发器会自动更新 update_time 字段。

DELIMITER $$

CREATE TRIGGER before_user_update
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
    SET NEW.update_time = NOW();
END$$

DELIMITER ;

解释:

  • 这个触发器会在 users 表的每次 UPDATE 操作之前触发。
  • NEW.update_time = NOW() 将把 update_time 字段设置为当前时间。
  • 这样,每次更新 users 表中的数据时,update_time 字段都会自动更新。

推荐做法:应用层和数据库层的配合,即通过 SQL 显式更新 update_time 字段,同时也可以使用触发器作为补充。这样能确保数据一致性和准确性。

【推荐】不要写一个大而全的数据更新接口。传入为POJO类,不管是不是自己的目标更新字 段,都进行updatetablesetc1=value1,c2=value2,c3=value3; 这是不对的。执行SQL时, 不要更新无改动的字段,一是易出错;二是效率低;三是增加 binlog 存储。

你提到的“不要写一个大而全的数据更新接口”实际上是在强调 只更新实际发生改变的字段,这有助于避免更新无关字段,提高数据库操作的效率,减少出错的风险,并且避免增加不必要的日志存储(如 binlog)或数据传输负担。下面是对这个问题的详细解释和示例。

问题分析:

  • 易出错:如果你更新了所有字段(即使字段没有变化),可能会在无意中覆盖掉正确的值,或者导致字段值的错误。
  • 效率低:如果不必要的字段被更新,数据库需要进行不必要的操作,这会浪费资源。
  • 增加 binlog 存储:在 MySQL 等数据库中,所有更新操作都会被记录到 binlog 中。无必要的字段更新会导致更多的 binlog 记录,增加存储和网络负担。

推荐做法:

  1. 只更新变动的字段:动态检查每个字段是否发生变化,如果发生了变化才执行更新操作。
  2. 使用 UPDATE 语句时避免不必要的字段更新
  3. 提供灵活的更新接口:根据传入的参数只更新实际发生变化的字段。

手动检查字段变化(适用于简单的更新逻辑)

当 POJO 类的字段发生变化时,动态构建 UPDATE 语句,只有发生变化的字段才会被更新。

POJO 类示例:假设我们有一个 User 类,包含以下属性:

@Data
public class User {
    private Integer id;
    private String username;
    private String email;
    private Integer age;
    private Date updateTime;

    // Getters and Setters
}

更新接口:可以使用 MyBatis 的动态 SQL 标签 <set> 来仅更新实际发生变化的字段。

<update id="updateUser" parameterType="User">
  UPDATE users
  <set>
    <if test="username != null">username = #{username},</if>
    <if test="email != null">email = #{email},</if>
    <if test="age != null">age = #{age},</if>
    <if test="updateTime != null">update_time = #{updateTime},</if>
  </set>
  WHERE id = #{id}
</update>

解释:

  • <set> 标签:MyBatis 中的 <set> 标签会自动去掉末尾多余的逗号。
  • <if> 标签:用于判断字段是否为空或者是否发生变化,如果发生变化,则更新该字段。
  • 如果字段值发生变化,SQL 语句会更新该字段;如果没有变化(字段值为 null),则不会更新。

【参考】@Transactional事务不要滥用。事务会影响数据库的QPS,另外使用事务的地方需 要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。

【参考】<isEqual>中的 compareValue 是与属性值对比的常量,一般是数字,表示相等时 带上此条件;<isNotEmpty>表示不为空且不为 null 时执行;<isNotNull>表示不为 null 值 时执行。

在 MyBatis 中,<isEqual>, <isNotEmpty>, 和 <isNotNull> 是常用的动态 SQL 标签,用于根据不同条件生成 SQL 语句的不同部分。它们通常用于 WHERE 子句中,来控制 SQL 语句的生成。以下是每个标签的解释及其使用示例:

<isEqual>:用于判断字段是否等于某个常量(如 1),条件成立时才会生成对应的 SQL。
<isNotEmpty>:检查属性是否不为空且不为 null。如果属性的值不为空或 null,则会加入 SQL 条件。
<isNotNull>:检查属性是否不为 null。如果属性不为 null,则会加入 SQL 条件。
<select id="selectUserByCondition" resultType="User">
    SELECT id, username, email, is_active
    FROM users
    WHERE 1 = 1
    <isEqual property="isActive" compareValue="1">
        AND is_active = 1
    </isEqual>
    <isNotEmpty property="username">
        AND username = #{username}
    </isNotEmpty>
    <isNotNull property="email">
        AND email IS NOT NULL
    </isNotNull>
</select>

博文参考

《阿里巴巴java开发规范》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/940319.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

springboot462学生心理压力咨询评判(论文+源码)_kaic

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装学生心理压力咨询评判软件来发挥其高效地信息处理的作用&am…

练习题:一维数组

练习题 第一题 键盘录入一组数列&#xff0c;利用冒泡排序将数据由大到小排序 代码 #include <stdio.h>int arr_home01() {int arr[10];int i,j,temp;printf("请输入10个测试整数&#xff1a;\n");int len sizeof(arr) / sizeof(arr[0]);for(i 0;i < …

基于LR/GNB/SVM/KNN/DT算法的鸢尾花分类和K-Means算法的聚类分析

花瓣轮廓&#xff1a; 分类与聚类 使用各种模型进行鸢尾花分类和聚类 1. | 介绍 &#x1f44b; &#x1f914; 数据集问题 鸢尾花分类项目是使用简单数据集实现机器学习模型的实际演示。数据集本身包含有关花瓣和萼片大小的信息&#xff0c;包括鸢尾属物种。通过分析鸢尾花的…

创新驱动医疗变革:SSM+Vue 医院预约挂号系统的设计与实践

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理医院预约挂号系统的相关信息成为必然。开发…

【Java】HashMap的简单教程

HashMap 是 Java 中最常用的数据结构之一&#xff0c;属于 java.util 包&#xff0c;主要用于以键值对&#xff08;key-value&#xff09;形式存储数据。 基本用法 1.创建 HashMap 使用泛型&#xff0c;存储键值对。 import java.util.HashMap;HashMap<KeyType, ValueTy…

windwos defender实现白名单效果(除了指定应用或端口其它一律禁止)禁止服务器上网

一、应用场景说明 当我们的一台windows服务器中毒&#xff0c;变成别人肉鸡&#xff0c;不断向外请示非法网站或攻击其它服务器。 要彻底清除相关木马或病毒往往需要的时间比较长&#xff0c;比较有效的方法是禁止服务器主动向外发包除了网站端口和远程程序除外。 其实这就是一…

免费送源码:Java+ssm++MVC+HTML+CSS+MySQL springboot 社区医院信息管理系统的设计与实现 计算机毕业设计原创定制

摘 要 随着互联网趋势的到来&#xff0c;各行各业都在考虑利用互联网将自己推广出去&#xff0c;最好方式就是建立自己的互联网系统&#xff0c;并对其进行维护和管理。在现实运用中&#xff0c;应用软件的工作规则和开发步骤&#xff0c;采用Java技术建设社区医院信息管理系统…

AI的进阶之路:从机器学习到深度学习的演变(四)

AI的进阶之路&#xff1a;从机器学习到深度学习的演变&#xff08;三&#xff09; 五、深度学习的应用领域 深度学习的应用领域广泛&#xff0c;涵盖了计算机视觉、自然语言处理、语音识别和推荐系统等多个方面。以下将详细探讨这些关键应用领域&#xff0c;展示深度学习在不同…

electron-vite【实战】自定义标题栏【组件封装】(含异形标题栏,指定区域拖拽,窗口置顶,窗口最小化,窗口最大化,取消最大化,隐藏窗口到托盘等)

效果预览 技术要点 透明背景 src/main/index.ts 的 new BrowserWindow 中添加 transparent: true, // 设置窗口背景透明frame: false, // 隐藏窗口边框仅图标和标题部分可拖拽 仅图标和标题部分添加样式 drag .drag {-webkit-app-region: drag; }图标与标题栏的融合 标题栏的…

[react] 获取ant组件ref用ts如何定义?

获取ant的轮播图组件, 我用ts如何定义? import React, { ElementRef } from react; const lunboRef useRef<ElementRef<typeof Carousel>>(null); <Carousel autoplay ref{lunboRef}> 这样就行了! ,然后点一下看看.弹出提示了 当然你还可以用ant内置的G…

springboot中Controller内文件上传到本地以及阿里云

上传文件的基本操作 <form action"/upload" method"post" enctype"multipart/form-data"> <h1>登录</h1> 姓名&#xff1a;<input type"text" name"username" required><br> 年龄&#xf…

python 曲线拟合,曲线拟合交点

目录 效果图: 源代码: 效果图: 源代码: import json import os import shutilimport cv2 import numpy as npfrom numpy.polynomial.polynomial import Polynomialdef calculate_distance(x1, y1, x2, y2):return np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)def get_new_g…

java 集合对象

Java 基础之集合_java集合继承关系图-CSDN博客 集合可以有序或无序&#xff0c;重复或不能重复&#xff0c;空或不能空。 List<> 集合&#xff0c;是有序&#xff0c;允许重复元素&#xff0c;允许空元素 1、ArrayList<> 非线程安全 2、LInkedList<> 非线…

【零基础学习UDS诊断测试】——0x27测试用例设计

从0开始学习CANoe使用 从0开始学习车载测试 相信时间的力量 星光不负赶路者,时光不负有心人。 目录 1.概述 2.响应情况 3.测试点解析 4.0x27诊断调查表 5.详细用例展示 1.概述 UDS(统一诊断服务)中的0x27服务,即安全访问服务(Security Access Service),其主要作用…

【BUG记录】Apifox 参数传入 + 号变成空格的 BUG

文章目录 1. 问题描述2. 原因2.1 编码2.2 解码 3. 解决方法 1. 问题描述 之前写了一个接口&#xff0c;用 Apifox 请求&#xff0c;参数传入一个 86 的电话&#xff0c;结果到服务器 就变成空格了。 Java 接收请求的接口&#xff1a; 2. 原因 2.1 编码 进行 URL 请求的…

视频直播点播平台EasyDSS推拉流技术结合无人机推流在道路交通巡检场景中的应用

随着城市化进程的加速&#xff0c;交通网络日益复杂&#xff0c;交通巡检工作面临着前所未有的挑战。传统的巡检方式往往依赖于人工巡查或地面监控设备&#xff0c;但这些方法存在巡检范围有限、效率低下等缺点。 无人机凭借其高空视野、灵活机动、实时监控等优势&#xff0c;…

git仓库的基本概念和流程以及一些基本命令

什么是版本库&#xff1f;版本库又名仓库&#xff0c;英文名repository,你可以简单的理解一个目录&#xff0c;这个目录里面的所有文件都可以被Git管理起来&#xff0c;每个文件的修改&#xff0c;删除&#xff0c;Git都能跟踪&#xff0c;以便任何时刻都可以追踪历史&#xff…

2025erp系统开源免费进销存系统搭建教程/功能介绍/上线即可运营软件平台源码

系统介绍 基于ThinkPHP与LayUI构建的全方位进销存解决方案 本系统集成了采购、销售、零售、多仓库管理、财务管理等核心功能模块&#xff0c;旨在为企业提供一站式进销存管理体验。借助详尽的报表分析和灵活的设置选项&#xff0c;企业可实现精细化管理&#xff0c;提升运营效…

2024技能大赛Vue流程复现

1. 关于版本的控制 vue/cli 5.0.8vscode 最新下载版本 2. 创建vuecli项目 若没有安装vuecli则可以先安装 npm install -g vue/cli # 默认下载最新版本。vue --version vue -V # 查看版本&#xff0c;两个选一 使用vuecli来创建一个新的vue项目&#xff0c;vs code打开…

Qwen2.5-7B-Instruct FastApi部署与调用

Qwen2.5-7B-Instruct FastApi 部署调用 环境准备 本文基础环境如下&#xff1a; ---------------- ubuntu 22.04 python 3.12 cuda 12.1 pytorch 2.3.0 ----------------本文默认学习者已安装好以上 Pytorch(cuda) 环境&#xff0c;如未安装请自行安装。 首先 pip 换源加速下…