近来有空,鉴于工作经常会使用到mybatis。所以想将这个框架研究的更加彻底一些!
【MyBatis源码分析】整个文章结构会在每一部分源码分析的开头列出要分析的源码的实例,比如:
- 分析加载解析XML配置流程,就会先写相关节点的xml配置及解析的源码展示。
- 分析mybatis四大对象流程,就会先写针对单个对象展示源码。
整个系列文章,在本文中会一次性地将所有的代码示例写完,之后就针对这些代码一部分一部分进行分析,探究Mybatis原理。
一、 建表
这里准备一段SQL:
drop table if exists mail;
create table mail
(
id int auto_increment not null comment '主键id',
create_time datetime not null comment '创建时间',
modify_time timestamp not null comment '修改时间',
web_id int not null comment '站点id,1表示新浪,2表示QQ,3表示搜狐,4表示火狐',
mail varchar(50) not null comment '邮箱名',
use_for varchar(30) comment '邮箱用途',
primary key(id),
index use_for(use_for),
unique index web_id_mail(web_id, mail)
)charset=utf8 engine=innodb comment='邮箱表';
很多人可能有不止一个邮箱,新浪的、腾讯的、搜狐的,每个邮箱可能又有不一样的用途,这里就拿邮箱做一个例子。
建立每张表的时候应当注意唯一约束,像这里,一个网站下的邮箱一定是唯一的,不可能在新浪下同时存在两个名为"123@sina.com"的邮箱名,因此对web_id+mail做唯一索引。
二、 建实体类
在SQL层面不同的词语使用"_"分割,在Java层面不同的词语则使用驼峰命名法:
- 对于类名/接口名/枚举类,使用首字母大写的驼峰命名法
- 对于字段,使用首字母小写的驼峰命名法
mail表建立实体类:
public class Mail {
/**
* 主键id
*/
private long id;
/**
* 创建时间
*/
private Date createTime;
/**
* 修改时间
*/
private Date modifyTime;
/**
* 网站id,1表示新浪,2表示QQ,3表示搜狐,4表示火狐
*/
private int webId;
/**
* 邮箱
*/
private String mail;
/**
* 用途
*/
private String useFor;
public Mail() {
}
public Mail(int webId, String mail, String useFor) {
this.webId = webId;
this.mail = mail;
this.useFor = useFor;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getModifyTime() {
return modifyTime;
}
public void setModifyTime(Date modifyTime) {
this.modifyTime = modifyTime;
}
public int getWebId() {
return webId;
}
public void setWebId(int webId) {
this.webId = webId;
}
public String getMail() {
return mail;
}
public void setMail(String mail) {
this.mail = mail;
}
public String getUseFor() {
return useFor;
}
public void setUseFor(String useFor) {
this.useFor = useFor;
}
@Override
public String toString() {
return "MailDO [id=" + id + ", createTime=" + createTime + ", modifyTime=" + modifyTime + ", webId=" + webId + ", mail=" + mail + ", useFor="
+ useFor + "]";
}
}
注意实体类一定要重写toStirng()方法,便于定位问题。
三、 建数据访问层
对于数据访问层通常有如下约定:
- 数据访问层使用Dao命名,它定义了对表的基本增删改查操作
- 数据访问层之上使用Service命名,它的作用是对于数据库的多操作进行组合,比如先查再删、先删再增、先改再查再删等等,这些操作不会放在Dao层面去操作,而会放在Service层面去进行组合
那么,首先定义一个MailDao,我定义增删改查五个方法,其中查询两个方法,一个查单个,一个查列表:
public interface MailDao {
/**
* 插入一条邮箱信息
*/
public long insertMail(Mail mail);
/**
* 删除一条邮箱信息
*/
public int deleteMail(long id);
/**
* 更新一条邮箱信息
*/
public int updateMail(Mail mail);
/**
* 查询邮箱列表
*/
public List<Mail> selectMailList();
/**
* 根据主键id查询一条邮箱信息
*/
public Mail selectMailById(long id);
}
接着是Dao的实现类,通常以"Impl"结尾,"Impl"是关键字"Implements"的缩写,表示接口实现类的意思。MailDao的实现类就命名为MailDaoImpl了,代码为:
public class MailDaoImpl implements MailDao {
private static final String NAME_SPACE = "MailMapper.";
private static SqlSessionFactory ssf;
private static Reader reader;
static {
try {
reader = Resources.getResourceAsReader("mybatis/config.xml");
ssf = new SqlSessionFactoryBuilder().build(reader);
}
catch (IOException e) {
e.printStackTrace();
}
}
@Override
public long insertMail(Mail mail) {
SqlSession ss = ssf.openSession();
try {
int rows = ss.insert(NAME_SPACE + "insertMail", mail);
ss.commit();
if (rows > 0) {
return mail.getId();
}
return 0;
} catch (Exception e) {
ss.rollback();
return 0;
} finally {
ss.close();
}
}
@Override
public int deleteMail(long id) {
SqlSession ss = ssf.openSession();
try {
int rows = ss.delete(NAME_SPACE + "deleteMail", id);
ss.commit();
return rows;
} catch (Exception e) {
ss.rollback();
return 0;
} finally {
ss.close();
}
}
@Override
public int updateMail(Mail mail) {
SqlSession ss = ssf.openSession();
try {
int rows = ss.update(NAME_SPACE + "updateMail", mail);
ss.commit();
return rows;
} catch (Exception e) {
ss.rollback();
return 0;
} finally {
ss.close();
}
}
@Override
public List<Mail> selectMailList() {
SqlSession ss = ssf.openSession();
try {
return ss.selectList(NAME_SPACE + "selectMailList");
} finally {
ss.close();
}
}
@Override
public Mail selectMailById(long id) {
SqlSession ss = ssf.openSession();
try {
return ss.selectOne(NAME_SPACE + "selectMailById", id);
} finally {
ss.close();
}
}
}
四、 配置相关环境及SQL的XML文件
MyBatis的配置文件有两个,一个是环境的配置config.xml,一个是具体SQL的编写mail.xml。首先看一下config.xml:
<?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>
<properties resource="properties/db.properties" />
<settings>
<setting name="logImpl" value="LOG4J" />
<!-- 自定义配置LOG4J为输出日志 默认没有设置-->
<setting name="cacheEnabled" value="true" />
<!-- 对在此配置文件下的所有cache 进行全局性开/关设置。 默认true -->
<setting name="lazyLoadingEnabled" value="true" />
<!-- 全局性设置懒加载。如果设为‘false’,则所有相关联的都会被初始化加载。 默认true -->
<setting name="multipleResultSetsEnabled" value="true" />
<!-- 允许和不允许单条语句返回多个数据集(取决于驱动需求)。 默认true -->
<setting name="useColumnLabel" value="true" />
<!-- 使用列标签代替列名称。不同的驱动器有不同的作法。参考一下驱动器文档,或者用这两个不同的选项进行测试一下。 默认true-->
<setting name="useGeneratedKeys" value="true" />
<!-- 允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。 默认true-->
<setting name="autoMappingBehavior" value="PARTIAL" />
<!-- 指定MyBatis 是否并且如何来自动映射数据表字段与对象的属性。PARTIAL将只自动映射简单的,没有嵌套的结果。FULL 将自动映射所有复杂的结果。默认PARTIAL -->
<setting name="defaultExecutorType" value="SIMPLE" />
<!-- 配置和设定执行器,SIMPLE 执行器执行其它语句。REUSE 执行器可能重复使用prepared statements 语句,BATCH执行器可以重复执行语句和批量更新。 默认SIMPLE-->
<setting name="defaultStatementTimeout" value="25000" />
<!-- 设置一个时限,以决定让驱动器等待数据库回应的多长时间为超时 默认没有设置 -->
<setting name="safeRowBoundsEnabled" value="false" />
<!-- 允许在嵌套语句中使用RowBounds。如果允许,设置为false。 默认false-->
<setting name="mapUnderscoreToCamelCase" value="false" />
<!-- 使数据库列名称A_COLUMN自动映射到骆驼类型Java属性名称aColumn。 -->
<setting name="localCacheScope" value="SESSION" />
<!-- MyBatis使用本地缓存来防止循环引用,并加速重复的嵌套查询。默认情况下(SESSION)会话中执行的所有查询都被缓存。如果localCacheScope = STATEMENT本地会话将仅用于语句执行,则不会在对同一个SqlSession的两个不同调用之间共享数据。 默认SESSION -->
<setting name="jdbcTypeForNull" value="OTHER" />
<!-- 当没有为参数提供特定的JDBC类型时,指定空值的JDBC类型。一些驱动程序需要指定列JDBC类型,但其他驱动程序则使用NULL,VARCHAR或OTHER等通用值。 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode ,toString" />
<!-- 指定哪个对象的方法触发延迟加载 用逗号分隔的方法名称列表 -->
</settings>
<typeAliases>
<typeAlias type="model.Mail"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driveClass}"/>
<property name="url" value="${url}"/>
<property name="username" value="${userName}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mail.xml"/>
</mappers>
</configuration>
接着是编写SQL语句的mail.xml:
<?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="MailMapper">
<resultMap type="Mail" id="MailResultMap">
<result column="id" property="id" />
<result column="create_time" property="createTime" />
<result column="modify_time" property="modifyTime" />
<result column="web_id" property="webId" />
<result column="mail" property="mail" />
<result column="use_for" property="useFor" />
</resultMap>
<sql id="fields">
id, create_time, modify_time, web_id, mail, use_for
</sql>
<sql id="fields_value">
null, now(), now(), #{webId}, #{mail}, #{useFor}
</sql>
<insert id="insertMail" parameterType="Mail" useGeneratedKeys="true" keyProperty="id">
insert into mail(
<include refid="fields" />
) values(
<include refid="fields_value" />
);
</insert>
<delete id="deleteMail" parameterType="java.lang.Long">
delete from mail where id = #{id};
</delete>
<update id="updateMail" parameterType="Mail">
update mail
<set>
<if test="web_id != 0">
web_id = #{webId}
</if>
<if test="mail != null">
mail = #{mail}
</if>
<if test="use_for != null">
use_for = #{useFor}
</if>
</set>
where id = #{id};
</update>
<select id="selectMailList" resultMap="MailResultMap">
select <include refid="fields" /> from mail;
</select>
<select id="selectMailById" resultMap="MailResultMap" parameterType="java.lang.Long">
select <include refid="fields" /> from mail where id = #{id};
</select>
</mapper>
这个mail.xml我尽量写得全一点,这样后面分析的时候都会有代码示例,mail.xml中包括:
- resultMap
- <sql>标签
- 插入主键返回主键id
- 动态sql
五、 建立单元测试类
软件的正确性离不开良好的测试,通常测试有两种方式:
- 写main函数,这种方式我基本不使用,除非是测试一个很小的功能点比如Math.round这种,这种代码写完我也会直接删除的,不会留着提交到代码库上
- 使用单元测试工具比如junit,这是我常用的方式
接着看一下单元测试代码:
public class TestMyBatis {
private static MailDao mailDao;
static {
mailDao = new MailDaoImpl();
}
@Test
public void testInsert() {
Mail mail1 = new Mail(1, "123@sina.com", "个人使用");
Mail mail2 = new Mail(2, "123@qq.com", "企业使用");
Mail mail3 = new Mail(3, "123@sohu.com", "注册账号使用");
System.out.println(mailDao.insertMail(mail1));
System.out.println(mailDao.insertMail(mail2));
System.out.println(mailDao.insertMail(mail3));
}
@Test
public void testDelete() {
System.out.println(mailDao.deleteMail(1));
}
@Test
public void testUpdate() {
Mail mail = new Mail(2, "123@qq.com", "个人使用");
mail.setId(2);
System.out.println(mailDao.updateMail(mail));
System.out.println(mailDao.selectMailById(2));
}
@Test
public void testSelectOne() {
System.out.println(mailDao.selectMailById(2));
}
@Test
public void testSelectList() {
List<Mail> mailList = mailDao.selectMailList();
if (mailList != null && mailList.size() != 0) {
for (Mail mail : mailList) {
System.out.println(mail);
}
}
}
}
正确的情况下,应当五个方法跑出来全部是绿色的进度条。
当然,单元测试也可以单独跑每一个,Eclipse/MyEclipse/idea都支持的!
现在简单的一个访问数据源的demo就出来,接下来我们就通过这个demo去debug相关mybatis的源码!