1. 程式人生 > 其它 >Spring實戰 十 連線資料庫

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
        );
    }
}