Spring JDBC 【转】

以下内容转载和参考自 使用JDBC - 廖雪峰的官方网站

1、Spring JDBC

  Spring中包含支持JDBC的数据访问模块,其提供了简化的JDBC操作,不必手动释放资源,通过使用JdbcTemplate。如下所示,我们通过IoC创建并管理一个DataSource实例(DataSource使用HikariCP),一个JdbcTemplate实例,,然后就可以通过JdbcTemplate来执行SQL操作: 

@Configuration
@ComponentScan
@PropertySource("jdbc.properties") //读取数据库配置文件jdbc.properties
public class AppConfig {

    @Value("${jdbc.url}") //获取配置文件中jdbc.url属性值到jdbcUrl
    String jdbcUrl;

    @Value("${jdbc.username}") //获取配置文件中jdbc.username属性值到jdbcUsername
    String jdbcUsername;

    @Value("${jdbc.password}") //获取配置文件中jdbc.password属性值到jdbcPassword
    String jdbcPassword;

    @Bean //提供给IoC创建DataSource对象的方法
    DataSource createDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(jdbcUrl);
        config.setUsername(jdbcUsername);
        config.setPassword(jdbcPassword);
        config.addDataSourceProperty("autoCommit", "true");
        config.addDataSourceProperty("connectionTimeout", "5");
        config.addDataSourceProperty("idleTimeout", "60");
        return new HikariDataSource(config);
    }

    @Bean //提供给IoC创建JdbcTemplate对象的方法
    JdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

  配置文件jdbc.properties:

# 数据库文件名为testdb:
jdbc.url=jdbc:hsqldb:file:testdb

# Hsqldb默认的用户名是sa,口令是空字符串:
jdbc.username=sa
jdbc.password=

   因为我们要使用@PostConstruct注解,以及使用的JDBC连接池是HikariCP,所以除了Spring和JDBC依赖外,还需要Annotation和HikariCP相关依赖。另外我们需要添加使用的MySQL或者Oracle等数据库的驱动依赖,我们这里使用的是HSQLDB这个数据库:

<dependencies>

    <!--Spring Context-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.0.RELEASE</version>
    </dependency>

	<!--Spring JDBC-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.0.RELEASE</version>
    </dependency>

	<!--@PostConstruct注解-->
    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>1.3.2</version>
    </dependency>

	<!--JDBC连接池-->
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>3.4.2</version>
    </dependency>

	<!--JDBC驱动-->
	<dependency>
        <groupId>org.hsqldb</groupId>
        <artifactId>hsqldb</artifactId>
        <version>2.5.0</version>
    </dependency>

</dependencies>

    在需要访问数据库的类中,注入JdbcTemplate,然后可以通过JdbcTemplate的queryForObject()来执行SQL查询、update()来执行插入、更新和删除操作:

@Component
public class DatabaseOper {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void init() {
        //创建数据库表
        //int	update(String sql) 返回值为生效的行数
        jdbcTemplate.update("CREATE TABLE IF NOT EXISTS users (" //
                + "id BIGINT IDENTITY NOT NULL PRIMARY KEY, " //
                + "email VARCHAR(100) NOT NULL, " //
                + "password VARCHAR(100) NOT NULL, " //
                + "name VARCHAR(100) NOT NULL, " //
                + "UNIQUE (email))");
    }

    public void updateUser(User user) {
    //int	update(String sql, Object... args/*SQL语句参数*/) 返回值为更新的行数 
    if (1 != jdbcTemplate.update("UPDATE users SET name = ? WHERE id=?", user.getName(), user.getId())) {
        throw new RuntimeException("User not found by id");
    }
}

