Loading

Spring实战 十 连接数据库

扯dz

Spring提供了jdbcTemplate简化数据库操作。使用JDBC原生来开发数据库难受的一批,只有一个SQLException让我们不知道发生了什么问题

以下是Spring提供的异常和JDBC的异常对照表

而且Spring的异常都是运行时异常,不强制我们必须对异常进行处理,其实大部分SQL异常我们都没办法处理。

还有就是jdbc的模板代码,都写过,即使只需要编写一行插入语句也要几十行模板代码。Spring的jdbcTemplate使用了模板设计模式,把构造SQL语句和将ResultSet转换成对象这两个核心操作留给我们,其他模板代码由Spring执行。

这是使用jdbcTemplate后的查询代码

数据源

既然要连接数据库,就要有数据源。Spring支持多种数据源

  1. 通过JNDI查找的数据源
  2. JDBC驱动中定义的数据源
  3. 连接池的数据源

连接池数据源

这里只介绍连接池的数据源。选用c3p0连接池,mysql数据库和一个用于开发环境的h2嵌入式数据库,需要引入的依赖如下


<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>${spring.version}</version>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.3.172</version>
</dependency>
<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>${c3p0.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql.connector.version}</version>
</dependency>

然后就是定义数据源,在WebConfig.java类上添加了@PropertySource注解引入外部配置文件,然后通过@Value来将对应的配置绑定在参数上。

@Bean
public DataSource dataSource(
        @Value("${jdbc.url}") String url,
        @Value("${jdbc.username}") String username,
        @Value("${jdbc.password}") String password,
        @Value("${jdbc.driverClass}") String driverClass,
        @Value("${jdbc.initialSize}") Integer initialSize,
        @Value("${jdbc.maxPoolSize}") Integer maxPoolSize,
        @Value("${jdbc.minPoolSize}") Integer minPoolSize,
        @Value("${jdbc.maxIdleTime}") Integer maxIdleTime
) throws PropertyVetoException {
    ComboPooledDataSource dataSource = new ComboPooledDataSource();
    dataSource.setJdbcUrl(url);
    dataSource.setUser(username);
    dataSource.setPassword(password);
    dataSource.setDriverClass(driverClass);
    dataSource.setInitialPoolSize(initialSize);
    dataSource.setMaxPoolSize(maxPoolSize);
    dataSource.setMinPoolSize(minPoolSize);
    dataSource.setMaxIdleTime(maxIdleTime);
    return dataSource;
}

基于profile配置数据源

使用@Profile注解定义在不同环境下的数据源,在开发环境下使用h2嵌入式数据库。

@Bean
@Profile("dev")
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:schema.sql")
            .addScript("classpath:test-data.sql")
            .build();
}

@Bean
@Profile("prod")
public DataSource dataSource(
        @Value("${jdbc.url}") String url,
        @Value("${jdbc.username}") String username,
        @Value("${jdbc.password}") String password,
        @Value("${jdbc.driverClass}") String driverClass,
        @Value("${jdbc.initialSize}") Integer initialSize,
        @Value("${jdbc.maxPoolSize}") Integer maxPoolSize,
        @Value("${jdbc.minPoolSize}") Integer minPoolSize,
        @Value("${jdbc.maxIdleTime}") Integer maxIdleTime
) throws PropertyVetoException {
    System.out.println("CREATING DATASOURCE c3p0");
    ComboPooledDataSource dataSource = new ComboPooledDataSource();
    dataSource.setJdbcUrl(url);
    dataSource.setUser(username);
    dataSource.setPassword(password);
    dataSource.setDriverClass(driverClass);
    dataSource.setInitialPoolSize(initialSize);
    dataSource.setMaxPoolSize(maxPoolSize);
    dataSource.setMinPoolSize(minPoolSize);
    dataSource.setMaxIdleTime(maxIdleTime);
    return dataSource;
}

基于我们这个方法配置的嵌入式数据库h2会在程序启动时在内存中创建数据库,并且执行schema.sqltest-data.sql,这两个文件不贴出来了。

然后就是设置当前激活的profile,在java配置中有两种方式设置这个profile,如下是其中一种。

