简介
在之前的文章里讨论过mybatis纯xml或者annotation的开发。同时,也讨论了针对不同对象关系情况下的实现细节。在实际的开发应用中,我们会发现,有时候单纯的使用某一种方式来开发的话并不一定有最佳的效率。比如说当我们使用纯xml的时候,会发现里面有很多比较繁琐的配置,而且因为很多sql语句因为是写在xml配置文件里,一个是容易出错,另外对于一些特殊符号还要做一些处理,这样就显得开发的效率不理想。但是使用annotation的话,如果想要重用一些元素比如ResultMap的话就会比较麻烦,每次要重复定义一些元素。所以,如果能够结合两者一些比较好的地方,对于开发来说会更加理想。
示例
SQL脚本
示例对应的SQL脚本如下:
CREATE TABLE ADDRESSES
(
ADDR_ID INT(11) NOT NULL AUTO_INCREMENT,
STREET VARCHAR(50) NOT NULL,
CITY VARCHAR(50) NOT NULL,
STATE VARCHAR(50) NOT NULL,
ZIP VARCHAR(10) DEFAULT NULL,
COUNTRY VARCHAR(50) NOT NULL,
PRIMARY KEY (ADDR_ID)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=LATIN1;
CREATE TABLE STUDENTS
(
STUD_ID INT(11) NOT NULL AUTO_INCREMENT,
NAME VARCHAR(50) NOT NULL,
EMAIL VARCHAR(50) NOT NULL,
PHONE VARCHAR(15) DEFAULT NULL,
DOB DATE DEFAULT NULL,
GENDER VARCHAR(6) DEFAULT NULL,
BIO LONGTEXT DEFAULT NULL,
PIC BLOB DEFAULT NULL,
ADDR_ID INT(11) DEFAULT NULL,
PRIMARY KEY (STUD_ID),
UNIQUE KEY UK_EMAIL (EMAIL),
CONSTRAINT FK_STUDENTS_ADDR FOREIGN KEY (ADDR_ID) REFERENCES ADDRESSES (ADDR_ID)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=LATIN1;
CREATE TABLE TUTORS
(
TUTOR_ID INT(11) NOT NULL AUTO_INCREMENT,
NAME VARCHAR(50) NOT NULL,
EMAIL VARCHAR(50) NOT NULL,
PHONE VARCHAR(15) DEFAULT NULL,
DOB DATE DEFAULT NULL,
GENDER VARCHAR(6) DEFAULT NULL,
BIO LONGTEXT DEFAULT NULL,
PIC BLOB DEFAULT NULL,
ADDR_ID INT(11) DEFAULT NULL,
PRIMARY KEY (TUTOR_ID),
UNIQUE KEY UK_EMAIL (EMAIL),
CONSTRAINT FK_TUTORS_ADDR FOREIGN KEY (ADDR_ID) REFERENCES ADDRESSES (ADDR_ID)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=LATIN1;
CREATE TABLE COURSES
(
COURSE_ID INT(11) NOT NULL AUTO_INCREMENT,
NAME VARCHAR(100) NOT NULL,
DESCRIPTION VARCHAR(512) DEFAULT NULL,
START_DATE DATE DEFAULT NULL,
END_DATE DATE DEFAULT NULL,
TUTOR_ID INT(11) NOT NULL,
PRIMARY KEY (COURSE_ID),
CONSTRAINT FK_COURSE_TUTOR FOREIGN KEY (TUTOR_ID) REFERENCES TUTORS (TUTOR_ID)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=LATIN1;
CREATE TABLE COURSE_ENROLLMENT
(
COURSE_ID INT(11) NOT NULL,
STUD_ID INT(11) NOT NULL,
PRIMARY KEY (COURSE_ID,STUD_ID),
CONSTRAINT FK_ENROLLMENT_STUD FOREIGN KEY (STUD_ID) REFERENCES STUDENTS (STUD_ID),
CONSTRAINT FK_ENROLLMENT_COURSE FOREIGN KEY (COURSE_ID) REFERENCES COURSES (COURSE_ID)
) ENGINE=INNODB DEFAULT CHARSET=LATIN1;
mybatis配置文件
对应的mybatis配置文件如下:
<?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="application.properties"/> <typeAliases> <package name="com.yunzero.domain"/> </typeAliases> <typeHandlers> <typeHandler handler="com.yunzero.typehandlers.PhoneTypeHandler"/> </typeHandlers> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <mappers> <package name="com.yunzero.mappers"/> </mappers> </configuration>
这个文件主要指定数据源和配置环境,另外也指定类型转换和对应的mapper接口。
mapper接口和对应配置文件
我们这里重点考察TutorMapper,它的对应的配置文件如下:
<?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.yunzero.mappers.TutorMapper"> <resultMap type="Course" id="CourseResult"> <id column="course_id" property="courseId"/> <result column="course_name" property="name"/> <result column="description" property="description"/> <result column="start_date" property="startDate"/> <result column="end_date" property="endDate"/> </resultMap> <resultMap type="Tutor" id="TutorResult"> <id column="tutor_id" property="tutorId"/> <result column="tutor_name" property="name"/> <result column="email" property="email"/> <result column="phone" property="phone"/> <association property="address" resultMap="com.yunzero.mappers.AddressMapper.AddressResult"/> <collection property="courses" resultMap="CourseResult"></collection> </resultMap> </mapper>
这里定义了两个映射的结果,一个是CourseResult,一个就是TutorResult。这里也引用了其他地方定义的ResultMap。
对应的TutorMapper接口定义如下:
public interface TutorMapper {
@SelectProvider(type=TutorDynaSqlProvider.class, method="findAllTutorsSql")
List<Tutor> findAllTutors();
@SelectProvider(type=TutorDynaSqlProvider.class, method="findTutorByIdSql")
Tutor findTutorById(int tutorId);
@SelectProvider(type=TutorDynaSqlProvider.class, method="findTutorByNameAndEmailSql")
Tutor findTutorByNameAndEmail(@Param("name")String name, @Param("email")String email);
@InsertProvider(type=TutorDynaSqlProvider.class, method="insertTutor")
@Options(useGeneratedKeys=true, keyProperty="tutorId")
int insertTutor(Tutor tutor);
@UpdateProvider(type=TutorDynaSqlProvider.class, method="updateTutor")
int updateTutor(Tutor tutor);
@DeleteProvider(type=TutorDynaSqlProvider.class, method="deleteTutor")
int deleteTutor(int tutorId);
@SelectProvider(type=TutorDynaSqlProvider.class, method="selectTutorById")
@ResultMap("com.yunzero.mappers.TutorMapper.TutorResult")
Tutor selectTutorById(int tutorId);
}
这里有几个值得注意的地方。和前面简单的定义@Select, @Insert等方法不同,这里我们不是直接在这里写sql脚本。因为一方面除了前面提到的在字符串形式写的sql更加容易出错以外,在某些地方,我们需要结合程序逻辑和一些参数做一些更加复杂的运算。也就是动态sql。这些问题使得直接在xml或者annotation里写sql并不是一个很理想的选择。于是mybatis提供了一种比较好的写sql的方法,叫sqlprovider。它使得写sql脚本的代码显得更加面向对象,也显得好理解一点。
SQL provider
前面示例里使用到的sql provider类详细实现如下:
public class TutorDynaSqlProvider {
public String findAllTutorsSql() {
return new SQL() {{
SELECT("tutor_id as tutorId, name, email");
FROM("tutors");
}}.toString();
}
public String findTutorByIdSql(final int tutorId) {
return new SQL() {{
SELECT("tutor_id as tutorId, name, email");
FROM("TUTORS");
WHERE("tutor_id = #{tutorId}"); // using placeholder #{tutorId}
}}.toString();
}
public String findTutorByNameAndEmailSql(Map<String, Object> map) {
return new SQL() {{
SELECT("tutor_id as tutorId, name, email");
FROM("tutors");
WHERE("name=#{name} AND email=#{email}");
}}.toString();
}
public String insertTutor(final Tutor tutor) {
return new SQL() {{
INSERT_INTO("TUTORS");
if (tutor.getName() != null) {
VALUES("NAME", "#{name}");
}
if (tutor.getEmail() != null) {
VALUES("EMAIL", "#{email}");
}
}}.toString();
}
public String updateTutor(final Tutor tutor) {
return new SQL() {{
UPDATE("TUTORS");
if (tutor.getName() != null) {
SET("NAME = #{name}");
}
if (tutor.getEmail() != null) {
SET("EMAIL = #{email}");
}
WHERE("TUTOR_ID = #{tutorId}");
}}.toString();
}
public String deleteTutor(int tutorId) {
return new SQL() {{
DELETE_FROM("TUTORS");
WHERE("TUTOR_ID = #{tutorId}");
}}.toString();
}
public String selectTutorById() {
return new SQL() {{
SELECT("t.tutor_id, t.name as tutor_name, email");
SELECT("a.addr_id, street, city, state, zip, country");
SELECT("course_id, c.name as course_name, description, start_date, end_date");
FROM("TUTORS t");
LEFT_OUTER_JOIN("ADDRESSES a on t.addr_id=a.addr_id");
LEFT_OUTER_JOIN("COURSES c on t.tutor_id=c.tutor_id");
WHERE("t.TUTOR_ID = #{id}");
}}.toString();
}
}
通过这部分的实现我们可以看到,我们可以将sql脚本,每个操作对应一个方法。然后在annotation里指定对应的类和方法。针对每个不同的实现,我们来看一下它们的细节。
SelectProvider
SelectProvider里有几种情况,针对没有参数和有参数的情况。在没有参数的情况下,比如findAllTutorsSql,这里只需要在Mapper接口里指定对应的sqlprovider类和方法。在有一个参数的时候,比如有这个方法:
@SelectProvider(type=TutorDynaSqlProvider.class, method="findTutorByIdSql")
Tutor findTutorById(int tutorId);
在这里mapper 接口的方法里其实是提供了一个参数的。但是sqlprovider方法里的实现如下:
public String findTutorByIdSql() {
return new SQL() {{
SELECT("tutor_id as tutorId, name, email");
FROM("TUTORS");
WHERE("tutor_id = #{tutorId}"); // using placeholder #{tutorId}
}}.toString();
}
这里不是简单的做一个sql的拼接,注意到这里将mapper接口里的参数和占位符里的参数一一对应上了。当然,这里的findTutorByIdSql方法并没有带参数。
如果需要provider方法里的方法支持mapper接口里多个参数的话,需要做一些调整。比如mapper接口里有方法:
@SelectProvider(type=TutorDynaSqlProvider.class, method="findTutorByNameAndEmailSql")
Tutor findTutorByNameAndEmail(@Param("name")String name, @Param("email")String email);
这里接口指定了两个@Param参数,它就对应sqlprovider提供的参数里需要映射过来的字段。而在这种情况下,sqlProvider对应的方法必须提供一个Map<String, Object>类型的参数。它对应的方法实现如下:
public String findTutorByNameAndEmailSql(Map<String, Object> map) {
return new SQL() {{
SELECT("tutor_id as tutorId, name, email");
FROM("TUTORS");
WHERE("name=#{name} AND email=#{email}");
}}.toString();
}
除了selectprovider,其他的像insertProvider, deleteProvider, updateProvider则相对比较简单直观。它们有一个共同的特点就是所有的sqlprovider方法都必须返回String类型的值。另外,我们也注意到,有的方法里有一些判断的逻辑,比如
public String updateTutor(final Tutor tutor) {
return new SQL() {{
UPDATE("TUTORS");
if (tutor.getName() != null) {
SET("NAME = #{name}");
}
if (tutor.getEmail() != null) {
SET("EMAIL = #{email}");
}
WHERE("TUTOR_ID = #{tutorId}");
}}.toString();
}
这种判断方式如果用纯sql的方式来实现会显得比较麻烦。但是这里用一种类似sql并结合java程序逻辑的方式实现了。它比对应的xml配置方式显得更加灵活。
总结
总的来说,结合xml, annotaion和sqlprovider的时候,需要注意几个点。一个是mapper的xml配置适合定义一些ResultMap,这样可以方便它们被其他的mapper接口重用。另外,annotation里适合指定对应的sqlprovider以及对应resultMap引用。而对于具体sql语句的编写,在sqlprovider里写则比较合适。而且,要特别注意sqlprovider里方法的参数和mapper接口里方法参数的对应。在这一块,目前感觉还不是很灵活。