设计模式与数据库:连接池中的享元、事务管理中的模板方法、ORM中的代理

数据库编程,说白了就是跟资源打交道。连接要省着用,事务要管好,对象关系映射要灵活。这三个场景,恰好对应了三个经典设计模式。我这些年做后端系统,几乎每天都在跟它们打交道。

今天咱们就聊聊:连接池里的享元模式、事务管理里的模板方法、ORM框架里的代理模式。这三个组合,是数据库编程的黄金搭档。

一、连接池中的享元模式

先说说连接池。你想想看,每次数据库操作都新建一个连接,那性能得多差?TCP三次握手、数据库认证,这些开销可不小。

享元模式的核心思想:共享细粒度对象,避免重复创建。连接池就是典型的享元池。

关键点:连接池中的连接对象是享元,它们被多个线程共享使用。但要注意,连接本身是有状态的(事务、隔离级别等),所以需要「借出-归还」机制。

我在项目中遇到过一个问题:连接池配置太小,高峰期大量请求等待连接,系统直接雪崩。后来我学乖了,连接池大小要压测确定,而且一定要设置等待超时。

连接池享元模式的核心结构

// 享元接口 - 数据库连接
public interface DbConnection {
    void executeQuery(String sql);
    void close();
}

// 具体享元 - 真实连接
public class RealConnection implements DbConnection {
    private String connectionId;
    private boolean inUse = false;
    
    public RealConnection(String id) {
        this.connectionId = id;
        // 模拟建立真实连接
        System.out.println("创建连接: " + id);
    }
    
    @Override
    public void executeQuery(String sql) {
        if (!inUse) {
            throw new RuntimeException("连接未激活");
        }
        System.out.println(connectionId + " 执行: " + sql);
    }
    
    @Override
    public void close() {
        // 不是真正关闭,而是归还到池中
        System.out.println(connectionId + " 归还到池");
    }
    
    public void markInUse() { this.inUse = true; }
    public void markFree() { this.inUse = false; }
}

// 享元工厂 - 连接池
public class ConnectionPool {
    private List<RealConnection> pool = new ArrayList<>();
    private static final int MAX_POOL_SIZE = 10;
    
    public ConnectionPool() {
        // 预创建连接
        for (int i = 0; i < 5; i++) {
            pool.add(new RealConnection("conn-" + i));
        }
    }
    
    public synchronized DbConnection getConnection() {
        for (RealConnection conn : pool) {
            if (!conn.isInUse()) {
                conn.markInUse();
                return conn;
            }
        }
        // 池满,等待或新建(这里简化处理)
        if (pool.size() < MAX_POOL_SIZE) {
            RealConnection newConn = new RealConnection("conn-" + pool.size());
            newConn.markInUse();
            pool.add(newConn);
            return newConn;
        }
        throw new RuntimeException("连接池已满");
    }
    
    public synchronized void releaseConnection(DbConnection conn) {
        if (conn instanceof RealConnection) {
            ((RealConnection) conn).markFree();
        }
    }
}

避坑指南:我曾经在连接池里犯过一个低级错误——没有做连接有效性检测。数据库重启后,池里的连接全断了,但状态还是「可用」。结果所有请求都拿到死连接,系统瘫痪了半小时。后来我加上了心跳检测和自动重连。

享元模式在连接池中的变体

实际生产环境中的连接池(比如HikariCP、Druid)比上面复杂得多。它们通常包含:

  • 空闲队列:存放空闲连接的容器
  • 活跃队列:正在使用的连接
  • 最小/最大连接数:控制资源使用
  • 连接泄漏检测:自动回收长时间未归还的连接

嗯,这里要注意:享元模式要求享元对象的状态是「外部化」的。连接池里的连接,其「是否被使用」这个状态就是外部状态,由连接池管理,而不是连接自身。

二、事务管理中的模板方法模式

事务管理,说白了就是「开启事务 → 执行业务逻辑 → 提交或回滚」这个固定流程。你想想看,不管是什么业务,事务的骨架都是一样的,变化的只是中间的业务逻辑。

这不就是模板方法模式的典型场景吗?

模板方法模式的核心:在父类中定义算法的骨架,让子类实现具体步骤。事务管理正好符合这个结构。

