博由
在大概介绍完一些关于Mybatis Config的使用之后,我们进一步的了解Mapper Config文件的使用,大致可以分为:
1. cache-ref:缓存引用
2. cache:引入自定义的缓存
3. resultMap:结果集映射
4. select:对应SQL select
5. insert:对应SQL insert
6. update:对应SQL update
7. delete:对应SQL delete
映射文件
关系:--- Table <---> Mapper <---> Bean(Pojo)
mybatis是ORM框架,其中最大的一个问题在于如何简历javaBean与Table之间的关系,然后通过操作javaBean就可以操作DB,以屏蔽DB的差异问题等等,其实就是简单理解为:Mapper目的就是建立Table和Bean的一一对应关系。
解析原理
代码链路:
1. XmlConfigBuilder(parseConfiguration --> mapperElement) -> XmlMapperBuidler
2. XmlMapperBuidler(parse -> configurationElement)
configurationElement 内部就是初始化mapper配置文件的操作,接下来个个击破
sql
这个比较简单,其实就是SQL模板,提高复用性。
1. 定义:
<sql id="tableAll">
SELECT * FROM ${table}
</sql>
2. 使用
<select id="findById"
parameterType="int"
resultMap="userMap">
<!-- include引用 -->
<include refid="tableAll">
<!-- 注入sql定义的${info} -->
<property name="table" value="user"/>
</include> WHERE id = #{id}
</select>
Cache
Mybatis的缓存结构
* | -- Cache
* | -- -- PerpetualCache 其实就是HashMap的实现
* | -- -- -- BlockingCache 阻塞Cache,实际上就是加个锁的实现
* | -- -- -- FifoCache 队列Cache,First in First Out
* | -- -- -- LoggingCache 日志Cache
* | -- -- -- WeaKCache 弱引用Cache
* | -- -- -- TransactionalCache 事务Cache
* | -- -- -- LRUCache LRU Cache
* | -- -- -- ScheduledCache 定时Cache
* | -- -- -- SerializedCache 序列化Cache
* | -- -- -- SoftCache 和Weak有点类似,引用Cache
* | -- -- -- SynchronizedCache 同步Cache
这些都是预定义的缓存类,通过装时器模式。
使用自定义缓存
1. implements Cache interface
2. Mapper Config 配置<cache></cache>
implements Cache Interface
public class HashMapCache implements Cache{
// 缓存唯一标识
private String id;
// 缓存名字
private String name;
// k:主键ID;v:缓存内容
private Map<Object, Object> cache = new HashMap<Object, Object>();
public HashMapCache(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public int getSize() {
return cache.size();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
}
Mapper Config 配置
<!--
type: 缓存类型,默认PerpetualCache(PERPETUAL)
eviction: 缓存回收策略,默认LRU
flushInterval: 缓存刷新间隔
size: 缓存的大小
readOnly: default false,是否只读
blocking: default false,是否
-->
<cache type="com.mapper.caches.HashMapCache">
<property name="name" value="testCache" />
</cache>
缓存如何使用代码解析
XmlMapperBuilder(->cacheElement
->builderAssisant.useNewCache)
useNewCache
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build(); // build方法创
// 创建之后添加cache到配置中
configuration.addCache(cache);
currentCache = cache;
return cache;
}
单元测试
TestCase: 同一个Session中,无法使用到缓存
@Test
public void testCacheSql(){
SqlSession session = instance.getSession();
long t1 = System.currentTimeMillis();
User user1 = session.selectOne(User.class.getName() + ".findById", 5);
System.out.println(System.currentTimeMillis() - t1);
long t2 = System.currentTimeMillis();
User user2 = session.selectOne(User.class.getName() + ".findById", 5);
long t3 = System.currentTimeMillis();
User user3 = session.selectOne(User.class.getName() + ".findById", 5);
System.out.println(System.currentTimeMillis() - t3);
}
TestCase2: 不同事务中,可以使用到缓存
@Test
public void testMyCache(){
// 这是的查询,在commit时,会将TransactionalCache中的key,value信息
// 重新设置到LoggingCache->HashMapCache中。
User user1 = UserDao.getInstance().findById(5);
// 这样就可以使用到user1操作的cache结果了,这里查询时,不需要使用到TransactionalCache,
// 而是在执行之前就使用了HashMapCache
User user2 = UserDao.getInstance().findById(5);
System.out.print(user1 + " :=====: " + user2);
}
源码解析现象
HashMapCache 被 LoggingCache包装
public Cache build() {
setDefaultImplementations();
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
// 注意这里使用LoggingCache来包装HashMapCache
cache = new LoggingCache(cache);
}
return cache;
}
TransactionalCache的诞生
- CachingExecutor.java
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 在构建Configuraion的mappedStatement时,会将Cache设置
Cache cache = ms.getCache();
if (cache != null) {
// 如果设置flushCache,此时会先刷新cache
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
// 在执行操作前,先查看cache是否存在
// 但是这里不是先使用的loggingCache而是TransactionalCache
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 查询后,会将结果设置到cache中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
- TransactionalCacheManager
public class TransactionalCacheManager {
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
// 刚才在cachingExecutor.query中,在执行查询之前,选执行了
// tcm.getObject(cache,key),就是这个方法
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
// 注意每次都是根据LogginCache获取,都会获取到TransactionalCache
// 这是我们看看TransactionalCache的putObject,并不是我们设想的
// 会设置到LoggingCache中
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
// 获取cache
private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
// 第一次cache操作时,tcm不存在,会将loggingCache作为key,transactionalCache
// 作为value执行操作。
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
}
- TransactionalCache
private Cache delegate;
private boolean clearOnCommit;
private Map<Object, Object> entriesToAddOnCommit;
private Set<Object> entriesMissedInCache;
@Override
public void putObject(Object key, Object object) {
// 不是使用了delegate.put而是将结果设置到了entriesToAddOnCommit中
entriesToAddOnCommit.put(key, object);
}
@Override
public Object getObject(Object key) {
// issue #116
// 先从LoggingCache获取结果,如果hit了, 获取结果但是不是返回
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
// clearOnCommit才是关键,如果是已经提交了,那么返回null
if (clearOnCommit) {
return null;
} else {
return object;
}
}
// 关键: 只有事务结束后,才会把TransactionalCache的缓存刷到LoggingCache
// 因此TestCase1,无法使用缓存,也就会导致hits rate为0
// TestCase2,每个find都是独立的,第一查询commit之后,会把缓存存放在LogginCache
// 然后就可以直接使用自己的缓存了。
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
// 在commit之后,会将信息刷到LogginCache
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
// 这里就是刷的过程
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
TransactionalCache的作用:
可能你会问为啥要加上TransactionalCache,这是为了保证事务数据的一致性设计的,如果我们不加设个Cache,那么直接将结果存放在LoggingCache,一旦出现事务错误,下次查询会查询到错误事务的结果,这是不合理的,只有在commit之后,commit是认为事务正确的标识,正确了才把数据刷到对应的Cache,不然会出现Cache错误。
Cache 转换结构:
1. HashMapCache 被 LoggingCache 包装;
2. TransactionalCacheManager.getObject(cache, key),会初次生成TransactionalCache
3. 第一次查询结果后,会把结果通过TransactionalCacheManager.putObject(cache,key,value) 设置到TransactionalCache的entriesToAddOnCommit中,而不是LogginCache中;
4. 如果没有进行commit操作,执行第二次重复查询实际上使用的缓存不是自定义的HashMapCache,而是TransactionalCache;如果要确保使用到自己的Cache,那么commit时,会将TransactionalCache中的entriesToAddOnCommit刷到LoggingCache中,也就刷到了HashMapCache了,下次查询就可以使用到。
CacheRef
目的就是引用现成的cache,比较简单,例如:
<cache-ref namespace="com.mapper.pojo.Cache" />
简单看看源码
XmlMapperBuilder->cacheRefElement
private void cacheRefElement(XNode context) {
if (context != null) {
// 设置cacheRef的映射表<currentNamespace, namespace>
// currentNamespace: cacheRef所在的namespace
// cachRef->namespace: 是引用的Cache空间 configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
// 这里就是解析cacheRef的过程,内部策略就是缓存的namesapce问题比较简单就不做详细介绍了。
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
ResultMap
ResultMap就是结果集的映射,一般的情况我们使用resultType就可以,但是存在一些情况resultType无法满足,这个时候就可以使用resultMap实现更加复杂多样的映射关系。
映射注入的方式
resultMap最大的目的在于结果集的映射,从Table到Bean是如何查询结果最终设置到Bean中的。主要有两种方式:
1. 基于属性注入(Setter/Getter)
2. 基于构造函数注入(Constrcutor)
Property 注入 Setter/Getter
<!--
id: resultMap的唯一标识
type: resultMap的结果映射类型
extends: 继承,resultMap之间可以实现继承的
-->
<resultMap id="userMap" type="User" extends="baseUserMap">
<!-- 主键标识
property: 属性名(类中的)
column: 对应table的列名
javaType: property字段的类型
jdbcType: column对应数据库的类型
typeHandler: 指定的类型处理器,前面篇章有所介绍,这里不做详细介绍。
-->
<id property="id" column="id" javaType="int" jdbcType="INTEGER"/>
<!-- result 非主键字段的标识,内部属性与ID一致 -->
<result property="createdAt" column="created_at" />
<result property="updatedAt" column="updated_at" />
</resultMap>
属性注入过程:
1. XmlMapperBuilder.resultMapElement 将resultMap信息加载到Configuration中,
其中一个resultMap标签对应一个ResultMap类,resultMap内部属性对应ResultMapping类;
2. 在加载配置文件之后,在执行query时,出触发PreparedStatementHandler.query -> DefaultResultSetHandler.handleResultSets -> DefaultResultSetHandler.handleResultSet ->
DefaultResultSetHandler.handleRowValues ->
DefaultResultSetHandler.handleRowValuesForSimpleResultMap ->
DefaultResultSetHandler.getRowValue ->
DefaultResultSetHandler.applyAutomaticMappings ->
DefaultResultSetHandler.createAutomaticMappings |
DefaultResultSetHandler.applyPropertyMappings 这个方法中,进行了属性注入
Constructor 构造函数注入
<resultMap id="baseUserMap" type="BaseUser">
<!-- 构造方法注入数据,是通过目标类的构造方法注入的 -->
<constructor>
<idArg column="id" javaType="int" jdbcType="INTEGER" />
<arg column="name" javaType="string" jdbcType="VARCHAR" />
</constructor>
</resultMap>
构造函数注入过程:
和属性注入有点相似,但是DefaultResultSetHandler.createResultObject进行了判断,如何创建。
DefaultResultSetHandler.getRowValue ->
DefaultResultSetHandler.createResultObject -> 在注入属性之前先进行构造函数注入,然后在属性注入。
这是合理的,都是现有对象,然后在设置属性。
实体映射关系
实体的映射关系主要有:
1. OneToOne
2. OneToMany | ManyToOne
3. ManyToMany
OneToOne
一对一的关系,案例:夫妻关系
SQL:
create table husband (
`id` int not null auto_increment,
`name` varchar(255) not null default '' comment '姓名',
`wife_id` int not null default 0 comment '妻子ID',
`created_at` datetime not null default '1970-01-01' comment '创建时间',
`updated_at` datetime not null default '1970-01-01' comment '更新时间',
primary key(`id`),
unique key `uk_wife_id` (`wife_id`)
)ENGINE INNODB charset=utf8 COMMENT '丈夫表';
create table wife (
`id` int not null auto_increment,
`name` varchar(255) not null default '' comment '姓名',
`created_at` datetime not null default '1970-01-01' comment '创建时间',
`updated_at` datetime not null default '1970-01-01' comment '更新时间',
primary key(`id`)
)ENGINE INNODB charset=utf8 COMMENT '妻子表';
POJO:
public class Husband implements Serializable {
private int id;
private String name;
private int wife_id;
private Date createdAt;
private Date updatedAt;
private Wife wife;
public int getWife_id() {
return wife_id;
}
public void setWife_id(int wife_id) {
this.wife_id = wife_id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public Date getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
public Wife getWife() {
return wife;
}
public void setWife(Wife wife) {
this.wife = wife;
}
@Override
public String toString() {
return "Husband{" +
"id=" + id +
", name='" + name + '\'' +
", wife_id=" + wife_id +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
", wife=" + wife +
'}';
}
}
public class Wife implements Serializable {
private int id;
private String name;
private Date createdAt;
private Date updatedAt;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public Date getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
@Override
public String toString() {
return "Wife{" +
"id=" + id +
", name='" + name + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';
}
}
Mapper
1. Husband.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="com.mapper.pojo.Husband">
<resultMap id="husbandMap" type="Husband" >
<id property="id" column="id"/>
<result property="name" column="name" />
<result property="wife_id" column="wife_id"/>
<result property="createdAt" column="created_at" />
<result property="updatedAt" column="updated_at" />
<!-- 建立关联关系 -->
<association property="wife"
column="wife_id"
fetchType="lazy"
select="com.mapper.pojo.Wife.findById"
javaType="Wife"/>
</resultMap>
<select id="findById" parameterType="int" resultMap="husbandMap" >
SELECT * FROM husband WHERE id=#{id}
</select>
</mapper>
2. Wife.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="com.mapper.pojo.Wife">
<resultMap id="wifeMap" type="Wife" >
<id property="id" column="id"/>
<result property="name" column="name" />
<result property="createdAt" column="created_at" />
<result property="updatedAt" column="updated_at" />
</resultMap>
<select id="findById" parameterType="int" resultMap="wifeMap" >
SELECT * FROM wife WHERE id=#{id}
</select>
</mapper>
OneToMany|ManyToOne
1对多或者多对1的问题,案例:一个教师多个学生
SQL:
create table teacher (
`id` int not null auto_increment,
`name` varchar(255) not null default '' comment '姓名',
`created_at` datetime not null default '1970-01-01' comment '创建时间',
`updated_at` datetime not null default '1970-01-01' comment '更新时间',
primary key(`id`)
)ENGINE INNODB charset=utf8 COMMENT '教师表';
create table student (
`id` int not null auto_increment,
`name` varchar(255) not null default '' comment '姓名',
`tid` int not null default 0 comment '教师ID',
`teacher_name` varchar(255) not null default '' comment '教师名称',
`created_at` datetime not null default '1970-01-01' comment '创建时间',
`updated_at` datetime not null default '1970-01-01' comment '更新时间',
primary key(`id`)
)ENGINE INNODB charset=utf8 COMMENT '学生表';
POJO:
1. Student.java
public class Student implements Serializable {
private int id;
private String name;
private int tid;
private String teacherName;
private Date createdAt;
private Date updatedAt;
private Teacher teacher;
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getTid() {
return tid;
}
public void setTid(int tid) {
this.tid = tid;
}
public String getTeacherName() {
return teacherName;
}
public void setTeacherName(String teacherName) {
this.teacherName = teacherName;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public Date getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", tid=" + tid +
", teacherName='" + teacherName + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
", teacher=" + teacher +
'}';
}
}
2. Teacher.java
public class Teacher implements Serializable {
private int id;
private String name;
private Date createdAt;
private Date updatedAt;
private List<Student> students = new ArrayList<Student>();
public List<Student> getStudents() {
return students;
}
public void setStudents(List<Student> students) {
this.students = students;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public Date getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}
@Override
public String toString() {
return "Teacher{" +
"id=" + id +
", name='" + name + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
", students=" + students +
'}';
}
}
Mapper:
1. Student.xml
<mapper namespace="com.mapper.pojo.Student">
<resultMap id="studMap" type="Student">
<id property="id" column="id" />
<result property="name" column="name" />
<result property="tid" column="tid" />
<result property="teacherName" column="teacher_name" />
<result property="createdAt" column="created_at" />
<result property="updatedAt" column="updated_at" />
<association property="teacher" column="tid" select="findByTid"/>
</resultMap>
<select id="findById" parameterType="int" resultMap="studMap">
SELECT * FROM student WHERE id = #{id}
</select>
<select id="findByTid" parameterType="int" resultType="Teacher" >
SELECT * FROM teacher WHERE id = #{tid}
</select>
</mapper>
2. Teacher.xml
<mapper namespace="com.mapper.pojo.Teacher">
<resultMap id="teacherDtoMap" type="Teacher" >
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="createdAt" column="created_at" />
<result property="updatedAt" column="updated_at" />
<!-- 关联关系:OneToMany -->
<collection property="students" ofType="Student" >
<id property="id" column="sid"/>
<result property="name" column="sname"/>
<result property="tid" column="tid" />
<result property="teacherName" column="teacher_name" />
<result property="createdAt" column="s_created_at" />
<result property="updatedAt" column="s_updated_at" />
</collection>
</resultMap>
<select id="findById" parameterType="int" resultMap="teacherDtoMap">
SELECT
teacher.id AS id,
teacher.name AS name,
teacher.created_at AS created_at,
teacher.updated_at AS updated_at,
student.id AS sid,
student.name AS sname,
student.tid AS tid,
student.teacher_name AS teacher_name,
student.created_at AS s_created_at,
student.updated_at AS s_updated_at
FROM teacher join student ON teacher.id = student.tid
WHERE teacher.id=#{id}
</select>
</mapper>
貌似Mybatis对象关联关系没Hibernate做的好,不过实际在开发中用的也比较少,感觉不支持双向绑定。多对多貌似实现不了,下次深刻研究下,专题研究,这里不做概述。
insert|select|update|delete
这个就比较简单了,直接使用:
<insert><select><update><delete>四个标签就可以了,
比较简单:
<select id="findById"
parameterType="int"
resultMap="userMap"
>
<include refid="tableAll">
<property name="table" value="user"/>
</include> WHERE id = #{id}
</select>
<!-- insert操作 -->
<insert id="add" parameterType="User">
INSERT INTO user VALUES(null, #{name}, #{createdAt},#{updatedAt});
</insert>
<!-- update更新操作 -->
<update id="update" parameterType="User">
UPDATE user SET name=#{name},created_at=#{createdAt}, updated_at=#{updatedAt}
WHERE id=#{id}
</update>
<!-- 删除操作 -->
<delete id="deleteById" parameterType="int" >
DELETE FROM user
WHERE id = #{id}
</delete>
GitHub地址
branch v1.6: https://github.com/wzpthq/csdn_mybatis.git