在报表类应用中,通常需要根据不同的维度去组合复杂的查询条件,然后构造SQL去执行查询。如果只是通过在程序中简单地拼接SQL语句,工作量会非常大,而且代码可能也非常难以维护。Mybatis支持动态SQL查询功能,可以通过配置动态的SQL来简化程序代码中复杂性,不过,这个颇有点XML编程的韵味,通过XML来处理复杂的数据判断、循环的功能,其实也很好理解。
准备工作
下面,我们首先创建一个MySQL示例表,如下所示:
CREATE TABLE `traffic_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`domain` varchar(64) NOT NULL,
`traffic_host` varchar(64) NOT NULL,
`month` varchar(8) NOT NULL,
`monthly_traffic` int(11) DEFAULT '0',
`global_traffic_rank` int(11) DEFAULT '0',
`native_traffic_rank` int(11) DEFAULT '0',
`rank_in_country` varchar(64) DEFAULT NULL,
`address` varchar(200) DEFAULT NULL,
`email` varchar(50) DEFAULT NULL,
`traffic_type` int(2) DEFAULT '-1',
`status` int(2) DEFAULT '0',
`created_at` date DEFAULT NULL,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`f1` varchar(255) DEFAULT NULL,
`f2` varchar(255) DEFAULT NULL,
`f3` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_traffic` (`domain`,`month`,`traffic_type`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
这个表用来存储域名的流量信息,流量信息我们从互联网上像Alexa、Compete、Quantcast等提供商获取,通过Crawler抓取的方式实现。我们先从简单的查询做起,只是根据某个字段进行查询,说明如何配置使用Mybatis,这里面也包含如何与Spring进行集成。
配置实践
下面是用到的一些资源的定义:
该类对应于traffic_info表中一条记录的数据,我们简单取几个字段,如下所示:
package org.shirdrn.mybatis;
import java.io.Serializable;
public class TrafficInfo implements Serializable {
private static final long serialVersionUID = -8696613205078899594L;
int id;
String domain;
String month;
int monthlyTraffic;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
public String getMonth() {
return month;
}
public void setMonth(String month) {
this.month = month;
}
public int getMonthlyTraffic() {
return monthlyTraffic;
}
public void setMonthlyTraffic(int monthlyTraffic) {
this.monthlyTraffic = monthlyTraffic;
}
@Override
public String toString() {
return "[id=" + id + ", domain=" + domain + ", month=" +
month + ", monthlyTraffic=" + monthlyTraffic + "]";
}
}
org.shirdrn.mybatis.mapper.TrafficInfoMapper接口类
该类定义了一个与SQL配置进行映射的基本操作,实际的SQL配置有专门的XML文件来进行配置。该接口定义了如下操作:
package org.shirdrn.mybatis.mapper;
import java.util.List;
import java.util.Map;
import org.shirdrn.mybatis.TrafficInfo;
public interface TrafficInfoMapper {
/**
* 根据指定id去查询记录,结果至多只有一条
* @param id
* @return
*/
TrafficInfo getTrafficInfo(int id);
/**
* 根据指定的domain参数查询记录,返回一个记录的列表
* @param domain
* @return
*/
List getTrafficInfoList(String domain);
/**
* 根据一个 字段domain进行查询,但是存在多个domain的值,传入一个数组
* @param domains
* @return
*/
List getMultiConditionsList(String[] domains);
/**
* 根据多个字段进行查询,每个字段可能有多个值,所以参数是Map类型
* @param conditions
* @return
*/
List getMapConditionsList(Map conditions);
}
上面接口中定义的操作,一个比一个复杂,我们通过这一系列操作来说明在Mybatis中如果使用各种查询功能。
这个文件TrafficInfoMapper.xml对应了上面的org.shirdrn.mybatis.mapper.TrafficInfoMapper中定义的操作,通过XML的方式将对应的SQL查询构造出来,这个是Mybatis的核心功能。该文件的内容示例如下所示:
如果你之前用过ibatis,应该很熟悉上面这个配置文件。上面:
namespace指定该SQL映射配置文件的Mapper接口类,其中定义了基本的SQL查询操作(以我们给出的例子为例);
resultMap中的type的值这里是一个别名,当然也可以使用对应的具体类全名(包名+类名),我们会在Mybatis的总的映射配置文件中进行配置,详见后面说明;
select是查询SQL的配置,可以通过不同的元素进行动态构造,如if、foreach等;
该文件可以指定数据库连接池配置、别名配置、SQL映射配置文件组等内容,这里示例的配置内容如下所示:
Spring配置文件applicationContext.xml
classpath*:/proxool.properties
简单说明一下:
dataSource使用的Proxool连接池组件;
sqlSessionFactory是Mybatis的SessionFactory,注入了前面获取到的dataSource,同时指定了Mybatis的总的映射配置文件classpath:sqlMapConfig.xml,属性名为configLocation;
trafficInfoMapper直接由Spring的org.mybatis.spring.mapper.MapperFactoryBean进行代理,需要注入属性mapperInterface(即我们定义的SQL Mapper操作的接口类)和sqlSessionFactory(前面的SessionFactory实例);
trafficInfoService是我们最终在其中进行调用的服务类,注入了我们定义的SQL Mapper接口类的实例trafficInfoMapper。
为简单起见,我们就不定义服务接口了,直接在该类中实现,调用SQL Mapper中预定义的SQL查询操作,实现代码如下所示:
package org.shirdrn.mybatis;
import java.util.List;
import java.util.Map;
import org.shirdrn.mybatis.mapper.TrafficInfoMapper;
public class TrafficInfoService {
private TrafficInfoMapper trafficInfoMapper;
public void setTrafficInfoMapper(TrafficInfoMapper trafficInfoMapper) {
this.trafficInfoMapper = trafficInfoMapper;
}
public TrafficInfo getTrafficInfo(int id) {
return trafficInfoMapper.getTrafficInfo(id);
}
public List getTrafficInfoList(String domain) {
return trafficInfoMapper.getTrafficInfoList(domain);
}
public List getMultiConditionsList(String[] domains) {
return trafficInfoMapper.getMultiConditionsList(domains);
}
List getMapConditionsList(Map conditions) {
return trafficInfoMapper.getMapConditionsList(conditions);
}
}
按照上面的配置,我们就能够实现从单个字段的查询,到多个字段的组合复杂查询。可以通过与实际编写代码来控制这些逻辑相比较,使用Mybatis可能配置上相对复杂一些,但是或得到的好处是非常多的,如代码可维护性好,看起来配置比较直观,出错的几率会大大减小。实际上,如果熟练的这种配置方式,就会在实际开发过程中,更好地去处理更加复杂的统计查询条件的组合逻辑。
测试用例
测试用例可以检测我们上面的配置是否生效,实现代码:
package org.shirdrn.mybatis;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:/applicationContext*.xml" })
public class TestTrafficInfoService {
@Autowired
private TrafficInfoService trafficInfoService;
@Test
public void getTraffic() {
int id = 1196;
TrafficInfo result = trafficInfoService.getTrafficInfo(id);
System.out.println(result);
}
@Test
public void getTrafficList() {
String domain = "make-the-cut.com";
List results = trafficInfoService.getTrafficInfoList(domain);
System.out.println(results);
}
@Test
public void getMultiConditionsList() {
String[] domains = new String[] {
"make.tv", " make-the-cut.com", "makgrills.com", "makino.com"
};
List results = trafficInfoService.getMultiConditionsList(domains);
System.out.println(results);
}
@Test
public void getMapConditionsList() {
String[] domains = new String[] {
"make.tv", " make-the-cut.com", "makgrills.com", "makino.com"
};
List months = Arrays.asList(new String[] {
"201203", "201204", "201205"
});
Map conditions = new HashMap(2);
conditions.put("domains", domains);
conditions.put("months", months);
List results = trafficInfoService.getMapConditionsList(conditions);
System.out.println(results);
}
}
查询进阶
这里,给出一个实际的例子,是对每日报表的一个统计实例,为简单起见,只拿出2张表做LEFT JOIN连接。这个需求,要求查询时可以对每个维度取过得查询条件值,如对于维度osName,值可以使包含Android、IOS,对于另一个维度statDate,可以取最近2天(昨天和前天),等等,并且,这些组合条件可有可无。
对应的Mybatis映射配置文件,内容如下所示:
SELECT
d.id AS id,
d.primary_category_id AS primary_category_id,
d.primary_category_name AS primary_category_name,
d.secondary_category_id AS secondary_category_id,
d.secondary_category_name AS secondary_category_name,
d.cooperation_mode AS cooperation_mode,
d.merchant_id AS merchant_id,
d.osName AS osName,
d.channel_id AS channel_id,
(CASE WHEN d.channel_name IS NOT NULL THEN d.channel_name ELSE d.channel_id END) AS channel_name,
d.version AS version,
d.stat_date AS stat_date,
d.new_user_open AS new_user_open,
d.new_user_play AS new_user_play,
d.active_user_open AS active_user_open,
d.active_user_play AS active_user_play,
d.old_user_open AS old_user_open,
d.old_user_play AS old_user_play,
d.average_time AS average_time,
d.new_user_average_time AS new_user_average_time,
d.old_user_average_time AS old_user_average_time,
d.new_user_open_2retention AS new_user_open_2retention,
d.new_user_open_3retention AS new_user_open_3retention,
d.new_user_open_7retention AS new_user_open_7retention,
d.new_user_open_15retention AS new_user_open_15retention,
d.new_user_open_30retention AS new_user_open_30retention,
d.uninstall_cnt AS uninstall_cnt,
m.merchant_name AS merchant_name
FROM daily_app_user d
LEFT JOIN merchant m ON d.merchant_id=m.id
WHERE d.stat_date = #{statDate}
AND d.osName IN
#{osName}
AND
(d.channel_name LIKE CONCAT('%', CONCAT(#{channelName}, '%')))
AND d.version IN
#{version}
AND
(m.merchant_name LIKE CONCAT('%', CONCAT(#{%merchantName%}, '%')))
AND d.primary_category_id IN
#{primaryCategory}
AND d.secondary_category_id IN
#{secondaryCategory}
AND d.cooperation_model IN
#{cooperationMode}
上述映射配置对应的Mapper定义,接口如下所示:
package org.shirdrn.data.mappers;
import java.util.List;
import java.util.Map;
import org.shirdrn.data.beans.DailyAppUser;
public class DailyAppUserMapper {
List getDailyAppUserListByPage(Map conditions);
List getDailyAppUserListForReport(Map conditions);
}
需要说明的是,如果多个表,一定要设置好Mapper映射配置中每个select元素的resultMap属性,属性值就是前部分的resultMap定义的id。如果只从单个表查询数据,完全可以使用resultType,对应resultMap元素中配置的type属性所指定的别名。
实际上,我们需要通过Map来传递参数,也就是把查询的条件值都收集起来,然后放到Map中,示例如下:
Map conditions = new HashMap();
if(osNames != null) {
conditions.put(DailyAppUserMapper.KEY_OS_NAMES, osNames);
}
if(channelNames != null) {
conditions.put(DailyAppUserMapper.KEY_CHANNEL_NAMES, channelNames);
}
if(versions != null) {
conditions.put(DailyAppUserMapper.KEY_VERSIONS, versions);
}
if(merchantNames != null) {
conditions.put(DailyAppUserMapper.KEY_MERCHANT_NAMES, merchantNames);
}
if(primaryCategories != null) {
conditions.put(DailyAppUserMapper.KEY_PRIMARY_CATEGORIES, primaryCategories);
}
if(secondaryCategories != null) {
conditions.put(DailyAppUserMapper.KEY_SECONDARY_CATEGORIES, secondaryCategories);
}
if(cooperationModes != null) {
conditions.put(ChannelDayMapper.KEY_COOPERATION_MODES, cooperationModes);
}