MyBatis 从入门到明白.md

MyBatis从入门到熟习

[TOC]

以下代码获取地址:
https://github.com/Jenyow/codelib/tree/master/codelib-parent/codelib-springboot-samples/codelib-springboot-sample-mybatis
对此代码中的不足,或者其他实现需要补给的,可以指出,我们一并探索琢磨

MyBatis Generator

MyBatis Generator 是一个可以生成 MyBatis
代码的工具。(当然绝不也是足以的,只要你就算麻烦( ╯▽╰))
以下是遵照 Maven 的 MyBatis Generator 配置:
POM.xml:

<build>
        <plugins>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.5</version>
                <configuration>
                    <!--执行过程会输出到控制台 -->
                    <verbose>true</verbose>
                    <!--不允许覆盖生成的文件 -->
                    <!-- 如果设置为true,如果生成的java文件存在已经同名的文件,新生成的文件会覆盖原有的文件。 这个过程大概可以理解为:删除原来的,重新生成新的.对于自己写的、修改的内容没有了 
                        如果设置为false,如果存在同名的文件,MBG会给新生成的代码文件生成一个唯一的名字 例如: MyClass.java.1, MyClass.java.2 
                        等等 -->
                    <overwrite>false</overwrite>
                    <!-- 如果指定了该参数,逗号隔开的这个表会被运行, 这些表名必须和 <table> 配置中的表面完全一致。 只有指定的这些表会被执行。 
                        如果没有指定该参数,所有的表都会被执行。 -->
                    <!-- 对于修改、新增的表,指定该属性进行构建比较合理 -->
                    <!-- <tableNames></tableNames> -->
                </configuration>
                <dependencies>
                    <!-- 配置这个依赖主要是为了等下在配置MG的时候可以不用配置classPathEntry这样的一个属性,避免代码的耦合度太高 -->
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>5.1.42</version><!-- 必须指定版本号 -->
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

无法不参加数据库驱动,我用的是 mysql ,所以参加如下依赖。

<!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

generatorConfig.properties:

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/db_dbtest
username=root
password=******