事务模板的经典实现

// 抽象模板 - 事务管理器
public abstract class TransactionTemplate {
    
    // 模板方法 - 定义事务骨架
    public final Object execute(TransactionCallback callback) {
        Object result = null;
        try {
            // 1. 开启事务
            beginTransaction();
            
            // 2. 执行业务逻辑(由子类或回调实现)
            result = callback.doInTransaction();
            
            // 3. 提交事务
            commitTransaction();
            
            return result;
        } catch (Exception e) {
            // 4. 异常时回滚
            rollbackTransaction();
            throw new RuntimeException("事务执行失败", e);
        } finally {
            // 5. 清理资源
            cleanup();
        }
    }
    
    // 抽象方法 - 由具体子类实现
    protected abstract void beginTransaction();
    protected abstract void commitTransaction();
    protected abstract void rollbackTransaction();
    protected abstract void cleanup();
}

// 回调接口 - 业务逻辑的抽象
public interface TransactionCallback {
    Object doInTransaction();
}

// 具体模板 - JDBC事务管理器
public class JdbcTransactionTemplate extends TransactionTemplate {
    private Connection connection;
    
    public JdbcTransactionTemplate(Connection conn) {
        this.connection = conn;
    }
    
    @Override
    protected void beginTransaction() {
        try {
            connection.setAutoCommit(false);
            System.out.println("JDBC事务已开启");
        } catch (SQLException e) {
            throw new RuntimeException("开启事务失败", e);
        }
    }
    
    @Override
    protected void commitTransaction() {
        try {
            connection.commit();
            System.out.println("JDBC事务已提交");
        } catch (SQLException e) {
            throw new RuntimeException("提交事务失败", e);
        }
    }
    
    @Override
    protected void rollbackTransaction() {
        try {
            connection.rollback();
            System.out.println("JDBC事务已回滚");
        } catch (SQLException e) {
            // 回滚失败通常只能记录日志
            System.err.println("回滚事务失败: " + e.getMessage());
        }
    }
    
    @Override
    protected void cleanup() {
        try {
            connection.setAutoCommit(true);
            connection.close();
        } catch (SQLException e) {
            // 忽略
        }
    }
}

// 使用示例
public class UserService {
    private TransactionTemplate template;
    
    public void transferMoney(String from, String to, double amount) {
        template.execute(() -> {
            // 业务逻辑 - 转账
            System.out.println("从 " + from + " 转账 " + amount + " 到 " + to);
            // 这里执行实际的SQL操作
            return null;
        });
    }
}

个人经验:我习惯在模板方法里加入「重试机制」。比如遇到死锁异常,自动重试3次。这个逻辑放在模板里,所有事务都能受益,不用每个业务自己写重试代码。

模板方法在Spring事务中的体现

Spring的@Transactional注解底层就是模板方法模式。它的TransactionInterceptor相当于模板,PlatformTransactionManager是具体实现。你只需要写业务逻辑,事务的开启、提交、回滚都由框架搞定。

说白了,模板方法模式把「不变」和「变化」分开了。事务骨架不变,业务逻辑变化。这个思想在框架设计中无处不在。

三、ORM中的代理模式

ORM框架(比如MyBatis、Hibernate)里,代理模式用得特别多。最典型的就是延迟加载(Lazy Loading)。

你想想看,查询一个订单时,要不要同时加载它的所有商品详情?如果订单有100个商品,每个商品还有图片、评论...一次性加载完,性能就崩了。

代理模式的核心:为真实对象提供一个代理,控制对真实对象的访问。延迟加载就是典型的「按需加载」。

ORM延迟加载的代理实现

// 真实主题 - 商品详情
public class ProductDetail {
    private String productId;
    private String description;
    private byte[] imageData;  // 大字段,加载成本高
    
    public ProductDetail(String id) {
        this.productId = id;
        // 模拟从数据库加载
        loadFromDatabase();
    }
    
    private void loadFromDatabase() {
        System.out.println("从数据库加载商品详情: " + productId);
        // 实际数据库查询...
        this.description = "商品描述...";
        this.imageData = new byte[1024 * 1024]; // 1MB图片
    }
    