public class SpittrWebApplicationInitializer
    extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        super.onStartup(servletContext);
        // 设置环境为dev
        servletContext.setInitParameter("spring.profiles.active", "dev");
    }

    // ...
}

jdbcTemplate

下面就是使用jdbcTemplate进行开发了。

先定义一个jdbcTemplate的Bean

@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
    return new JdbcTemplate(dataSource);
}

Repository

Repository是我们程序中定义的持久层接口,接口中提供最基本的数据库访问,比如如下两个接口。

public interface SpitterRepository {
    int save(Spitter spitter);
    Spitter getOne(Long id);
    Spitter getOneByUserName(String username);
}
public interface SpittleRepository {
    List<Spittle> findSpittles(long max, int count);
    Spittle findOne(long id);
}

我们的Spittr程序之前就是通过这两个接口来操作数据访问层的。在学习第十章之前,我们都没有连接过数据库,但是我们的程序依旧可以使用虚拟的数据正常运行,就像真的有数据库在那一样。这个好处就是接口带来的。

程序只知道我们有一个SpitterRepository接口,并且能通过save方法保存一个Spitter对象,通过getOne方法来根据id获取一个Spitter对象等,但是对于实现类是如何实现的,程序并不知道也不关心,这才给了我们机会使用虚拟的数据。

贴出之前SpittleRepository实现类的代码:

@Component
public class SpittleRepositoryImpl implements SpittleRepository{
    private List<Spittle> spittles = Arrays.asList(
            new Spittle(1l,"First Spittle!!!!", new Date(), null, null),
            new Spittle(2l,"Another Spittle!!!", new Date(), null, null),
            new Spittle(3l,"Spittle!! Spittle!! Spittle!!", new Date(), null, null),
            new Spittle(4l,"Spittles go forth!!", new Date(), null, null)
    );

    @Override
    public List<Spittle> findSpittles(long max, int count) {
        return spittles;
    }

    @Override
    public Spittle findOne(long id) {
        return spittles.stream().filter(s->s.getId().equals(id)).findFirst().get();
    }
}

很简单,在这个类中虚拟了四条数据,并且findSpittles方法直接简单粗暴的返回了全部四条数据,忽略了参数。findOne方法在虚拟的四条数据中进行过滤筛选,返回匹配的数据。

接口让我们在还没有学习,没有将数据库系统接入到程序中之前,不影响其他功能的开发。这便是提供Repository接口的好处。

使用jdbcTemplate实现Repository接口

用于开发的嵌入式数据库和用于生产的mysql数据库都已经接入到程序中,没理由再使用这些虚拟的数据。

下面是使用jdbcTemplate来实现的SpittleRepository,这其中有一些需要注意的点

@Repository
@Primary
@DependsOn("jdbcTemplate")
public class JdbcSpittleRepository implements SpittleRepository{

    JdbcOperations jdbcOperations;
    SpittleRowMapper mapper;

    @Autowired
    public JdbcSpittleRepository(JdbcOperations operations) {
        this.jdbcOperations = operations;
        this.mapper = new SpittleRowMapper();
    }

    @Override
    public List<Spittle> findSpittles(long max, int count) {
        return jdbcOperations.query(
                "SELECT * FROM spittle WHERE id <= ? LIMIT 0, ?",
                mapper,
                max, count
        );
    }

    @Override
    public Spittle findOne(long id) {
        return jdbcOperations.queryForObject(
                "SELECT * FROM spittle WHERE id=?",
                mapper,
                id
        );
    }
}
  1. @Repository注解让该类作为Bean被Spring扫描到,其实它就是一个@Component
  2. @Primary注解告诉Spring如果遇到多个SpittleRepository实现类,使用这个,这样我们就不需要删除之前的实现类以防以后还会使用到
  3. @DependsOn注解表明该Bean依赖名为jdbcTemplate的Bean。默认情况下的加载顺序好像是@Configuration中的@Bean晚于这些@Component声明的Bean被加载,所以该类被加载时jdbcTemplate还没被加载,就会出错。但是我使用了@DependsOn后有时也会出现该类先于jdbcTemplate前被加载的情况。
  4. JdbcOperations是一个接口,JdbcTemplate是它的一个实现类,Spring处处都使用接口。这里声明成JdbcTemplate也行
  5. SpittleRowMapper是将SQL查询的ResultSet转换成Spittle对象的一个映射类,后面会放出来,JdbcTemplate中使用这种映射类进行处理结果集,和DBUtils差不多。