generatorConfig.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
  PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
  "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>

    <properties resource="generatorConfig.properties"/>

    <!-- 
        元素类型为 "context" 的内容必须匹配 (注意顺序)
        "(property*,plugin*,commentGenerator?,(connectionFactory|jdbcConnection),
        javaTypeResolver?,javaModelGenerator,sqlMapGenerator?,javaClientGenerator?,table+)"。 
    -->
    <context id="DB2Tables" targetRuntime="MyBatis3">
        <!-- 这个插件给由MBG生成的Java模型对象增加了equals和hashCode方法 -->
        <plugin type="org.mybatis.generator.plugins.EqualsHashCodePlugin" />
        <!-- 这个插件给由MBG生成的Javas添加了java.io.Serializable标记接口。这个插件给实体类增加了serialVersionUID字段。 -->
        <plugin type="org.mybatis.generator.plugins.SerializablePlugin" />
        <!-- 该插件给Example类添加方法(实际上是给Criteria内部类)来支持不区分大小写的LIKE搜索 -->
        <plugin type="org.mybatis.generator.plugins.CaseInsensitiveLikePlugin" />
        <!-- 该插件给实体类添加toString()方法。 -->
        <plugin type="org.mybatis.generator.plugins.ToStringPlugin" /> 

        <commentGenerator>
            <!-- 设置为flase,生成注释包含时间时间戳 -->
            <property name="suppressDate" value="true" />
            <!-- 用来指定MBG生成的代码中是否包含任何注释
                设置为false,运行多次插件,不会生成重复的内容
                在pom.xml插件配置中,配置不重写文件,所以不需要插件生成的注释 -->
            <property name="suppressAllComments" value="flase" />
        </commentGenerator>

        <!-- 元素定义如何连接目标数据库 -->
        <jdbcConnection driverClass="${driver}"
                        connectionURL="${url}" userId="${username}" password="${password}">
        </jdbcConnection>
        <!-- 
            targetProject:被假定为一个已存在的目录结构。 如果目录结构不存在MBG将会失败.
            targetPackage:将会转换为 targetProject 适当的子目录结构。 如果有必要,MBG会创建这些子目录。 
            enableSubPackages:Java模型生成器应该使用子包。 
                                   这意味着在这种情况下生成的模型对象将被放置在名为 test.model.db2admin 的包中(因为表在 DB2ADMIN schema中)。 
                                   如果 enableSubPackages 属性设置为 false, 那么包名将会是 test.model。 
                              Java模型生成器也应该对字符串进行trim操作。
                                    这意味着任何字符串属性的setter方法将调用trim方法 - 如果您的数据库可能会在字符末尾返回空白符,这是非常有用的。
        -->
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>

        <!-- 元素来指定生成 Java 模型对象所属的包 -->
        <javaModelGenerator targetPackage="com.codelib.springboot.sample.mybatis.pojo"
            targetProject="src/main/java">
            <property name="constructorBased" value="true" />
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>
        <!-- 元素来指定生成 SQL 映射文件所属的包和的目标项目 -->
        <!-- 如果目标是MyBatis3,那么只有当您选择javaClientGenerator需要XML时,他才是 <context> 元素的一个必须的子元素。 -->
        <sqlMapGenerator targetPackage="mapper"
            targetProject="src/main/resources">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>
        <!-- 元素来指定目标包和目标项目生成的客户端接口和类 -->
        <javaClientGenerator type="XMLMAPPER"
            targetPackage="com.codelib.springboot.sample.mybatis.mapper" targetProject="src/main/java">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>
        <!-- 配置表映射 -->
        <table tableName="courses" domainObjectName="Course">
            <property name="constructorBased" value="true" />
            <property name="ignoreQualifiersAtRuntime" value="true" />
        </table>
        <table tableName="students" domainObjectName="Student">
            <property name="constructorBased" value="true" />
            <property name="ignoreQualifiersAtRuntime" value="true" />
        </table>
        <table tableName="textbooks" domainObjectName="Textbook">
            <property name="constructorBased" value="true" />
            <property name="ignoreQualifiersAtRuntime" value="true" />
        </table>
        <table tableName="grades" domainObjectName="Grade">
            <property name="constructorBased" value="true" />
            <property name="ignoreQualifiersAtRuntime" value="true" />
        </table>
        <table tableName="student_courses" domainObjectName="StudentCourse">
            <property name="constructorBased" value="true" />
            <property name="ignoreQualifiersAtRuntime" value="true" />
        </table>        
    </context>
</generatorConfiguration>

我安排的是生成 xml 的映照。
主导配备可以复用,只需改变对应的包名和表信息即可。
布置好未来,通过 maven 执行 mybatis-generator:generate
命令,即可生成对应的代码。

对此地点的部署,假使推行五回 mybatis-generator:generate 命令,java
的代码会转移备份,xml的不会被遮住,在xml 中新增的一些会被移到 xml
的尾巴。然而值得注意的是,假设改动了代码生成的有些,再执行命令,修改的有的将会被覆盖掉。
影响这一行为的配备是 <commentGenerator>
。指出不用在诠释里转变时间戳消息。
对于新增的表,可以在 pom.xml 中
<tableNames></tableNames> 指定本次实施
mybatis-generator:generate 命令生成表的代码,六个表名用逗号隔开。

MyBatis Generator
给每个表生成一个实体类、Example条件类、Mapper接口、xml映射。

MyBatis

MyBatis 配置:

import java.util.Properties;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.alibaba.druid.pool.DruidDataSource;
import com.github.pagehelper.PageInterceptor;
@Configuration
@EnableTransactionManagement
@MapperScan(value = "com.codelib.springboot.sample.mybatis.mapper")
public class MyBatisConfig {
    @Autowired
    private DruidDataSource dataSource;
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        // mybatis分页
        PageInterceptor pageHelper = new PageInterceptor();
        Properties props = new Properties();
        props.setProperty("reasonable", "true");
        props.setProperty("supportMethodsArguments", "true");
        props.setProperty("returnPageInfo", "check");
        props.setProperty("params", "count=countSql");
        pageHelper.setProperties(props); // 添加插件
        Interceptor[] plugins = new Interceptor[] { pageHelper };
        sqlSessionFactoryBean.setPlugins(plugins);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean
                .setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
        // 在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。
        sqlSessionFactoryBean.setTypeAliasesPackage("com.codelib.springboot.sample.mybatis.pojo");
        return sqlSessionFactoryBean.getObject();
    }
}

