4、CRUD基础操作:增删改查的标准写法、批量操作、事务的基本使用

好,咱们直接进入正题。CRUD 这四个字母,说白了就是软件和数据库打交道的日常。增删改查,哪个项目能离得开?我见过不少新手,上来就写原生 SQL,然后到处拼字符串,最后被 SQL 注入教做人。也有老手,ORM 用得飞起,但一遇到批量操作就卡壳,事务边界也搞不清楚。

今天这一章,我就带你把这些基础操作彻底捋一遍。咱们用 Python 的 SQLAlchemy 和 Java 的 Hibernate 来举例,但核心思想是通用的。你只要理解了这一套,换哪个 ORM 框架都差不多。

4.1 标准增删改查写法

先看最基础的。我个人习惯,写 CRUD 之前,先确认好 Session 或 EntityManager 的生命周期。别在循环里反复创建,也别忘记关闭。嗯,这是很多性能问题的根源。

4.1.1 新增(Create)

新增操作,ORM 框架会帮你把对象映射成 INSERT 语句。你只需要 new 一个对象,设置属性,然后调用 save 或 persist 方法。

Python (SQLAlchemy) 示例:

# 创建一个新用户
new_user = User(name="张三", email="zhangsan@example.com", age=28)

# 添加到会话并提交
session.add(new_user)
session.commit()

print(f"新增用户 ID: {new_user.id}")  # 提交后,id 自动回填

Java (Hibernate) 示例:

// 创建一个新用户
User newUser = new User();
newUser.setName("张三");
newUser.setEmail("zhangsan@example.com");
newUser.setAge(28);

// 持久化
entityManager.persist(newUser);
entityManager.getTransaction().commit();

System.out.println("新增用户 ID: " + newUser.getId()); // 提交后,id 自动回填
小提示: 注意看,提交之后对象的 id 字段会被自动填充。这是因为 ORM 框架帮你执行了 SELECT LAST_INSERT_ID() 或者类似的操作。你想想看,如果不用 ORM,你得手动写两条 SQL,多麻烦。

4.1.2 查询(Retrieve)

查询是 CRUD 里花样最多的。按主键查、按条件查、分页查、关联查。咱们先看最标准的几种。

按主键查询:

# Python
user = session.get(User, 1)  # 查 ID 为 1 的用户

// Java
User user = entityManager.find(User.class, 1); // 查 ID 为 1 的用户

条件查询:

# Python - 使用 filter
users = session.query(User).filter(User.age > 20).all()

// Java - 使用 JPQL
List<User> users = entityManager.createQuery(
    "SELECT u FROM User u WHERE u.age > :age", User.class)
    .setParameter("age", 20)
    .getResultList();
注意: 我曾经见过一个项目,查询所有用户时用了 .all() 或 .getResultList(),结果表里有 100 万条数据,直接把内存撑爆了。记住,生产环境一定要加分页!用 .limit() 和 .offset(),或者 setFirstResult() 和 setMaxResults()。

4.1.3 更新(Update)

更新操作,ORM 框架的核心理念是「脏检查」。你只要修改了对象的属性,框架会在提交时自动生成 UPDATE 语句。

# Python
user = session.get(User, 1)
user.age = 30  # 修改属性
session.commit()  # 自动生成 UPDATE

// Java
User user = entityManager.find(User.class, 1);
user.setAge(30); // 修改属性
entityManager.getTransaction().commit(); // 自动生成 UPDATE

为什么会这样?因为 ORM 框架在内存里维护了一份对象的「快照」。提交时一对比,发现 age 变了,就帮你 UPDATE 了。说白了,这就是 ORM 的「自动挡」特性。

4.1.4 删除(Delete)

删除操作最简单,但也最容易出问题。尤其是级联删除,一不小心就把关联表的数据全删了。

# Python
user = session.get(User, 1)
session.delete(user)
session.commit()

// Java
User user = entityManager.find(User.class, 1);
entityManager.remove(user);
entityManager.getTransaction().commit();
避坑指南: 我曾经在删除用户时,忘了检查这个用户有没有关联的订单。结果用户删了,订单表里留下一堆「孤儿数据」。后来我养成了一个习惯:删除之前,先确认级联策略。要么在模型里配好 cascade,要么手动先删关联数据。

4.2 批量操作

批量操作是性能优化的重灾区。你想想看,如果循环 1000 次,每次 insert 一条,那就要建立 1000 次网络连接,生成 1000 条 SQL。这效率,谁用谁知道。

正确的做法是:批量提交

4.2.1 批量新增

Python (SQLAlchemy) 示例:

users = []
for i in range(1000):
    users.append(User(name=f"用户{i}", email=f"user{i}@example.com"))

# 批量添加,每 100 条提交一次
session.bulk_save_objects(users)
session.commit()

Java (Hibernate) 示例:

// 开启批量处理
entityManager.setProperty("hibernate.jdbc.batch_size", 50);

