springboot和mybatis結合

xiaoqb發表於2016-02-19

springboot和mybatis結合

依賴和資料來源配置

springboot依賴了spring4,需要依賴mybatis-spring,最新版本是1.2.2。
資料來源相關的依賴:

<!-- datasource -->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP-java6</artifactId>
    <version>${HikariCP.version}</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql-connector-java.version}</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>${mybatis.version}</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>${mybatis-spring.version}</version>
</dependency>

前兩個是資料來源的依賴,包括HikariCP和mysql驅動。後面兩個是mybatis依賴,包括mybatis本身和mybatis-spring模組。

有了這些依賴之後,就可以通過spring4的配置類,對mybatis資料來源等進行配置。

@Configuration
@PropertySource("classpath:datasource.properties")
@MapperScan(basePackages="xxx.repository", sqlSessionFactoryRef = "sqlSessionFactory")
public class DatasourceConfig {
    @Autowired
    private Environment env;

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setDriverClassName("com.mysql.jdbc.Driver");
        config.setAutoCommit(false);
        config.setJdbcUrl(env.getProperty("xxx.db.url"));
        config.setUsername(env.getProperty("xxx.db.username"));
        config.setPassword(env.getProperty("xxx.db.password"));

        return new HikariDataSource(config);
    }

    @Bean
    public DataSourceTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setTypeAliasesPackage("xxx.mybatis");
        return sessionFactory.getObject();
    }

}

首先引入配置檔案,並且注入到env物件中。env類似System的properties物件,封裝了配置檔案中的key value。
然後通過MapperScan註解定義mapper介面包路徑。這裡同時定義了sqlSessionFactoryRef,是因為需要用到多資料來源,
防止spring無法注入,後面會提到。

之後程式碼就可以開始定義輸出的bean。一個是datasource,直接初始化一個Hikari的資料來源,springboot提供了builder類,
但是檢視原始碼和api之後,DataSourceBuilder無法配置autocommit屬性。

再下面是事務管理,需要通過建構函式注入dataSource。最後一個是mybatis的sqlSessionFactory,主要也是注入一個資料來源。

mapper(DAO)實現

dao實現和原先的ibatis差不多,但是mybatis可以通過註解的形式直接生成動態sql。既然springboot用了程式碼來取代xml,mybatis
中也同樣去掉了xml。

插入

插入操作需要注意兩個地方,一個是如何返回插入之後的主鍵(mysql),一個是如何使用資料型別的handler。
首先看程式碼:

@Insert("INSERT INTO aegis_cron_timer " +
            "(id, gmt_create, gmt_modified, name, expression, event_class_name, description, last_trigger_time, status, parameter) " +
            "VALUES (NULL, now(), now(), #{name:VARCHAR}, #{expression:VARCHAR}, " +
            "#{eventClassName:VARCHAR}, #{description:VARCHAR}, now(), #{status:VARCHAR}, " +
            "#{parameter,typeHandler=com.alibaba.aegis.seawater.cron.service.dao.mybatis.MapToJsonTypeHandler})")