    public String getDescription() { return description; }
    public byte[] getImageData() { return imageData; }
}

// 代理类 - 延迟加载代理
public class ProductDetailProxy extends ProductDetail {
    private String productId;
    private ProductDetail realDetail;
    private boolean loaded = false;
    
    public ProductDetailProxy(String id) {
        super(id);  // 注意:这里不会真正加载
        this.productId = id;
        // 代理初始化时不加载真实数据
        System.out.println("创建代理对象,尚未加载数据");
    }
    
    @Override
    public String getDescription() {
        ensureLoaded();
        return realDetail.getDescription();
    }
    
    @Override
    public byte[] getImageData() {
        ensureLoaded();
        return realDetail.getImageData();
    }
    
    private void ensureLoaded() {
        if (!loaded) {
            // 第一次访问时真正加载
            realDetail = new ProductDetail(productId);
            loaded = true;
            System.out.println("延迟加载完成");
        }
    }
}

// 使用示例
public class OrderService {
    public void displayOrderSummary(String orderId) {
        // 假设从ORM框架获取订单,商品详情是代理对象
        ProductDetailProxy proxy = new ProductDetailProxy("prod-123");
        
        // 只显示描述,不加载图片
        System.out.println("商品描述: " + proxy.getDescription());
        
        // 如果用户点击查看图片,才真正加载
        // byte[] image = proxy.getImageData(); // 此时才加载
    }
}

注意陷阱:我曾经在项目里遇到一个坑——在事务外访问延迟加载的属性,结果抛出了LazyInitializationException。这是因为Session已经关闭了,代理对象无法加载数据。解决方案要么是提前加载,要么是延长Session生命周期。

代理模式在ORM中的其他应用

除了延迟加载,代理模式在ORM中还有这些用途:

  • 脏检查:代理对象记录属性变化,在提交时自动生成UPDATE语句
  • 权限控制:代理检查当前用户是否有权访问某些字段
  • 缓存:代理先查缓存,缓存未命中再查数据库
  • 日志记录:代理自动记录SQL执行时间

说白了,代理模式就是在「调用方」和「真实对象」之间加了一层。这一层可以做很多事情,而且对调用方完全透明。

四、三种模式的协同关系

这三种模式在数据库编程中经常一起出现。我画了一张图来展示它们的关系:

设计模式与数据库:三种模式的协同 应用层(业务代码) 调用ORM代理对象 → 代理控制延迟加载、缓存、权限 ORM层(代理模式) 代理对象 → 延迟加载、脏检查、自动生成SQL 事务管理层(模板方法模式) 模板方法 → 开启事务 → 执行业务 → 提交/回滚 连接池层(享元模式) 共享连接对象 → 避免频繁创建/销毁

从这张图可以看出:

  • 连接池(享元)在最底层,提供可复用的数据库连接
  • 事务管理(模板方法)在中间层,控制事务的生命周期
  • ORM代理(代理模式)在最上层,为业务代码提供灵活的数据访问

这三层各司其职,又相互配合。连接池保证连接高效复用,事务管理保证数据一致性,ORM代理提供开发便利性。

我的建议:学习设计模式不要孤立地学。把它们放到真实场景中,看看它们怎么配合。比如你写一个数据库访问层,可以同时用到这三种模式。理解了它们的协同关系,你才能真正用好设计模式。

五、总结

今天聊了三个模式在数据库编程中的实战应用:

设计模式 数据库场景 核心作用 常见实现
享元模式 连接池 共享连接,避免重复创建 HikariCP、Druid、C3P0
模板方法模式 事务管理 固定事务骨架,灵活业务逻辑 Spring @Transactional、JTA
代理模式 ORM延迟加载 按需加载,透明增强 MyBatis懒加载、Hibernate代理

说白了,设计模式不是空中楼阁。它们是从实际开发中提炼出来的最佳实践。你写代码时遇到重复问题,不妨想想:有没有现成的模式可以套用?

我个人习惯是:先理解问题本质,再选择模式。不要为了用模式而用模式。模式是工具,不是目的。

嗯,今天就聊到这里。这些模式你平时肯定都用过,只是可能没意识到它们就是设计模式。下次写数据库代码时,可以留意一下——你正在用哪个模式?