    public User getUserByEmail(String email) {
        User user = null;
        //T queryForObject(String sql /*SQL语句*/, Object[] args/*SQL语句的参数,不需要可以为null*/, RowMapper<T> rowMapper/*包含结果的Lambda表达式*/)
        user = jdbcTemplate.queryForObject("SELECT * FROM users WHERE email = ?", new Object[] { email },
                (ResultSet rs/*结果集*/, int rowNum/*行号?行数?*/) -> {
                    //Lambda表达式可以返回一个对象
                    return new User(
                            rs.getLong("id"), // id
                            rs.getString("email"), // email
                            rs.getString("password"), // password
                            rs.getString("name")); // name
                });

        return user;
    }
}

  如果SQL查询结果的结构恰好和JavaBean的属性名称一致,那么使用BeanPropertyRowMapper就可以直接把一行记录按列名转换为JavaBean。如果数据库表中列名与JavaBean不一致的话,也可以在查询的时候指定列的别名与JavaBean中一致,如"SELECT id, email, office_address AS workAddress, name FROM users WHERE email = ?"。

    public User getUserByEmail(String email) {
        User user = null;
        user = jdbcTemplate.queryForObject("SELECT * FROM users WHERE email = ?", new Object[] { email },
                new BeanPropertyRowMapper<>(User.class));
        return user;
    }

  如果SQL查询返回的是多行记录,可以使用query()方法:

    public List<User> getUsers(int pageIndex) {
        int limit = 100;
        int offset = limit * (pageIndex - 1);
        //<T> List<T>	query(String sql, Object[] args, RowMapper<T> rowMapper)
        return jdbcTemplate.query("SELECT * FROM users LIMIT ? OFFSET ?", new Object[] { limit, offset },
                new BeanPropertyRowMapper<>(User.class));
    }

  可以看到,使用Spring JDBC可以直接使用JdbcTemplate来进行SQL操作,不用再使用Connection、Statement / PreparedStatementJdbcTemplate对象,如果我们需要自己手动操作Jdbc的Connection或PreparedStatement来执行SQL语句的话,可以使用jdbcTemplate的execute()方法:

    public User getUserById(long id) {
        //<T> T	execute(ConnectionCallback<T> action)
        return jdbcTemplate.execute((Connection conn) -> {
            //回调结束后Connection自动释放,在回调中自己创建的PreparedStatement、ResultSet必须用try(...)手动释放
            try (var ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
                ps.setObject(1, id);
                try (var rs = ps.executeQuery()) {
                    if (rs.next()) {
                        return new User(
                                rs.getLong("id"),
                                rs.getString("email"),
                                rs.getString("password"),
                                rs.getString("name"));
                    }

                    throw new RuntimeException("user not found by id.");
                }
            }
        });
    }

    public User getUserByName(String name) {
        //<T> T	execute(String sql, PreparedStatementCallback<T> action)
        return jdbcTemplate.execute("SELECT * FROM users WHERE name = ?", (PreparedStatement ps) -> {
            // PreparedStatement会在回调后自动释放,在回调中自己创建的ResultSet必须用try(...)手动释放
            ps.setObject(1, name);
            try (var rs = ps.executeQuery()) {
                if (rs.next()) {
                    return new User(
                            rs.getLong("id"),
                            rs.getString("email"),
                            rs.getString("password"),
                            rs.getString("name"));
                }
                throw new RuntimeException("user not found by id.");
            }
        });
    }

    如果某一列是自增列(例如自增主键),我们需要获取其插入后的自增值的话,那么JdbcTemplate提供了一个KeyHolder来进行这一操作:

    public User register(String email, String password, String name) {
        KeyHolder holder = new GeneratedKeyHolder(); // 创建一个KeyHolder
        //int	update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder)
        int iRet = jdbcTemplate.update((conn) -> {
                    var ps = conn.prepareStatement("INSERT INTO users(email,password,name) VALUES(?,?,?)",
                            Statement.RETURN_GENERATED_KEYS); // 创建PreparedStatement时,必须指定RETURN_GENERATED_KEYS
                    ps.setObject(1, email);
                    ps.setObject(2, password);
                    ps.setObject(3, name);
                    return ps;
                }, holder);

        if(iRet != 1){
            throw new RuntimeException("Insert failed.");
        }

        // 从KeyHolder中获取返回的自增值
        return new User(holder.getKey().longValue(), email, password, name);
    }

