欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SpringBoot学习-(六)SpringBoot与Mybatis整合

程序员文章站 2024-01-26 12:40:34
...

mybatis-spring-boot-starter依赖树如下:

SpringBoot学习-(六)SpringBoot与Mybatis整合

mybatis开发团队为Spring Boot 提供了 MyBatis-Spring-Boot-Starter
首先,MyBatis-Spring-Boot-Starter will:

  • Autodetect an existing DataSource.
  • Will create and register an instance of a SqlSessionFactoryBean passing that DataSource as an input.
  • Will create and register an instance of a SqlSessionTemplate got out of the SqlSessionFactoryBean.
  • Autoscan your mappers, link them to the SqlSessionTemplate and register them to Spring context so they can be injected into your beans.

来源: http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/index.html

百度翻译过来如下:

  • 自动检测现有的数据源。
  • 将创建并登记一sqlsessionfactorybean传递数据源作为输入的一个实例。
  • 将创建一个实例并登记sqlsessiontemplate了sqlsessionfactorybean。
  • 自动扫描你的映射,它们链接到sqlsessiontemplate和登记他们的Spring上下文可以注入你的bean。

就是说,使用了该Starter之后,只需要定义一个DataSource即可,它会自动创建使用该DataSource的SqlSessionFactoryBean以及SqlSessionTemplate。会自动扫描你的Mappers,连接到SqlSessionTemplate,并注册到Spring上下文中。

1.添加pom依赖

<!-- spring mvc支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- springboot整合mybatis(核心就这一个) -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.1</version>
</dependency>

<!-- 阿里巴巴druid数据库连接池 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.3</version>
</dependency>

<!-- mysql驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

2.配置datasource(Druid)

具体配置步骤点这里

3.配置application.yml

mybatis:
  # mybatis配置文件
  config-location: classpath:mybatis.xml
  # 映射文件所在路径
  mapper-locations:
  # 前面的 - 不要删除
  - classpath:mapper/*.xml

项目目录结构如下:

SpringBoot学习-(六)SpringBoot与Mybatis整合

4.代码(没有任何变化,可以跳过去)

mybatis配置

<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- 打印sql语句 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>
</configuration>

实体类

package com.ahut.entity;

import java.io.Serializable;
import java.util.Date;

/**
 * 
 * @ClassName: GoodsType
 * @Description: 商品类型实体类
 * @author cheng
 * @date 2017年7月12日 下午5:37:33
 */
public class GoodsType implements Serializable {
    private static final long serialVersionUID = -4039634130866820668L;

    private String typeId;// 类型id
    private String typeName;// 类型名称
    private Date createTime;// 创建时间
    private Date updateTime;// 更新时间

    /**
     * 重写tostring
     */
    @Override
    public String toString() {
        return "GoodsType [typeId=" + typeId + ", typeName=" + typeName + ", createTime=" + createTime + ", updateTime="
                + updateTime + "]";
    }

    /**
     * 无参构造函数
     */
    public GoodsType() {
        super();
    }

    /**
     * 有参构造函数
     * 
     * @param typeId
     * @param typeName
     * @param createTime
     * @param updateTime
     */
    public GoodsType(String typeId, String typeName, Date createTime, Date updateTime) {
        super();
        this.typeId = typeId;
        this.typeName = typeName;
        this.createTime = createTime;
        this.updateTime = updateTime;
    }

    public String getTypeId() {
        return typeId;
    }

    public void setTypeId(String typeId) {
        this.typeId = typeId;
    }

    public String getTypeName() {
        return typeName;
    }

    public void setTypeName(String typeName) {
        this.typeName = typeName;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public Date getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }

}

