简介
在之前的文章里已经讨论过mybatis的基本配置和应用,但是在实际应用中,我们需要支持更加复杂的操作,比如对多个表之间数据的连接访问等。这里就牵涉到数据关系建模里的各种关系映射。比如一对一映射,一对多映射等。这里对这几种情况的实现做一个讨论。
数据库表结构定义
在讨论具体的实现代码之前,我们先定义一系列的数据库表。它们有的是一对一的关系,有的是一对多的关系。这些表格的详细定义如下:
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;
关于表的详细定义和数据部分可以参考附件里的内容。假定所有表格和数据都构建好之后,我们该怎么去访问它们呢?下面就针对常用的两种映射方式进行讨论。
一对一映射
在前面数据库的定义中,我们看到有表格students, addresses。假设我们有定义两个实体对象Student和Address。它们的定义如下:
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
private Integer studId;
private String name;
private String email;
private PhoneNumber phone;
private Address address;
// get, set methods omitted
@Override
public String toString() {
return "Student [studId=" + studId + ", name=" + name + ", email=" + email
+ ", phone=" + (phone==null?null:phone.getAsString()) + ", address=" + address + "]";
}
}
public class Address implements Serializable {
private static final long serialVersionUID = 1L;
private Integer addrId;
private String street;
private String city;
private String state;
private String zip;
private String country;
// get set methods omitted...
@Override
public String toString() {
return "Address [addrId=" + addrId + ", street=" + street + ", city=" + city
+ ", state=" + state + ", zip=" + zip + ", country=" + country
+ "]";
}
}
很显然,从代码里可以看到,Student和Address类是一对一的关系。从实现的角度来说,我们需要在对应的maper xml文件里定义它们的映射关系。常用的几种方式如下:
继承ResultMap
这种方式就是定义一个继承自Student类型的ResultMap,然后将对应需要包含的address信息给映射进去。原有Student的映射定义如下:
继承Student的定义如下:
这个定义里相当于把address的对应属性和表里的字段映射给单独定义在一个地方映射起来。如果我们在后续定义如下的查找语句:
对应的mapper接口定义如下:
public interface StudentMapper {
Student selectStudentWithAddress(int id);
}
我们也在对应的StudentService里定义相关的方法如下:
public Student findStudentWithAddressById(int id) {
SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
try {
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
return studentMapper.selectStudentWithAddress(id);
} finally {
sqlSession.close();
}
}
此时,如果我们执行如下的代码:
public static void main( String[] args ) {
StudentService studService = new StudentService();
Student student1 = studService.findStudentWithAddressById(1);
System.out.println(student1);
}
将看到如下的输出结果:
Student [studId=1, name=Timothy, [email protected], phone=123-123-1234, address=Address [addrId=3, street=710 N Cable Rd, city=Lima, state=OH, zip=45825, country=Allen]]
很显然,这里将address里的信息都读出来了。
针对这种方式,我们如果没有实现定义单独的Address的映射关系的话,还是一个比较合理的选择。但是在一些情况下,我们已经定义了Address的映射ResultMap,如果再在这里重新定义一遍,就显得比较冗余了。那么有没有别的办法呢?
association直接关联
如果我们已经定义好了Address的映射,假设它的定义如下:
那么,我们对应的Student ResultMap就需要修改如下:
在这个配置里,有一个比较值得注意的地方。一个是这里定义了一个association的属性,然后将对应的resultMap指向对应的AddressResult。这里定义的AddressResult是定义在另外一个文件以及命名空间里,所以需要引用它的全名。
内嵌select关联
如果我们查看前面Address的映射文件,会发现里面有一些根据id查找address的方法。既然前面可以直接引用它的映射结果,那么这里也可以直接引用它的查找方法。我们只是需要在定义的结果里嵌入这个查找的定义方法就可以了。这种方式的定义如下:
采用这种方式同样需要注意的就是对目标方法的命名空间引用。这里还要设定的一个地方就是在association里指定column字段,它作为嵌入select语句里对应的参数。
ResultMap嵌套
其实,除了上述的继承类型以外,我们也可以采用ResultMap嵌套的方式来实现这种一对一的映射关系。这种设置的定义如下:
采用这种方式和前面直接用association的方式很接近,只不过它将具体的address的映射细节放到这里了。
一对多映射
和一对一的关系比起来,一对多的映射要稍微复杂一点。我们先定义一个一对多的一组对象。
Tutor:
public class Tutor implements Serializable {
private static final long serialVersionUID = 1L;
private Integer tutorId;
private String name;
private String email;
private Address address;
private List courses;
// get, set methods omitted...
@Override
public String toString() {
return "Tutor [tutorId=" + tutorId + ", name=" + name + ", email=" + email
+ ", address=" + address + ", courses=" + courses + "]";
}
}
在Tutor类的定义中,它包含了一个List
嵌入ResultMap
假设Course类的对应ResultMap定义如下:
那么对应的Tutor ResultMap的定义如下:
其实在Tutor的定义里,它关联了两个对象,一个是Address,一个是Course。只是它和Address是一对一的关系,而和Course是一对多的关系。于是在这里关联的方式就有点差别。对于多个course,它需要使用collection属性,并指定对应的CourseResult。
我们可以采用对应的select语句如下:
这里通过两个表连接来返回所有需要的字段。这种方式虽然没有直接在方法名里阐明要包含courses,但是结果里会包含这个结果。它对应的mapper接口和TutorService的实现如下:
TutorMapper:
public interface TutorMapper {
Tutor selectTutorById(int tutorId);
}
TutorService:
public Tutor findTutorById(int tutorId) {
SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
try {
TutorMapper mapper = sqlSession.getMapper(TutorMapper.class);
return mapper.selectTutorById(tutorId);
}
finally {
sqlSession.close();
}
}
嵌入select
和前面描述的嵌入select方法很类似,这里需要将对应courses的选择嵌入进来。它的对应实现如下:
这里要注意的是选择的select的命名空间以及对应的column名。当然,因为有了对应的select之后,对应的select方法可以稍微简单一点:
这种方式和前面那种方式的差别在于它将查找对应course的方法委派给对应course的映射定义里,所以这里值需要考虑它和address的映射就可以了。而前面通过两个连接操作相当于将所有结果都返回来了,就不需要再去利用别的查询。当然,因为是一对多的映射牵涉到返回多个结果,这种方式可能需要执行多次查询,有可能导致性能的问题。
总结
在mybatis里针对各种映射关系的查询还是有很多小细节值得注意的。像一对一关系里,我们可以定义的类型继承,类型嵌套或者查询嵌套。我们可以根据实际的需要来进行调整。
参考材料
java persistence with mybatis 3