java 配置和 xml 的部署是应和的。
MyBatis xml 映射配置文件详解,可以通过网址(
http://www.mybatis.org/mybatis-3/zh/configuration.html)精晓学习
查看 SqlSessionFactoryBean 源码,不难窥见跟 xml 对应的 set 方法。

别名包的布置,感觉更是有用,可以在 mapper xml
配置中简单对象的包名,可以使 xml 更加清晰简洁。
对此不讲明别名的意况下,默认是类名(首字母小写)

测试

自我的是部署内存数据库 H2 进行测试。
在 src/text/resources 下创建五个公文:application.yml、init_table.sql
application.yml:

spring.datasource:
  type: com.alibaba.druid.pool.DruidDataSource
  name: mybatistest
  driverClassName: org.h2.Driver  
  url: jdbc:h2:mem:db_users;MODE=MYSQL;INIT=RUNSCRIPT FROM './src/test/resources/init_table.sql'  
  username: 
  password:

init_table.sql:

-- ----------------------------
-- Table structure for grades
-- ----------------------------
DROP TABLE IF EXISTS `grades`;
CREATE TABLE `grades` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '年级ID',
  `name` varchar(255) NOT NULL COMMENT '年级名',
  PRIMARY KEY (`id`)
);
-- ----------------------------
-- Records of grades
-- ----------------------------
INSERT INTO `grades` VALUES ('1', '大一');
INSERT INTO `grades` VALUES ('2', '大二');
INSERT INTO `grades` VALUES ('3', '大三');
-- ----------------------------
-- Table structure for students
-- ----------------------------
DROP TABLE IF EXISTS `students`;
CREATE TABLE `students` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '学生ID',
  `name` varchar(255) NOT NULL COMMENT '学生名',
  PRIMARY KEY (`id`)
);
-- ----------------------------
-- Records of students
-- ----------------------------
INSERT INTO `students` VALUES ('1', '赵一');
INSERT INTO `students` VALUES ('2', '钱二');
INSERT INTO `students` VALUES ('3', '孙三');
INSERT INTO `students` VALUES ('4', '李四');
INSERT INTO `students` VALUES ('5', '王五');
-- ----------------------------
-- Table structure for textbooks
-- ----------------------------
DROP TABLE IF EXISTS `textbooks`;
CREATE TABLE `textbooks` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '教科书ID',
  `name` varchar(255) NOT NULL COMMENT '教科书名',
  PRIMARY KEY (`id`)
);
-- ----------------------------
-- Records of textbooks
-- ----------------------------
INSERT INTO `textbooks` VALUES ('1', '《高等数学》');
INSERT INTO `textbooks` VALUES ('2', '《Java编程基础》');
INSERT INTO `textbooks` VALUES ('3', '《设计模式》');
INSERT INTO `textbooks` VALUES ('4', '《大学英语I》');
-- ----------------------------
-- Table structure for courses
-- ----------------------------
DROP TABLE IF EXISTS `courses`;
CREATE TABLE `courses` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '课程ID',
  `name` varchar(255) NOT NULL COMMENT '课程名',
  `textbook_id` int(11) NOT NULL COMMENT '教科书ID',
  `grade_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `textbook_id` (`textbook_id`),
  KEY `grade_id` (`grade_id`),
  CONSTRAINT `courses_ibfk_3` FOREIGN KEY (`textbook_id`) REFERENCES `textbooks` (`id`),
  CONSTRAINT `courses_ibfk_4` FOREIGN KEY (`grade_id`) REFERENCES `grades` (`id`)
);
-- ----------------------------
-- Records of courses
-- ----------------------------
INSERT INTO `courses` VALUES ('1', '高等数学', '1', '1');
INSERT INTO `courses` VALUES ('2', '大学英语I', '4', '1');
INSERT INTO `courses` VALUES ('3', 'JAVA入门', '2', '2');
INSERT INTO `courses` VALUES ('4', '设计模式', '3', '3');
-- ----------------------------
-- Table structure for student_courses
-- ----------------------------
DROP TABLE IF EXISTS `student_courses`;
CREATE TABLE `student_courses` (
  `course_id` int(11) NOT NULL COMMENT '课程ID',
  `student_id` int(11) NOT NULL COMMENT '学生ID',
  PRIMARY KEY (`course_id`,`student_id`),
  KEY `student_id` (`student_id`),
  CONSTRAINT `student_courses_ibfk_1` FOREIGN KEY (`student_id`) REFERENCES `students` (`id`),
  CONSTRAINT `student_courses_ibfk_2` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`)
);
-- ----------------------------
-- Records of student_courses
-- ----------------------------
INSERT INTO `student_courses` VALUES ('1', '1');
INSERT INTO `student_courses` VALUES ('2', '1');
INSERT INTO `student_courses` VALUES ('3', '1');
INSERT INTO `student_courses` VALUES ('1', '2');
INSERT INTO `student_courses` VALUES ('2', '2');
INSERT INTO `student_courses` VALUES ('3', '2');
INSERT INTO `student_courses` VALUES ('1', '3');
INSERT INTO `student_courses` VALUES ('2', '3');
INSERT INTO `student_courses` VALUES ('3', '3');

