最近刚开发完一个项目,用了MyBatis作为数据库持久。今天分析总结一下使用的心得。
Mybatis是一个半自动化的数据持久组件。和之前接触的Hibernate有很大的区别。Hibernate是全自动化的数据库组件,有HQL语言,使用起来很方便。但是调优的话只能在那一堆的属性里面抠了。Mybatis不提供SQL生成功能,初学者会觉得和直接拼JDBC没多大区别,只不过是对返回的数据做了一下封装罢了。而正是因为这个原因,我们的可创造性才得以体现出来。
工程准备:
1. Eclipse 3.7.2一个;
2. Mybatis3.1.1 相关jar包
3. Junit 4相关jar包
4. 数据库连接的必要jar包。(用的是MySQL5.0.22和c3p0 0.9.0)
精简后的jar包如下。我自己用的是IvyDE插件管理。能把包找全就可以了。
创建mybatis-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> <settings> <setting name="lazyLoadingEnabled" value="false" /> </settings> <typeAliases> <typeAlias alias="User" type="com.wheat.pojo.User" /> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver" /> <property name="poolMaximumActiveConnections" value="100" /> <property name="poolMaximumIdleConnections" value="100" /> <property name="url" value="jdbc:mysql://localhost:3306/fitweber?characterEncoding=UTF-8" /> <property name="username" value="root" /> <property name="password" value="123456" /> </dataSource> </environment> </environments> <mappers> <mapper resource="META-INF/mappers/UserMapper.xml" /> </mappers> </configuration>
在数据库建一张名为userinfo的表,表中只含两个字段,id varchar(32)和userName varchar(100)。这表只是方便测试。
重头戏是UserMapper.xml。里面包含了对表的查询、插入和删除。涉及了所有我想要总结的东西。update语句只要在insert语句上修改一下就可以了。
<?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.wheat.dao.UserDao"> <!-- <resultMap type="com.wheat.pojo.User" id="UserMaps"> --> <!-- </resultMap> --> <select id="returnAllUser" resultType="java.util.HashMap" > <![CDATA[ SELECT ID,USERNAME FROM USERINFO ]]> </select> <select id="searchUserById" resultType="java.util.HashMap" parameterType="String"> <![CDATA[ SELECT ID,USERNAME FROM USERINFO ]]> <trim prefix="where" prefixOverrides=","> <if test="_parameter != null">ID =#{id}</if> </trim> </select> <select id="searchUserByParameters" resultType="java.util.HashMap" parameterType="java.util.HashMap"> <![CDATA[ SELECT ID,USERNAME FROM USERINFO ]]> <trim prefix="where" prefixOverrides="AND | OR"> <if test="id != null">ID =#{id}</if> <if test="userName != null">AND USERNAME LIKE '%'|| #{userName} || '%'</if> </trim> </select> <insert id="saveUserOneByOne" parameterType="java.util.HashMap"> <![CDATA[ INSERT INTO USERINFO (ID,USERNAME) VALUES ]]> (#{id},#{userName}) </insert> <insert id="saveUsersBatch" parameterType="java.util.ArrayList"> <![CDATA[ INSERT INTO USERINFO (ID,USERNAME) VALUES ]]> <foreach collection="list" item="item" index="index" separator=","> (#{item.id},#{item.userName}) </foreach> <![CDATA[ ]]> </insert> <delete id="delUserBatch" parameterType="java.lang.String"> <![CDATA[ DELETE FROM USERINFO WHERE ID IN ]]> <foreach collection="array" item="item" index="index" open="(" separator="," close=")"> #{item} </foreach> </delete> <insert id="" statementType="CALLABLE"> </insert> </mapper>
在这里没有使用resultMap,而是用了java.util.HashMap。使用resultMap的本意是想让Mybatis直接返回实体类对象。可是在实际开发的很多场景中,我们从数据库拿出来后也是对属性进行分拆。与其每个类都要写一次resultMap来与之对应,倒不如直接返回一个HashMap。原因在于,数据库的字段如果按照规范来设计,和实体类的中的差别不会太大。正常来说,应该实体类的创建是根据数据库来的,二者的字段是比较一致的。在返回的HashMap中可以直接通过key来取得属性的值。有一点需要注意的是,返回的key值都是大写的。如果是要在JSP上表现,EL表达式对Map的支持和实体类几乎一样。也支持Object.xxx的操作。更有意思的是,HashMap和Json其实是相通的结构,可以说是近亲。都是键值对应的设计。如果是Ajax请求Json格式返回的话。直接把HashMap通过Json-lib一转就可以直接返回了。若返回的是实体对象还要转多一道手。
......
<if test="_parameter != null">ID =#{id}</if>
......
传进来的变量名应该叫id才对。怎么变成_parameter了呢?之前我也是id != null 这样写的。但这样会报一个Caused by:org.apache.ibatis.reflection.ReflectionException: There is no getter forproperty named 'id' in 'class java.lang.String'的错误。原因也是因为Mybatis的取参方式也是按照值对的关系来寻找的。你单单传一个String类型进来他本该新建一个key为id,value为String的Map对象。但他没有这样做,他偷懒把单个传入的值都存在了一个叫_parameter的私有变量里。所以只好这样访问了。下面一种使用HashMap来传参的方式可以避开这种情况。
......
parameterType="java.util.HashMap">
......
可以将各种类型的参数放在HashMap中传过来,可以是String、Int,也可以是List、Array。可以较为灵活地配置SQL的查询条件。取参方式和实体类一样。
AND USERNAME LIKE '%'|| #{userName} || '%'
上面是模糊匹配的写法。
还要定义一个接口类供调用。类名要和Mapper文件中的namespace对应。如下:
namespace="com.wheat.dao.UserDao"
@SuppressWarnings("rawtypes")
public interface UserDao {
public List<Map> returnAllUser();
public List<Map> searchUserById(String id);
public List<Map> searchUserByParameters(HashMap<String, String> requestParameters);
public void saveUserOneByOne(HashMap<String, String> user);
public void saveUsersBatch(ArrayList<HashMap<String, String>> users);
public void delUserBatch(String[] userIds);
}
如何让程序跑起来?这里要靠Junit了。
@Test
@SuppressWarnings("rawtypes")
public void TestUserDaoWithSingleParameter() throws IOException{
String resource = "META-INF/conf/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
UserDao userDao = session.getMapper(UserDao.class);
List<Map> users= userDao.searchUserById("1");
for(Map user:users){
String userName = (String) user.get("USERNAME");
System.out.println(userName);
}
session.close();
}
加载配置文件,打开session,拿到接口,执行SQL语句。这个过程就不说了。
最后想要验证的是,逐个插入和批量插入的优缺点。本来是没有什么想验证的。批量插入怎么都要比逐个入的速度快吧?我也是这样想的。但是在做单元测试的时候发现了,批量插入比逐个插入慢的情况。而且在几台机子上都存在这样的情况。
测试方法如下: @Test
public void TestUserSaveBatchWithOneByOne()throws IOException{
String resource = "META-INF/conf/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
UserDao userDao = session.getMapper(UserDao.class);
ArrayList<HashMap<String, String>> users = new ArrayList<HashMap<String,String>>();
int i=0;
for(i=0;i<testTime;i++){
HashMap<String, String> userMap = new HashMap<String, String>();
userMap.put("id", CommonUtils.generateUUID());
userMap.put("userName", "User"+i);
users.add(userMap);
}
long beginTime=0,endTime=0;
beginTime= System.currentTimeMillis();
System.out.println(beginTime+":OneByOne begin!");
for(HashMap<String, String> userMap : users){
userDao.saveUserOneByOne(userMap);
}
endTime = System.currentTimeMillis();
System.out.println(endTime+":OneByOne end!costs "+(endTime-beginTime)+"ms.");
users.clear();
for(i=0;i<testTime;i++){
HashMap<String, String> userMap = new HashMap<String, String>();
userMap.put("id", CommonUtils.generateUUID());
userMap.put("userName", "User"+i);
users.add(userMap);
}
beginTime = System.currentTimeMillis();
System.out.println(beginTime+":Batch begin!");
userDao.saveUsersBatch(users);
endTime = System.currentTimeMillis();
System.out.println(endTime+":Batch end!costs "+(endTime-beginTime)+"ms.");
session.close();
}
testTime是测试次数,一个常量。先使用程序生成testTime个待测试对象,加入users队列中。执行逐个插入语句。打印消耗的时间。清空users,重新生成testTime个待测试对象,加入users,执行批量插入语句,打印消耗的时间。要提的一点是,我是使用程序生成的UUID而不是让程序的ID自增。这有可能成为批量插入在超过一定数量级后就慢于逐个插入的原因。
testTime为500时,
testTime为1000时,
testTime为3000时,
testTime为5000时,
批量插入会随着插入个数的增加,插入速度急剧下降。这和机器的性能、内存有很大关系。因为批量插入在构建庞大的插入语句时,吃掉了大量的机器内存导致系统缓慢乃至宕机。所以在考虑使用批量插入的时候要考虑机器的性能。在批量插入的性能等于逐个插入时,切换回逐个插入。