for (int i = 0; i < 1000; i++) {
    User user = new User();
    user.setName("用户" + i);
    user.setEmail("user" + i + "@example.com");
    entityManager.persist(user);

    // 每 50 条 flush 一次,清空缓存
    if (i % 50 == 0) {
        entityManager.flush();
        entityManager.clear();
    }
}
entityManager.getTransaction().commit();
小提示: 注意 Java 示例里的 flush 和 clear。如果不做这一步,Hibernate 的一级缓存会越来越大,最后内存溢出。我建议你养成习惯:批量操作时,定期清空缓存。

4.2.2 批量更新与删除

批量更新和删除,千万别用循环里逐条操作。ORM 框架通常提供了「批量更新」的 API,直接生成一条 UPDATE 或 DELETE 语句。

# Python - 批量更新
session.query(User).filter(User.age < 18).update(
    {"status": "minor"}, synchronize_session=False
)
session.commit()

// Java - 批量更新(使用 JPQL)
entityManager.createQuery(
    "UPDATE User u SET u.status = 'minor' WHERE u.age < 18")
    .executeUpdate();
entityManager.getTransaction().commit();
注意: 批量更新时,synchronize_session 参数要小心。如果设为 False,ORM 不会同步内存中的对象状态。你想想看,如果后面又用到了这些对象,可能会读到旧数据。我一般建议:批量操作后,立即清空 Session 或 EntityManager 的缓存。

4.3 事务的基本使用

事务,说白了就是「要么全做,要么全不做」。转账、下单、库存扣减,这些场景都离不开事务。

4.3.1 事务的 ACID 特性

这个你肯定听过,但我还是想强调一下:

特性 含义 实际例子
原子性 事务里的操作,要么全部成功,要么全部回滚 转账时,扣钱和加钱必须同时成功或同时失败
一致性 事务前后,数据必须符合业务规则 转账后,总金额不变
隔离性 并发事务之间互不干扰 两个人同时转账,不会互相影响
持久性 事务提交后,数据永久保存 系统崩溃后,已提交的数据不会丢失

4.3.2 事务的标准写法

我个人习惯,事务一定要显式控制。别依赖框架的自动提交,那玩意儿在复杂场景下容易出问题。

Python (SQLAlchemy) 示例:

try:
    # 开启事务
    session.begin()

    # 业务操作
    user1 = session.get(User, 1)
    user1.balance -= 100

    user2 = session.get(User, 2)
    user2.balance += 100

    # 提交事务
    session.commit()
except Exception as e:
    # 回滚事务
    session.rollback()
    print(f"事务失败,已回滚: {e}")
finally:
    session.close()

Java (Hibernate) 示例:

EntityTransaction transaction = entityManager.getTransaction();
try {
    transaction.begin();

    // 业务操作
    User user1 = entityManager.find(User.class, 1);
    user1.setBalance(user1.getBalance() - 100);

    User user2 = entityManager.find(User.class, 2);
    user2.setBalance(user2.getBalance() + 100);

    transaction.commit();
} catch (Exception e) {
    if (transaction.isActive()) {
        transaction.rollback();
    }
    System.err.println("事务失败,已回滚: " + e);
} finally {
    entityManager.close();
}
避坑指南: 我曾经犯过一个错误:在事务里调用了远程 API。结果远程 API 超时了,事务一直没提交,数据库连接被占用了 30 秒。后来我学乖了:事务里只做数据库操作,远程调用、文件读写、消息发送,都放到事务外面。记住,事务要「短平快」。

4.3.3 事务的隔离级别

隔离级别决定了并发事务能看到什么数据。我直接给你一个经验值:

隔离级别 脏读 不可重复读 幻读 我推荐的使用场景
READ UNCOMMITTED 可能 可能 可能 几乎不用,除非你不在乎数据一致性
READ COMMITTED 不会 可能 可能 大多数业务系统,PostgreSQL 默认就是这个
REPEATABLE READ 不会 不会 可能 需要重复读取同一行数据的场景,MySQL InnoDB 默认
SERIALIZABLE 不会 不会 不会 金融、对账等强一致性场景,但性能最差

你想想看,如果你的系统并发不高,用默认的隔离级别就行。如果并发高,我建议用 READ COMMITTED,然后通过乐观锁或悲观锁来解决并发冲突。嗯,这个咱们后面章节会详细讲。

4.4 总结

好了,这一章的内容就这些。咱们回顾一下:

  • 标准 CRUD:记住 Session 的生命周期,脏检查机制,以及级联删除的坑。
  • 批量操作:别循环逐条操作,用批量 API,定期 flush 和 clear。
  • 事务:显式控制,短平快,别在事务里做远程调用。

下一章,咱们会深入 ORM 的关联映射。到时候我会讲讲「N+1 查询」这个经典问题,以及怎么用懒加载和预加载来优化。嗯,那才是真正考验 ORM 功底的地方。