测试类例子,DruidDataSourceConfig 是自我项目中 druid
数据库连接池的配置类:

import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import com.codelib.springboot.sample.mybatis.config.DruidDataSourceConfig;
import com.codelib.springboot.sample.mybatis.config.MyBatisConfig;
import com.codelib.springboot.sample.mybatis.pojo.Textbook;

@RunWith(SpringRunner.class)
@SpringBootTest(classes={MyBatisConfig.class, DruidDataSourceConfig.class})
// 需要加事务,防止各用例间相互影响
@Transactional
public class TextbookMapperTest {

@Autowired
private TextbookMapper textbookMapper;

@Before
public void setUp() throws Exception {
}

@Test
public void testSelectByPrimaryKey() {
Textbook textbook = textbookMapper.selectByPrimaryKey(1);
String expected = "《高等数学》";
String actual = textbook.getName();
assertEquals(expected, actual);
}

}

对于关系型数据库,除了单表操作外,表间关系一般可以归咎为二种状态:一对一、一对多和多对多。

一对一

课程 Course 和教材 Textbook 是一对一的涉及
插入:

/**
     * 一对一插入
     * 当依赖的Textbook不存在时,需要先将数据插入Textbook,然后再插入Course
     * @param course
     * @return
     */
    @Override
    public int intsertCourseTextbook(Course course) {
        int result = 0;
        try {
            Textbook textbook = course.getTextbook();
            textbookMapper.insert(textbook);
            // 关键在于 textbook 插入后获取 id
            course.setTextbookId(textbook.getId());
            courseMapper.insert(course);
            result = 1;
        } catch (Exception e) {
            logger.error("插入失败:{}", e.toString());
        }
        return result;
    }

xml 配置中至关首假设 <selectKey> 的配备,插入后回到 id :