@SelectKey(before = false, statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", resultType = java.lang.Long.class)
public Long insertCronTimer(CronTimer cronTimer);

針對mysql,可以通過SelectKey這個註解,設定插入後主鍵的返回。由於mysql是自增主鍵,所以設定為插入後執行,定義返回的型別為long
(資料庫中定義了bigint)。

另外,這裡有個欄位需要從map序列化成json字串,作為varchar型別存放到資料庫中。在插入的sql中,可以直接在變數後面定義typeHandler,
值是對應handler的完整類名。

更新

更新操作比較簡單,直接使用Update註解即可。和插入類似,如果需要指定type handler,直接在欄位後面增加引數即可。更新函式可以返回一個int值,
表示本次更新的行數。

查詢

查詢通過Select註解完成,mybatis可以直接通過欄位名字和查詢結果的java bean之間做自動關聯。如果名字不匹配,有兩種方式,一種是通過sql中
增加AS關鍵字轉成java bean中的欄位名,一種是通過@Result註解指定二者的對映關係。

@Select("SELECT name, expression, event_class_name AS eventClassName, description, status, parameter " +
            "FROM aegis_cron_timer " +
            "WHERE status = `ENABLE`")
@Results({
        @Result(column = "parameter", jdbcType = JdbcType.VARCHAR, property = "parameter", typeHandler = MapToJsonTypeHandler.class)
})
public List<CronTimer> listAllAvailableCronTimer();

這裡通過Result註解配置了type handler,特別注意Result註解必須在Results註解中,不然不會生效。

自定義type handler

前文已經提到了如何在插入、更新、查詢語句中使用type handler,type handler實現也比較簡單。mybatis自帶的type handler都是通過extends
BaseTypeHandler來實現的,但例子中直接實現了TypeHandler介面:

@MappedTypes(Map.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class MapToJsonTypeHandler implements TypeHandler<Map<String, Object>> {

    @Override
    public void setParameter(PreparedStatement ps, int i, Map<String, Object> parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, JSON.toJSONString(parameter));
    }

    @Override
    public Map<String, Object> getResult(ResultSet rs, String columnName) throws SQLException {
        String value = rs.getString(columnName);
        return jsonToMap(value);
    }

    @Override
    public Map<String, Object> getResult(ResultSet rs, int columnIndex) throws SQLException {
        String value = rs.getString(columnIndex);
        return jsonToMap(value);
    }

    @Override
    public Map<String, Object> getResult(CallableStatement cs, int columnIndex) throws SQLException {
        String value = cs.getString(columnIndex);
        return jsonToMap(value);
    }

    private Map<String,Object> jsonToMap(String value) {
        if (StringUtils.isBlank(value)) {
            return Collections.emptyMap();
        } else {
            return JSON.parseObject(value, new TypeReference<Map<String, Object>>() {
            });
        }
    }
}

實現比較簡單,序列化的時候直接通過fastjson將map物件轉成json string,放到PreparedStatement中。反序列化的時候返回來轉成Map即可。

多資料來源實現

由於專案需要從老的資料庫遷移到新的資料庫,所以需要兩個資料來源,在設定多資料來源的時候也踩了很多坑。

另一個資料來源配置類:

@Configuration
@PropertySource("classpath:amon-datasource.properties")
@MapperScan(basePackages="com.alibaba.aegis.seawater.cron.migrate.repository",
        sqlSessionFactoryRef = "amonSqlSessionFactory", sqlSessionTemplateRef = "amonSqlSessionTemplate")
public class AmonDataSourceConfig {
    @Autowired
    private Environment env;

    @Bean(name = "amonDataSource")
    public DataSource amonDataSource() {
        HikariConfig config = new HikariConfig();
        config.setDriverClassName("com.mysql.jdbc.Driver");
        config.setAutoCommit(true);
        config.setJdbcUrl(env.getProperty("amon.db.url"));
        config.setUsername(env.getProperty("amon.db.username"));
        config.setPassword(env.getProperty("amon.db.password"));

        return new HikariDataSource(config);
    }

    @Bean(name = "amonTransactionManager")
    public DataSourceTransactionManager amonTransactionManager(@Qualifier("amonDataSource")DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "amonSqlSessionFactory")
    public SqlSessionFactory amonSqlSessionFactory(@Qualifier("amonDataSource")DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        return sessionFactory.getObject();
    }

    @Bean(name = "amonSqlSessionTemplate")
    public SqlSessionTemplate amonSqlSessionTemplate(@Qualifier("amonSqlSessionFactory")SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

這裡也定義了一個配置檔案,需要注意的是不要和前面一個的key相同,不然會覆蓋的。定義bean的時候需要設定下name,或者函式名字改了也行。
需要定義的bean和之前的一樣,特別注意MapperScan註解需要修改sqlSessionFactoryRef或者sqlSessionTemplateRef。這裡兩個都改了但是啟動的時候會
提示:

Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.

這邊定義了bean之後,直接使用就沒有問題了。唯一需要特別注意的是@Transactional註解,由於定義了兩個transactionManager,
無法通過型別來注入事務管理器了,需要註解中特別指定。比如使用前面定義的資料來源的事物管理器,需要改成:

@Transactional("transactionManager")

這樣spring可以通過名字注入bean。

DAO測試

為了方便測試,對應測試類中,重新覆蓋了dataSource,採用h2這種記憶體資料庫,解決單元測試資料干擾。

@Configuration
@MapperScan(basePackages="com.alibaba.aegis.seawater.cron.repository")
public class TestDatasourceConfig extends DatasourceConfig {
    @Autowired
    private Environment env;

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .setName("cron")
                .addScript("h2.sql")
                .build();
    }

}

這裡直接通過springboot提供的EmbeddedDatabaseBuilder來建立一個h2的資料庫,並新增初始化資料庫schema的sql檔案。
這裡需要注意的是,如果這個sql檔案直接叫schema.sql,之前mysql資料來源在執行的時候也會去執行,因此這裡沒有使用預設的名字。

其他坑

在springboot注入properties檔案中配置的時候,還遇到一個噁心的問題,除了PropertySource註解指定的properties檔案之外,
spring還會預設帶上jvm變數、系統環境變數。剛開始直接把資料庫使用者名稱欄位的key寫成了username,結果由於測試伺服器上使用了sudo
命令,sudo在切換使用者的同時設定了USERNAME這個環境變數標識原始執行使用者,導致springboot一直在注入這個值,除錯了很久。

該文章來自於阿里巴巴技術協會(ATA

作者:金靈傑


相關文章