看这个方法,jdbcOperations.queryForObject是查询并返回一个对象的,第一个参数是SQL,第二个参数就是mapper,第三个参数是可变长参数,是SQL中的参数

@Override
public Spittle findOne(long id) {
    return jdbcOperations.queryForObject(
            "SELECT * FROM spittle WHERE id=?",
            mapper,
            id
    );
}

比较坑的是,该方法有这么一个重载方法:

/**
* The query is expected to be a single row/single column query; the returned result will be directly mapped to the corresponding object type.
*/
queryForObject(String sql, Class<T> requireClass, Object...args);

这个single row/single column,我以为是单行或者单列就能够直接通过反射映射到对应的Pojo类上,结果是单行并且单列,这个方法可以用于Integer.classString.class等等值的获取,比如如下获取总spittle数量

queryForObject("SELECT count(*) FROM spittle", Integer.class);

RowMapper

上面我们定义了一个SpittleRowMapper将结果集转换成了Spittle对象。

public class SpittleRowMapper implements RowMapper<Spittle> {
    @Override
    public Spittle mapRow(ResultSet resultSet, int i) throws SQLException {
        Date date = new Date(resultSet.getLong("createTime"));
        Long id = resultSet.getLong("id");
        String message = resultSet.getString("message");
        Double latitude = resultSet.getDouble("latitude");
        Double longtitude = resultSet.getDouble("longtitude");
        Spittle spittle = new Spittle(id,message,date,latitude,longtitude);
        return spittle;
    }
}

下面我们来解释下,定义的RowMapper必须实现RowMapper接口并指定泛型,重写mapRow方法将结果集中的一行映射成对象。换行的操作也是属于模板代码,jdbcTemplate也帮我们做了。

下面是mapRow方法,由于数据库中的createTime字段和Spittle中的数据类型不一致,Spittle中是java.sql.Date,而数据库中存储的是unix时间戳,所以这里转换了一下,其它的没啥可说的。

BeanPropertyRowMapper

如果数据库和Pojo类的字段都能对上,能够直接通过反射映射过去,那么可以不用自己定义RowMapper,使用BeanPropertyRowMapper

下面的SpitterRepository使用BeanPropertyRowMapper

@Repository
@Primary
@DependsOn("jdbcTemplate")
public class JdbcSpitterRepository implements SpitterRepository{

    JdbcOperations jdbcOperations;
    BeanPropertyRowMapper<Spitter> mapper;

    @Autowired
    public JdbcSpitterRepository(JdbcOperations jdbcOperations) {
        this.jdbcOperations = jdbcOperations;
        this.mapper = new BeanPropertyRowMapper<Spitter>(Spitter.class);
    }

    @Override
    public int save(Spitter spitter) {
        if (spitter.getId()!=null) {
            return jdbcOperations.update(
                    "UPDATE spitter SET firstName=?, lastName=?, userName=?, password=? where id=?",
                    spitter.getFirstName(),spitter.getLastName(),spitter.getUserName(),spitter.getPassword(),spitter.getId()
            );
        }else{
            return jdbcOperations.update(
                    "INSERT INTO spitter (firstName, lastName, userName, password) VALUES (?,?,?,?)",
                    spitter.getFirstName(),spitter.getLastName(),spitter.getUserName(),spitter.getPassword()
            );
        }
    }

    @Override
    public Spitter getOne(Long id) {
        return jdbcOperations.queryForObject(
                "SELECT * FROM spitter WHERE id=?", mapper, id
        );
    }

    @Override
    public Spitter getOneByUserName(String username) {
        return jdbcOperations.queryForObject(
                "SELECT * FROM spitter WHERE userName=?",
                mapper,
                username
        );
    }
}
posted @ 2021-09-17 14:52  yudoge  阅读(125)  评论(0编辑  收藏  举报