2、使用事务

  Spring提供了一个PlatformTransactionManager来表示事务管理器,而事务由TransactionStatus表示,Spring除了提供标准JDBC事务外,还支持分布式事务JTA(Java Transaction API),如果仅使用JDBC事务的话,事务管理器的实际类型是DataSourceTransactionManager:

@Configuration
@ComponentScan
@PropertySource("jdbc.properties")
public class AppConfig {
    ......
    @Bean
    PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}


@Component
public class Test {
    @Autowired
    private PlatformTransactionManager txManager;
    @Autowired
    JdbcTemplate jdbcTemplate;

    public void func(){
        TransactionStatus tx = null;
        try {
            tx = txManager.getTransaction(new DefaultTransactionDefinition()); // 开启事务

            jdbcTemplate.update("...");
            jdbcTemplate.update("...");

            txManager.commit(tx); // 提交事务
        } catch (RuntimeException e) {
            txManager.rollback(tx); // 回滚事务
            throw e;
        }
    }
}

    也可以使用声明式事务来简化事务代码,对需要成为事务的方法或者所有public方法都需要成为事务方法的类,使用@EnableTransactionManagement和@Transactional注解即可。

@Configuration
@ComponentScan
@EnableTransactionManagement // 启用声明式事务
@PropertySource("jdbc.properties")
public class AppConfig {
    ......
    @Bean
    PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

Component
public class UserService {
    @Transactional // 此public方法自动具有事务支持
    public User register(String email, String password, String name) {
       ...
    }
}

@Component
@Transactional // 此类的所有public方法自动具有事务支持
public class UserService {
    ...
}

    声明式事务的原理仍然是AOP代理,即通过自动创建Bean的Proxy实现:

public class UserService$$EnhancerBySpringCGLIB extends UserService {
    UserService target;
    PlatformTransactionManager txManager;
    ......
    public User register(String email, String password, String name) {
        TransactionStatus tx = null;
        try {
            tx = txManager.getTransaction(new DefaultTransactionDefinition());
            target.register(email, password, name);
            txManager.commit(tx);
        } catch (RuntimeException e) {
            txManager.rollback(tx);
            throw e;
        }
    }
    ......
}

    在事务方法中如果想要手动回滚事务的话,抛出RuntimeException或其子类对象即可,如果要针对Checked Exception回滚事务,需要在@Transactional注解中指定:

    @Transactional(rollbackFor = {RuntimeException.class, IOException.class}) //在抛出RuntimeException或IOException时,事务将回滚
    public buyProducts(long productId, int num) {
    ...
        if (store < num) {
            // 库存不够,购买失败,回滚事务:
            throw new IllegalArgumentException("No enough products");
        }
    ...
    }

    如果一个方法A是事务方法,在A中又调用了一个事务方法B,那么默认情况下B事务方法会融合到A事务方法中,即相当于B方法中没有了事务。我们也可以改变事务的传播模型,通过@Transactional注解,默认的事务传播级别是REQUIRED,还有一些其他的传播级别

      REQUIRED:如果当前没有事务,就创建一个新事务,如果当前有事务,就加入到当前事务中执行。

  SUPPORTS:表示如果有事务,就加入到当前事务,如果没有,那也不开启事务执行。这种传播级别可用于查询方法,因为SELECT语句既可以在事务内执行,也可以不需要事务;

  MANDATORY:表示必须要存在当前事务并加入执行,否则将抛出异常。这种传播级别可用于核心更新逻辑,比如用户余额变更,它总是被其他事务方法调用,不能直接由非事务方法调用;

  REQUIRES_NEW:表示不管当前有没有事务,都必须开启一个新的事务执行。如果当前已经有事务,那么当前事务会挂起,等新事务完成后,再恢复执行;

  NOT_SUPPORTED:表示不支持事务,如果当前有事务,那么当前事务会挂起,等这个方法执行完成后,再恢复执行;

  NEVER:和NOT_SUPPORTED相比,它不但不支持事务,而且在监测到当前有事务时,会抛出异常拒绝执行;