映射文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ahut.mapper.GoodsTypeMapper">
    <resultMap id="BaseResultMap" type="com.ahut.entity.GoodsType">
        <id column="TYPE_ID" jdbcType="VARCHAR" property="typeId" />
        <result column="TYPE_NAME" jdbcType="VARCHAR" property="typeName" />
        <result column="CREATE_TIME" jdbcType="TIMESTAMP" property="createTime" />
        <result column="UPDATE_TIME" jdbcType="TIMESTAMP" property="updateTime" />
    </resultMap>

    <!-- 添加一个商品类型 -->
    <insert id="saveGoodsType" parameterType="com.ahut.entity.GoodsType">
        insert into goods_type (TYPE_ID, TYPE_NAME, CREATE_TIME)
        values (replace(UUID(),'-',''), #{typeName,jdbcType=VARCHAR}, NOW())
    </insert>

    <!-- 删除一个商品类型 -->
    <delete id="deleteGoodsType" parameterType="java.lang.String">
        delete from goods_type
        where TYPE_ID = #{typeId,jdbcType=VARCHAR}
    </delete>

    <!-- 修改一个商品类型 -->
    <update id="updateGoodsType" parameterType="com.ahut.entity.GoodsType">
        update goods_type
        set TYPE_NAME = #{typeName,jdbcType=VARCHAR},
        UPDATE_TIME = NOW()
        where TYPE_ID = #{typeId,jdbcType=VARCHAR}
    </update>

    <!-- 获取所有的商品类型 -->
    <select id="getList" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from goods_type
        order by CREATE_TIME ASC
    </select>

    <!-- 依据商品名称查询 -->
    <select id="getByTypeName" parameterType="java.lang.String"
        resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from goods_type
        where TYPE_NAME = #{typeName,jdbcType=VARCHAR}
    </select>


    <!-- 自动生成 -->
    <sql id="Base_Column_List">
        TYPE_ID, TYPE_NAME, CREATE_TIME, UPDATE_TIME
    </sql>
    <select id="selectByPrimaryKey" parameterType="java.lang.String"
        resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from goods_type
        where TYPE_ID = #{typeId,jdbcType=VARCHAR}
    </select>
    <delete id="deleteByPrimaryKey" parameterType="java.lang.String">
        delete from goods_type
        where TYPE_ID = #{typeId,jdbcType=VARCHAR}
    </delete>
    <insert id="insert" parameterType="com.ahut.entity.GoodsType">
        insert into goods_type (TYPE_ID, TYPE_NAME, CREATE_TIME,
        UPDATE_TIME)
        values (#{typeId,jdbcType=VARCHAR}, #{typeName,jdbcType=VARCHAR},
        #{createTime,jdbcType=TIMESTAMP},
        #{updateTime,jdbcType=TIMESTAMP})
    </insert>
    <insert id="insertSelective" parameterType="com.ahut.entity.GoodsType">
        insert into goods_type
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="typeId != null">
                TYPE_ID,
            </if>
            <if test="typeName != null">
                TYPE_NAME,
            </if>
            <if test="createTime != null">
                CREATE_TIME,
            </if>
            <if test="updateTime != null">
                UPDATE_TIME,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="typeId != null">
                #{typeId,jdbcType=VARCHAR},
            </if>
            <if test="typeName != null">
                #{typeName,jdbcType=VARCHAR},
            </if>
            <if test="createTime != null">
                #{createTime,jdbcType=TIMESTAMP},
            </if>
            <if test="updateTime != null">
                #{updateTime,jdbcType=TIMESTAMP},
            </if>
        </trim>
    </insert>
    <update id="updateByPrimaryKeySelective" parameterType="com.ahut.entity.GoodsType">
        update goods_type
        <set>
            <if test="typeName != null">
                TYPE_NAME = #{typeName,jdbcType=VARCHAR},
            </if>
            <if test="createTime != null">
                CREATE_TIME = #{createTime,jdbcType=TIMESTAMP},
            </if>
            <if test="updateTime != null">
                UPDATE_TIME = #{updateTime,jdbcType=TIMESTAMP},
            </if>
        </set>
        where TYPE_ID = #{typeId,jdbcType=VARCHAR}
    </update>
    <update id="updateByPrimaryKey" parameterType="com.ahut.entity.GoodsType">
        update goods_type
        set TYPE_NAME = #{typeName,jdbcType=VARCHAR},
        CREATE_TIME = #{createTime,jdbcType=TIMESTAMP},
        UPDATE_TIME = #{updateTime,jdbcType=TIMESTAMP}
        where TYPE_ID = #{typeId,jdbcType=VARCHAR}
    </update>
</mapper>

接口层

package com.ahut.mapper;

import java.util.List;

import com.ahut.entity.GoodsType;

/**
 * 
 * @ClassName: GoodsTypeDao
 * @Description: 商品类型数据访问接口
 * @author cheng
 * @date 2017年7月17日 上午9:45:41
 */
public interface GoodsTypeMapper {

    /**
     * 
     * @Title: saveGoodsType
     * @Description: 添加一个商品类型
     * @param goodsType
     * @throws Exception
     */
    void saveGoodsType(GoodsType goodsType) throws Exception;

    /**
     * 
     * @Title: deleteGoodsType
     * @Description: 删除一个商品类型
     * @param typeId
     * @throws Exception
     */
    void deleteGoodsType(String typeId) throws Exception;

    /**
     * 
     * @Title: updateGoodsType
     * @Description: 修改一个商品类型
     * @param goodsType
     * @throws Exception
     */
    void updateGoodsType(GoodsType goodsType) throws Exception;

    /**
     * 
     * @Title: getList
     * @Description: 获取所有商品类型列表
     * @return
     * @throws Exception
     */
    List<GoodsType> getList() throws Exception;

    /**
     * 
     * @Title: getByTypeName
     * @Description: 依据商品名称查询
     * @param typeName
     * @return
     * @throws Exception
     */
    GoodsType getByTypeName(String typeName) throws Exception;
}

业务逻辑接口

package com.ahut.service;

import java.util.List;

import com.ahut.entity.GoodsType;

/**
 * 
 * @ClassName: GoodsTypeService
 * @Description: 商品类型业务逻辑接口
 * @author cheng
 * @date 2017年7月17日 上午10:03:28
 */
public interface GoodsTypeService {

    /**
     * 
     * @Title: saveGoodsType
     * @Description: 添加一个商品类型
     * @param goodsType
     * @throws Exception
     */
    String saveGoodsType(GoodsType goodsType) throws Exception;

    /**
     * 
     * @Title: deleteGoodsType
     * @Description: 删除一个商品类型
     * @param typeId
     * @throws Exception
     */
    void deleteGoodsType(String typeId) throws Exception;

    /**
     * 
     * @Title: updateGoodsType
     * @Description: 修改一个商品类型
     * @param goodsType
     * @throws Exception
     */
    String updateGoodsType(GoodsType goodsType) throws Exception;

    /**
     * 
     * @Title: getList
     * @Description: 从域对象中获取所有商品类型列表
     * @return
     * @throws Exception
     */
    List<GoodsType> getGoodsTypeList() throws Exception;

    /**
     * 
     * @Title: getList
     * @Description: 从数据库中获取所有商品类型列表
     * @return
     * @throws Exception
     */
    List<GoodsType> getList() throws Exception;

    /**
     * 
     * @Title: getByTypeName
     * @Description: 依据商品名称查询
     * @param typeName
     * @return
     * @throws Exception
     */
    GoodsType getByTypeName(String typeName) throws Exception;
}

业务逻辑实现

package com.ahut.serviceImpl;

import java.util.List;

import javax.servlet.ServletContext;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.ContextLoader;

import com.ahut.entity.GoodsType;
import com.ahut.mapper.GoodsTypeMapper;
import com.ahut.service.GoodsTypeService;

/**
 * 
 * @ClassName: GoodsTypeServiceImpl
 * @Description: 商品类型业务逻辑处理
 * @author cheng
 * @date 2017年7月17日 上午10:04:31
 */
@Service
@Transactional(rollbackFor = { RuntimeException.class, Exception.class })
public class GoodsTypeServiceImpl implements GoodsTypeService {
    // 数据访问
    @Autowired
    private GoodsTypeMapper typeDao;

    /**
     * 
     * @Title: saveGoodsType
     * @Description: 添加一个商品类型
     * @param goodsType
     * @throws Exception
     */
    @Override
    public String saveGoodsType(GoodsType goodsType) throws Exception {
        // 检查商品类型名称是否重复
        if (!checkTypeName(goodsType.getTypeName())) {
            // 添加
            typeDao.saveGoodsType(goodsType);
            // 更新域对象
            updateServletContext();
            return "添加成功";
        } else {
            return "商品类型名称已存在,添加失败";
        }
    }

    /**
     * 
     * @Title: deleteGoodsType
     * @Description: 删除一个商品类型
     * @param typeId
     * @throws Exception
     */
    @Override
    public void deleteGoodsType(String typeId) throws Exception {

        // 删除
        typeDao.deleteGoodsType(typeId);
        // 更新域对象
        updateServletContext();

    }

    /**
     * 
     * @Title: updateGoodsType
     * @Description: 修改一个商品类型
     * @param goodsType
     * @throws Exception
     */
    @Override
    public String updateGoodsType(GoodsType goodsType) throws Exception {

        // 检查商品类型名称是否重复
        if (!checkTypeName(goodsType.getTypeName())) {
            // 修改
            typeDao.updateGoodsType(goodsType);
            // 更新域对象
            updateServletContext();
            return "更新成功";
        } else {
            return "商品类型名称已存在,更新失败";
        }

    }

    /**
     * 
     * @Title: getGoodsTypeList
     * @Description: 从域对象中获取所有商品类型列表
     * @return
     * @throws Exception
     */
    @Override
    public List<GoodsType> getGoodsTypeList() throws Exception {
        return typeDao.getList();

    }

    /**
     * 
     * @Title: getList
     * @Description: 从数据库中获取所有商品类型列表
     * @return
     * @throws Exception
     */
    public List<GoodsType> getList() throws Exception {
        List<GoodsType> typeList = null;

        // 获取
        typeList = typeDao.getList();

        return typeList;
    }

    /**
     * 
     * @Title: checkTypeName
     * @Description: 检查商品类型名称是否重复
     * @param typeName
     */
    private boolean checkTypeName(String typeName) throws Exception {

        // 依据商品名称查询
        GoodsType goodsType = typeDao.getByTypeName(typeName);
        return goodsType == null ? false : true;

    }

    /**
     * 
     * @Title: updateServletContext
     * @Description: 更新域对象里的内容
     * @throws Exception
     */
    private void updateServletContext() throws Exception {
        // 通过Spring获取ServletContext域对象
        ServletContext servletContext = ContextLoader.getCurrentWebApplicationContext().getServletContext();

        List<GoodsType> typeList = getList();
        servletContext.setAttribute("GOODS_TYPE_LIST", typeList);

    }

    /**
     * 
     * @Title: getByTypeName
     * @Description: 依据商品名称查询
     * @param typeName
     * @return
     * @throws Exception
     */
    public GoodsType getByTypeName(String typeName) throws Exception {
        return typeDao.getByTypeName(typeName);
    }

}

控制层

package com.ahut.action;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.ahut.entity.GoodsType;
import com.ahut.service.GoodsTypeService;

/**
 * 
 * @ClassName: GoodsTypeAction
 * @Description: 商品类型控制层
 * @author chengrui
 * @date 2017年7月17日 上午11:09:47
 */
@RestController // 等价于@aaa@qq.com
public class GoodsTypeAction {
    // 业务逻辑
    @Autowired
    private GoodsTypeService typeService;

    /**
     * 
     * @Title: getGoodsTypeList
     * @Description: 获取商品类型列表
     * @return
     * @throws Exception
     */
    @RequestMapping(value = "/getGoodsTypeList")
    public List<GoodsType> getGoodsTypeList() throws Exception {
        // 调用业务逻辑,返回数据
        return typeService.getList();
    }

    @RequestMapping(value="/getByTypeName")
    public GoodsType getByTypeName(String typeName) throws Exception{
        typeName = "生活用品";
        return typeService.getByTypeName(typeName);
    }

}

5.springboot入口

注意:使用了@MapperScan(“com.ahut.mapper”)注解,Mapper接口上就不需要加上@Mapper注解了,两者只用选择其一

package com.ahut;

import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;

/**
 * 
 * @ClassName: AhutApplication
 * @Description: springboot入口
 * @author cheng
 * @date 2017年9月24日 下午1:46:51
 */
@SpringBootApplication
@PropertySource(value = { "classpath:mysql.properties" })
@MapperScan("com.ahut.mapper")//扫描数据访问接口
public class AhutApplication extends WebMvcConfigurationSupport {

    @Value("${spring.datasource.url}")
    private String dbUrl;

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    @Value("${spring.datasource.driverClassName}")
    private String driverClassName;

    @Value("${spring.datasource.initialSize}")
    private int initialSize;

    @Value("${spring.datasource.minIdle}")
    private int minIdle;

    @Value("${spring.datasource.maxActive}")
    private int maxActive;

    @Value("${spring.datasource.maxWait}")
    private int maxWait;

    @Value("${spring.datasource.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;

    @Value("${spring.datasource.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;

    @Value("${spring.datasource.validationQuery}")
    private String validationQuery;

    @Value("${spring.datasource.testWhileIdle}")
    private boolean testWhileIdle;

    @Value("${spring.datasource.testOnBorrow}")
    private boolean testOnBorrow;

    @Value("${spring.datasource.testOnReturn}")
    private boolean testOnReturn;

    @Value("${spring.datasource.poolPreparedStatements}")
    private boolean poolPreparedStatements;

    @Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize}")
    private int maxPoolPreparedStatementPerConnectionSize;

    @Value("${spring.datasource.filters}")
    private String filters;

    @Value("{spring.datasource.connectionProperties}")
    private String connectionProperties;

    @Bean // 声明其为Bean实例
    @Primary // 在同样的DataSource中,首先使用被标注的DataSource
    public DataSource dataSource() {
        DruidDataSource datasource = new DruidDataSource();

        datasource.setUrl(this.dbUrl);
        datasource.setUsername(username);
        datasource.setPassword(password);
        datasource.setDriverClassName(driverClassName);

        // configuration
        datasource.setInitialSize(initialSize);
        datasource.setMinIdle(minIdle);
        datasource.setMaxActive(maxActive);
        datasource.setMaxWait(maxWait);
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setValidationQuery(validationQuery);
        datasource.setTestWhileIdle(testWhileIdle);
        datasource.setTestOnBorrow(testOnBorrow);
        datasource.setTestOnReturn(testOnReturn);
        datasource.setPoolPreparedStatements(poolPreparedStatements);
        datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
        try {
            datasource.setFilters(filters);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        datasource.setConnectionProperties(connectionProperties);
        return datasource;
    }

    @Bean
    public ServletRegistrationBean druidServlet() {
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
        servletRegistrationBean.setServlet(new StatViewServlet());
        servletRegistrationBean.addUrlMappings("/druid/*");
        Map<String, String> initParameters = new HashMap<String, String>();
        initParameters.put("loginUsername", "admin");// 用户名
        initParameters.put("loginPassword", "admin");// 密码
        initParameters.put("resetEnable", "false");// 禁用HTML页面上的“Reset All”功能
        initParameters.put("allow", ""); // IP白名单 (没有配置或者为空,则允许所有访问)
        // initParameters.put("deny", "192.168.20.38");// IP黑名单
        // (存在共同时,deny优先于allow)
        servletRegistrationBean.setInitParameters(initParameters);
        return servletRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new WebStatFilter());
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        return filterRegistrationBean;
    }

    /**
     * 1、 extends WebMvcConfigurationSupport 2、重写下面方法; setUseSuffixPatternMatch:
     * 
     * 设置是否是后缀模式匹配,如“/user”是否匹配/user.*,默认真即匹配; setUseTrailingSlashMatch:
     * 设置是否自动后缀路径模式匹配,如“/user”是否匹配“/user/”,默认真即匹配;
     */
    @Override
    protected void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setUseSuffixPatternMatch(false).setUseTrailingSlashMatch(true);
    }

    public static void main(String[] args) {
        SpringApplication.run(AhutApplication.class);
    }

}

6.事务管理

Spring Boot中推荐使用@Transactional注解来申明事务。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

mybatis-spring-boot-starter会自动引入spring-boot-starter-jdbc依赖,所以不需要在额外添加

当引入jdbc依赖之后,Spring Boot会自动默认分别注入DataSourceTransactionManager或JpaTransactionManager,所以我们不需要任何额外配置就可以用@Transactional注解进行事务的使用。

@Transactional不仅可以注解在方法,也可以注解在类上。当注解在类上时,意味着此类所有public方法都会开启事务。如果类级别和方法级别同时使用了@Transactional注解,则使用在类级别的注解会重载方法级别的注解。