<insert id="insert" parameterType="com.codelib.springboot.sample.mybatis.pojo.Textbook">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
    -->
    <selectKey keyProperty="id" resultType="int" order="AFTER">
            SELECT LAST_INSERT_ID() AS ID 
    </selectKey>
    insert into textbooks (id, name)
    values (#{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR})
  </insert>

查询:
重点是 <association> 标签的布局

<resultMap id="CourseTextbookResultMap" type="course">
    <id column="id" javaType="java.lang.Integer" jdbcType="INTEGER" property="id" />
    <result column="name" javaType="java.lang.String" jdbcType="VARCHAR" property="name" />
    <result column="grade_id" javaType="java.lang.Integer" jdbcType="INTEGER" property="gradeId" />
    <result column="textbook_id" javaType="java.lang.Integer" jdbcType="INTEGER" property="textbookId" />
    <association column="textbook_id" javaType="textbook" property="textbook" resultMap="TextbookResult" />
  </resultMap>
<resultMap id="TextbookResult" type="textbook">
    <id column="textbook_id" javaType="java.lang.Integer" jdbcType="INTEGER" property="id" />
    <result column="textbook_name" javaType="java.lang.String" jdbcType="VARCHAR" property="name" />
  </resultMap>
<select id="selectCourseTextbookResultMapByPrimaryKey" parameterType="java.lang.Integer" resultMap="CourseTextbookResultMap">
    select a.id, a.name, a.grade_id, a.textbook_id, b.id textbook_id, b.name textbook_name
    from courses a
    left join textbooks b on b.id=a.textbook_id
    where a.id=#{id,jdbcType=INTEGER}
  </select>

@Test
    public void testSelectCourseTextbookResultMapByPrimaryKey() {
        Course course = coursemapper.selectCourseTextbookResultMapByPrimaryKey(1);
        String expected = "《高等数学》";
        String actual = course.getTextbook().getName();
        assertEquals(expected, actual);
    }

对此涉嫌查询,假如存在同名的字段,需要给字段取别名,以分别开来

一对多

关键在于 <collection> 标签的部署,多的一则是用 List 。
Course.java:

...
// 一对一
private Textbook textbook;
// 一对多
private List<Student> students = new ArrayList<>();
...

<resultMap id="CourseStudentsResultMap" type="course">
    <id column="id" javaType="java.lang.Integer" jdbcType="INTEGER" property="id" />
    <result column="name" javaType="java.lang.String" jdbcType="VARCHAR" property="name" />
    <result column="grade_id" javaType="java.lang.Integer" jdbcType="INTEGER" property="gradeId" />
    <result column="textbook_id" javaType="java.lang.Integer" jdbcType="INTEGER" property="textbookId" />
    <collection ofType="student" property="students" resultMap="StudentResult" />
  </resultMap>
<resultMap id="StudentResult" type="student">
    <id column="student_id" javaType="java.lang.Integer" jdbcType="INTEGER" property="id" />
    <result column="student_name" javaType="java.lang.String" jdbcType="VARCHAR" property="name" />
  </resultMap>
<select id="selectCourseStudentsResultMapByPrimaryKey" parameterType="int" resultMap="CourseStudentsResultMap">
    select 
    a.id, a.name, a.grade_id, a.textbook_id, c.id student_id, c.name student_name
    from courses a
    left join student_courses b on a.id=b.course_id
    left join students c on b.student_id=c.id
    where a.id = #{id,jdbcType=INTEGER}
  </select>

@Test
    public void selectCourseStudentsResultMapByPrimaryKey() {
        Course course = coursemapper.selectCourseStudentsResultMapByPrimaryKey(1);
        int actual = course.getStudents().size();
        int expected = 3;
        assertEquals(expected, actual);
    }

多对多

这一点跟 Hibernate
不太一样,需要为涉嫌表建实体类。此外的就跟一对多差不多了,只需要将一对多的思考转换一下即可。

/**
     * 多对多
     * 对于 Student 和 Coureses 都已经存在,只是建立关联关系的情况
     * 只需要往 student_courses 表插入数据即可
     */
    @Override
    public int insertStudentCourses(Student student) {
        int result = 0;
        try {
            int studentId = student.getId();
            List<Course> courses = student.getCourses();
            for (Course course : courses) {
                StudentCourseKey studentCourseKey = new StudentCourseKey(course.getId(), studentId);
                studentCourseMapper.insert(studentCourseKey);
                result ++;
            }
        } catch (Exception e) {
            logger.error("插入失败:{}", e.toString());
        }
        return result;
    }

总结

对此 MyBatis 的利用紧倘诺 sql 的编辑。先从单表先导,领悟 Example
类的用法,对理解 xml
映射及部署很有赞助。对于一对一、一对多、多对多的用法,关键是思想的转换,先明了了解一对一关联的用法,此外多少个就解决了。

源码是最好的文档
迎接指出不足

参考

网站地图xml地图