  NESTED:表示如果当前有事务,则开启一个嵌套级别事务,如果当前没有事务,则开启一个新事务。

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Product A() {
        ...
        B()
        ...
    }

   如果在A事务方法中使用另一个线程来执行B方法,那么将分别开启A和B两个完全独立的事务,即事务的默认传播模型或者我们设置的传播模型会失效。因此,事务能正确传播的前提是,方法调用是在一个线程内才行,换句话说,事务只能在当前线程传播,无法跨线程传播:

    @Transactional
    public void A() {
        ...
        new Thread(() -> {
            B();
        }).start();
        ...
    }

3、DAO

  编写数据访问层的时候,可以使用DAO模式,DAO即Data Access Object的缩写,如下针对数据库中的用户表Users编写了一个DAO模式的类:

public class User{ //对应Users表中一条记录
    public int id;
    public string name;
    public int age;
    ...
}

public class UserDao { //Users表操作类

    @Autowired
    JdbcTemplate jdbcTemplate;

    User getUserById(long id) {
        ...
    }

    List<User> getUsers(int page) {
        ...
    }

    User createUser(User user) {
        ...
    }

    User updateUser(User user) {
        ...
    }

    void deleteUser(User user) {
        ...
    }
}

  Spring提供了一个JdbcDaoSupport类,用于简化DAO的实现:

ublic abstract class JdbcDaoSupport extends DaoSupport {

    private JdbcTemplate jdbcTemplate;

    public final void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        initTemplateConfig();
    }

    public final JdbcTemplate getJdbcTemplate() {
        return this.jdbcTemplate;
    }

    ...
}
public class UserDao extends JdbcDaoSupport {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @PostConstruct
    public UserDao() {
        super.setJdbcTemplate(jdbcTemplate);
    }

    public User getUserById(long id) {
        return getJdbcTemplate().queryForObject(
                "SELECT * FROM users WHERE id = ?",
                new BeanPropertyRowMapper<>(User.class),
                id);
    }
    
    ......
}

    我们可以编写一个样板类,有需要的类从这个样板类继承即拥有了样板类中的SQL操作方法:

public abstract class AbstractDao<T> extends JdbcDaoSupport { //泛型T应该传入记录类的类型
    private String table; //表名
    private RowMapper<T> rowMapper; //保存查询结果

    public AbstractDao() {
        Class<T> entityClass = getParameterizedType(); // 获取当前泛型T的类型
        this.table = entityClass.getSimpleName().toLowerCase() + "s"; //数据库中的表名是"记录类名称 + s"
        this.rowMapper = new BeanPropertyRowMapper<>(entityClass);
    }

    public T getById(long id) {
        return getJdbcTemplate().queryForObject("SELECT * FROM " + table + " WHERE id = ?", this.rowMapper, id);
    }

    public void deleteById(long id) {
        getJdbcTemplate().update("DELETE FROM " + table + " WHERE id = ?", id);
    }

    ...

    private Class getParameterizedType(){
        Type type = getClass().getGenericSuperclass();
        Type[] types = ((ParameterizedType)type).getActualTypeArguments();
        Class<T> clazz = (Class<T>) types[0];
        return clazz;
    }
}


public class User{
    public int id;
    public string name;
    public int age;
    ...
}

@Component
@Transactional
public class UserDao extends AbstractDao<User> {
    // 自动有了下列方法:
    // User getById(long)
    // void deleteById(long)
    //...
}


 public class Book{
    public int id;
    public string name;
    public int pageNum;
    ...
}

@Component
@Transactional
public class BookDao extends AbstractDao<Book> {
    // 自动有了下列方法:
    // Book getById(long)
    // void deleteById(long)
    //...
}

4、总结

   通过Spring JDBC,只需要创建一个JdbcTemplate就可以通过它来执行SQL操作。

   使用Spring JDBC进行SQL查询的话,可以将用来保存查询结果的JavaBean中的属性名(成员名)设置成与数据库表中列名一致,这样可以直接将查询结果保存到JavaBean对象中。

   Spring JDBC支持事务操作。

   DAO模式即根据一定规则编写访问数据库表的类。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值