学校里的专业课学习Mybatis已经三周了,其中学起来最繁杂的就是映射文件.首先要感谢Anna老师的指导,她的教学非常认真负责,内容也很充实.但由于映射的内容多且杂,内容枯燥无聊等原因(茴字有几种写法的感觉),最近已经学麻了...今天趁空闲时间,我决定对映射的方式进行整理总结.
目录
项目基本结构介绍
要想学习框架技术,项目架构是不能回避的一个基础.如果不理清框架的结构,那么任何学习都是混乱的.所以首先要介绍笔者在学习时的项目基本架构.笔者对Mybatis的学习主要是依靠eclipse Maven项目完成.由于mybatis学习只涉及数据库,所以项目的架构也基本只需要停留在数据库层.下图可以清晰地展示该项目结构.当然,这个架构只能代表笔者和笔者老师的习惯,并不是唯一的标准结构。
在java文件夹中,存放的是所有的java文件,其中包括:
1.entity 实体类文件夹.
2.mapper 映射文件夹.这里会定义mybatis需要实现的数据库操作方法的接口,但是并不实现.相当于jsp+servlet项目结构中的dao层,但当我们采用Mybatis+spring+spring Boot架构后,dao这个称呼可以被mapping替代了.
3.main测试类文件夹.一般在这里运行mapper中定义的方法,对数据库进行增删改查操作,从而验证业务目标是否实现.
4.util工具包类.主要是MybatisUtil.
在resources文件夹中,存放的是非java文件,主要是xml文件,其中包括:
1.config 配置文件 主要是mybatis-config.xml和数据库property配置文件mybatis-config.xml文件,其中mybatis-config文件的内容也比较丰富,本文不做赘述.property是指一个.properties文件,用来指定jdbc.driver,jdbc.url,jdbc.username,jdbc.password,也就是数据库的属性。这种配置方式可以避免在数据库环境发生改变时在源码层面修改项目.笔者采用的是My SQL数据库.
2.mapper 映射文件 对java/com.qdu.mapper文件中定义的接口进行实现,实现sql语句和java对象的属性映射,并返回结果集,如何顺利地得到数据库的结果集,是我们今天研究的内容.值得注意的是,在mybatis-config.xml配置文件中,需要将这里的配置文件添加到<mappers>标签中,例如我刚刚写完StudentMapper.xml,那么在mybatis-config.xml中也需要添加以下代码:
<mappers>
<mapper resource="com/qdu/mapper/StudentMapper.xml"/>
</mappers>
此外,<mappers>标签在mubatis-config.xml文件中的位置不是随意的,具体标签排序方式属于mybatis配置文件的知识点,今天不做赘述.
log4j2.xml 日志配置文件,log4j2是一款很实用的日志实现框架,今天也不做赘述.(越写越觉得自己需要填的坑好多...)
一个参数的简单查询
在实现一个与数据库交互的方法时,需要以下三个步骤:
1.在src/main.java.mapper中定义你需要的方法。
2.在main.resources.com.qdu.mapper.xml中利用sql语句实现这个方法。(注意,如果此时mapper.xml刚刚创建,不要忘记在mybatis-config.xml中添加<mapping>标签。)
3.在你需要的地方应用这个方法。包括但不限于测试类和spring框架中。在这里笔者会在src/main.java.main中应用方法。
以下通过一个简单的例子来展示这个过程。
举例:在一个学生表中,通过学生的id来查询单个学生。
为了实现这个方法,我们定义了StudentMapper.java和StudentMapper.xml
1.在src/main.java.mapper中定义你需要的方法。
//1.1 根据学号查询单个学生
Student getOneById(String id);
一个简单的方法,传入参数为String类型的学生id,得到Student对象。
2.在main.resources.com.qdu.mapper.xml中实现这个方法。
<select id="getOneById" resultType="com.qdu.entity.Student" parameterType="string">
select * from student where sid = #{id}
</select>
利用sql语句实现了这个查询。
<select>标签用于映射Mapper接口的一个查询操作,它可以有以下参数:
id:指定一个名称,用于唯一标识每一个操作,必须与mapping接口中的方法名相同。
resultType:指定查询结果封装成什么类型,可以是类名,也可以是别名
parameterType:传入这条语句的参数类型,一般可以省略,因为系统可以根据传入参数自动确定参数类型
statementType:执行sql语句的语句类型。这个在学习jdbc时也有涉猎,它有三个类型
1.STATEMENT:使用Statement接口执行sql语句,如果只执行一次,建议使用
2.PREPARED:使用PreparedStatement执行,因为需要预编译,在需要重复执行时适合使用,PREPARED是默认值
3.CALLABLE:调用存储过程使用该类型
此外,参数传递到sql语句中可以有多种方式,最常用的是#{参数名}和${参数名}两种方式,它们有一定区别。从安全性角度讲,#{}可以防止SQL注入,但${}不能。从原理角度讲,#{}在sql语句执行时会被替换为?,然后调用PreparedStatement中的Set方法来赋值,而${}会被直接替换为变量的值。这决定了两者的用法有一定区别。例如,同上一个例题,使用${}来传入参数的话,写法如下:
<select id="getOneById" resultType="com.qdu.entity.Student" parameterType="string">
select * from student where sid = '${id}'
</select>
尤其是在涉及字符串操作的查询中,需要格外注意二者的区别。例如需要模糊查询包含keyword的语句:
select ... from ... where ... like
两者的等价写法如下:
select ... from ... where ... like concat('%',#{keyword},'%') //使用#{}描述
select ... from ... where ... like '%${keyword}%' //使用${}描述
3.在你需要的地方应用这个方法。
public class Test01_1_根据学号查询单个学生 {
public static void main(String[] args) {
String sid = "2019204385";
Student s = null;
try (SqlSession session = MybatisUtil.openSession()) {
s = session.selectOne("com.qdu.mapper.StudentMapper.getOneById", sid);
}
System.out.println("-------------------------------------------------------------------------------------------");
System.out.println("学号\t\t姓名\t\t密码\t\t性别\t班级");
System.out.println("-------------------------------------------------------------------------------------------");
System.out.println(s);
}
}
在src/main.java.main中,定义了测试类实现了这个方法。注意其中开启会话的方式,这在mybatis中是必要的。session.selectOne()方法用于返回一个结果,在这里,返回的Student对象被直接赋值给了s。
多个参数的查询
当方法存在多个参数时,可以在Mapping接口中使用注解@Param("参数别名")来标记多个参数,进而在Mapping.xml中直接使用参数名.
举例:根据姓名和id查询单个学生
1.在src/main.java.mapper中定义你需要的方法。
//1.1 根据学号和姓名查询单个学生
Student getOneByIdAndName(@Param("id") String id, @Param("name") String name)
使用@Param注解为多个参数确定了别名,便于在mapper.xml中调用。当然,这并不是必须的,因为系统也会自动为参数命名为“param1”,"param2"等,但我想还是自己定义的参数名比较实用。
2.在main.resources.com.qdu.mapper.xml中实现这个方法。
利用刚刚定义的param,实现mapping接口
<select id="getOneById" resultType="com.qdu.entity.Student" parameterType="string">
select * from student where sid = '${id}' and sname='${name}'
</select>
3.在你需要的地方应用这个方法。
public class Test01_1_根据学号查询单个学生 {
public static void main(String[] args) {
String sid = "2019204385";
String sname="张三";
Student s = null;
try (SqlSession session = MybatisUtil.openSession()) {
StudentMapper mapper=session.getMapper(StudentMapper.class);
s=mapper.getOneByIdAndName(sid,sname);
}
System.out.println("-------------------------------------------------------------------------------------------");
System.out.println("学号\t\t姓名\t\t密码\t\t性别\t班级");
System.out.println("-------------------------------------------------------------------------------------------");
System.out.println(s);
}
}
注意其中多个参数传入的方法,不再使用selectone
查询多个结果并且使用Map集合
当我们的查询需要返回一个对象列表List时,可以使用List类型存储结果。
举例:根据班级名称和性别查询学生列表:
1.在src/main.java.mapper中定义你需要的方法。
List<Student> getStudentListByBatchAndGender2(Map<String,Object> map);
使用Map传入参数,map的key是参数的名称,value是参数的值
2.在main.resources.com.qdu.mapper.xml中实现这个方法。
<select id="getStudentListByBatchAndGender2" resultType="com.qdu.entity.Student">
select * from student where sgender=#{xingbie} and sbatch=#{banji}
</select>
3.在你需要的地方应用这个方法。
public class Test08_根据班级和性别查询学生列表2 {
public static void main(String[] args) {
List<Student> list = null;
//sql语句需要的多个参数可以封装成一个Map对象
Map<String,Object> map=new HashMap<>();
//向Map集合中添加数据项,每个数据项由一个键和一个值构成
//键就是sql语句需要的参数的参数名称
//值就是sql语句需要的参数的参数值
map.put("xingbie", "女");
map.put("banji", "19软件J01");
try (SqlSession session = MybatisUtil.openSession()) {
//1) 如果使用selectList()等方法,将包含sql语句需要的参数的Map对象作为第二个参数传入即可
list=session.selectList("getStudentListByBatchAndGender2",map);
//2)如果使用getMapper()方式执行sql语句,则需要对应的Mapper接口的方法的参数类型设置为Map类型
//然后调用的时候将包含sql语句需要的参数的Map对象作为参数传入即可
//list=session.getMapper(StudentMapper.class).getStudentListByBatchAndGender2(map);
}
System.out.println("-------------------------------------------------------------------------------------------");
System.out.println("学号\t\t姓名\t\t密码\t\t性别\t班级");
System.out.println("-------------------------------------------------------------------------------------------");
for (Student s : list) {
System.out.println(s);
}
}
}
使用ResultMap结果映射
在上述例子中,我们从来没有关注过数据库表单与Java对象的属性映射。这是因为在数据库列名与Java对象属性名完全相同时,系统会自动进行映射。当数据库列明与Java对象属性名存在区别时,我们需要通过ResultMap进行手动结果映射,来保证功能的正常实现。这些主要发生在Mapper.xml中
例如,对于product表单和Product对象,二者的属性命名格式有所区别。在Product对象中,属性的命名是name,price,unit,stock,image,而在product表单中,分别对应的是productName,productPrice,productUnit,productStock,productImage,此时就只能手动映射了。
以下是在Mapping.xml中对映射的实现。
<resultMap id="productMap" type="com.qdu.entity.Product">
<!--id标记用于映射主键列和键属性,对于同名的属性,可不显式地写映射,会自动映射-->
<id property="productId" column="productId" />
<!--result标记用于映射非主键列和非键属性-->
<result property="name" column="productName" />
<result property="price" column="productPrice" />
<result property="unit" column="productUnit" />
<result property="stock" column="productStock" />
<result property="image" column="productImg" />
</resultMap>
<resultMap>标签用于实现映射,上面就是它的用法。每个resultMap都有一个标识符,也就是id,根据此id,在下面的mapping接口实现方法中,声明采用结果映射,就完成了它的使用过程。
以下是在Mapping.xml中对映射的调用。
<select id="getOne1" resultMap="productMap" parameterType="string">
select * from product where productId = #{productId}
</select>
增删改操作
增删改操作的映射方式等处理与查询相同。值得注意的是当你使用会话完成增删改操作后,记得提交修改,否则修改将不会被保存在数据库。
例如,更新学生信息的操作
public class Test02_更新学生信息 {
public static void main(String[] args) {
Student s = new Student("2019201111", "小明2", "777777", "F", "18软件J07");
try (SqlSession session = MybatisUtil.openSession()) {
session.getMapper(StudentMapper.class).update(s);
session.commit();
System.out.println("修改了学生信息!!!");
}
}
}
不要忘记其中的session.commit操作
举例:在表单中插入新条目时,如何实现自增Id?
在Mapping.xml中,实现这个操作:
<insert id="insert1" parameterType="category" useGeneratedKeys="true" keyProperty="cid“ keyColumn="cid">
insert into category values(#{cid},#{cname},#{cdescription})
</insert>
useGeneratedKeys属性:规定了使用自增主键,采用以后,插入的对象id列可以为空
keyProperty属性:规定主键列
举例:如何自定义一个id生成方法?
<insert id="insert2" parameterType="category">
<selectKey resultType="int" keyProperty="cid" order="BEFORE">
select ceiling(rand()*1000)
</selectKey>
insert into category values(#{cid},#{cname},#{cdescription})
</insert>
可以使用<selectKey>标签赋值参数。
order属性:规定selectKey执行的时间,BEFORE时是在增删改操作之前,AFTER是之后
联表查询:一对一
实现一对一关系映射的四种方式
1.使用resultMap元素映射关联表属性
2.使用列别名实现一对一关系映射
3.使用association标记实现嵌套结果映射
4.使用association标记实现嵌套查询映射
例如,在一个模型中,Customer与BankCard一一对应,二者通过CardId联系
1.使用resultMap元素映射关联表属性
可以使用Customer对象封装结果集,并在Customer对象中使用BankCard对象。
当然,这样做需要一个前提,就是对Customer实体类进行修改。也就是在Customer实体类中直接添加一个BankCard属性,在联表查询后就把BankCard的属性添加到这个Customer.BankCard中。以下部分是对Customer.java实体类的内容添加。
private BankCard bankCard;
public BankCard getBankCard() {
return bankCard;
}
public void setBankCard(BankCard bankCard) {
this.bankCard = bankCard;
}
在完成上面的操作后,就可以通过结果集封装联表查询的结果了。
<resultMap id="custMap1" type="customer">
<id property="custId" column="custId" />
<result property="custName" column="custName" />
<result property="custGender" column="custGender" />
<result property="custDob" column="custDob" />
<result property="custGrade" column="custGrade" />
<result property="cardId" column="cardId" />
<!-- 如果映射了关系,希望将客户和银行卡信息都封装到一个Customer对象中 -->
<!-- 那么需要将银行卡(关联表)信息封装到bankCard属性中 -->
<!-- 这里说明cardId列的值存入bankCard属性的cardId属性 -->
<!-- cardBalance列的值存入bankCard属性的cardBalance列 -->
<result property="bankCard.cardId" column="cardId" />
<result property="bankCard.cardBalance" column="cardBalance" />
</resultMap>
确定上面的封装方式后,在Mapper.xml中直接进行封装即可。
<select id="query4" resultMap="custMap1">
select
a.*,
b.cardBalance
from customer a, bankcard b
where a.cardId=b.cardId and a.custId=#{id}
</select>
在测试类使用该方法时,注意BankCard调用的方式
public class Test04_联接表查询_单个客户和其银行卡信息2_Customer {
public static void main(String[] args) {
String custId = "U0001";
Customer customer = null;
try (SqlSession session = MybatisUtil.openSession()) {
customer = session.selectOne("query4", custId);
}
System.out.println("--------------------------------------------------------客户信息--------------------------------------------------------");
System.out.println("客户编号:" + customer.getCustId());
System.out.println("客户姓名:" + customer.getCustName());
System.out.println("客户性别:" + customer.getCustGender());
System.out.println("出生日期:" + customer.getCustDob());
System.out.println("客户等级:" + customer.getCustGrade());
System.out.println("银行卡号:" + customer.getCardId());
System.out.println("--------------------------------------------------------持卡信息--------------------------------------------------------");
System.out.println("卡号:"+customer.getBankCard().getCardId());
System.out.println("余额:"+customer.getBankCard().getCardBalance());
System.out.println("----------------------------------------------------------------------------------------------------------------------");
}
}
2.使用列别名实现一对一关系映射
这种方式同样需要用到上一个方法中对实体类Customer的重构。它的思路比较简单,就是将查询得到的列名起一个与实体类中属性相同的名字,直接实现映射。例如同一个题目,我们可以这样实现。
<select id="query5" resultType="customer">
select
a.*,
b.cardId as 'bankCard.cardId',
b.cardBalance as 'bankCard.cardBalance'
from customer a, bankcard b
where a.cardId=b.cardId and a.custId=#{id}
</select>
直接将结果集映射成了一个customer对象,节省了对结果集映射的定义,但是增加了SQL语句的复杂程度。
3.使用association标记实现嵌套结果映射
也就是实现结果集与结果集的嵌套。例如,下面的custMap2结果集通过与bankCard结果集的association,实现了联表的效果。
<resultMap id="custMap2" type="customer">
<id property="custId" column="custId" />
<result property="custName" column="custName" />
<result property="custGender" column="custGender" />
<result property="custDob" column="custDob" />
<result property="custGrade" column="custGrade" />
<result property="cardId" column="cardId" />
<!-- 嵌套结果映射,就是使用association元素将关联表的映射做成一个嵌套的结果映射 -->
<!-- property指定映射关系的属性的名称 -->
<!-- javaType指定映射关系的属性的数据类型(全限定名或别名),可省略该属性 -->
<!--
<association property="bankCard" javaType="com.qdu.entity.BankCard">
<id property="cardId" column="cardId" />
<result property="cardBalance" column="cardBalance" />
</association>
-->
<!-- 如果存在已有的结果映射可以使用,使用resultMap属性指定结果映射的id即可 -->
<!-- 但是如果结果映射来自另一个映射文件,则必须使用"名称空间.id"才可 -->
<association property="bankCard"
resultMap="com.qdu.mapper.BankCardMapper.bankCardMap" />
</resultMap>
其sql语句实现如下。
<select id="query8" resultMap="custMap2">
select a.*,b.cardId,b.cardBalance from customer a,bankcard b where a.cardId=b.cardId
</select>
其使用方式与第2个相同。
4.使用association标记实现嵌套查询映射
这种方式是指定一个查询,来查询关联表的数据
<resultMap id="custMap3" type="customer">
<id property="custId" column="custId" />
<result property="custName" column="custName" />
<result property="custGender" column="custGender" />
<result property="custDob" column="custDob" />
<result property="custGrade" column="custGrade" />
<result property="cardId" column="cardId" />
<!-- 嵌套查询映射是指定一个查询,来查询关联表的数据,指定查询的完整名称
也就是"名称空间的名称.id"
column指定外键列的列名,也就是体现关系的列的名称
fetchType指定如何加载关联表数据
eager: 立刻加载,查询customer表数据时候会立刻查询关联表bankcard的数据
lazy: 延迟加载,对于关联表bankcard的数据,用到才会查询,才会加载数据
只有嵌套查询映射才能使用延迟加载
-->
<association property="bankCard" column="cardId" fetchType="lazy"
select="com.qdu.mapper.BankCardMapper.query2" />
</resultMap>
其中的com.qdu.mapper.BankCardMapper.query2是通过CardId查询BankCard的方法。
这样SQL语句的书写可以得到简化。
<select id="query9" resultMap="custMap3">
<!-- 如果使用的嵌套结果映射,则必须查询多个表所有需要的列 -->
<!-- 如果使用的是嵌套查询映射,则只需要查询一个表即可,另一个表会执行嵌套查询 -->
select * from customer where custId=#{id}
</select>
联表查询:一对多
映射一对多关系的两种方式
1.使用collection标记实现嵌套结果映射
2.使用collection标记实现嵌套查询映射
例如,在一个模型中,一个department对应多个employee
与一对一相同,一对多也需要对实体类进行一些重构。对于这个例子而言,在Department实体类中,需要添加一个List列表来存取该Department的Employee.下面为Department实体类中,重构添加的内容。
private List<Employee> employeeList;
public List<Employee> getEmployeeList() {
return employeeList;
}
public void setEmployeeList(List<Employee> employeeList) {
this.employeeList = employeeList;
}
在完成这一步后,我们对其进行映射。
1.使用collection标记实现嵌套结果映射
通过以下方法实现了对结果集的映射。
<resultMap id="deptMap2" type="department">
<id property="deptId" column="deptId" />
<result property="deptName" column="deptName" />
<!-- collection用来指定映射关系的属性名称 -->
<!-- * ofType:指定列表中数据项的数据类型 -->
<collection property="employeeList" javaType="list" ofType="employee">
<id property="empId" column="empId"/>
<result property="empName" column="empName"/>
<result property="empGender" column="empGender" />
<result property="deptId" column="deptId" />
</collection>
</resultMap>
然后通过正常的查询就可以得到结果集。
<select id="query7" resultMap="deptMap2">
<!--嵌套结果映射需要查询多个表,获取所有需要的列-->
select * from department a,employee b where a.deptId=b.deptId
and a.deptId=#{id}
</select>
2.使用collection标记实现嵌套查询映射
与association类似,collection也可以实现嵌套查询的映射,思路也比较相似。
<resultMap id="deptMap3" type="department">
<id property="deptId" column="deptId" />
<result property="deptName" column="deptName" />
<collection property="employeeList"
javaType="list"
ofType="employee"
column="deptId"
fetchType="lazy"
select="com.qdu.mapper.EmployeeMapper.query5" />
</resultMap>
同样地,这样可以省去部分SQL代码。
<select id="query8" resultMap="deptMap3">
<!--嵌套查询映射只查询department表即可-->
select * from department where deptId=#{id}
</select>
总结一下联表查询的映射。association实现的是1对1的映射,collection实现的是一对多的映射。二者的思路也比较类似,都是需要先将实体类进行重构,使其可以存下联表查询的结果,在此基础上才能进行查询。二者都可以映射结果集或者查询,两种方式各有优劣,映射结果集需要将SQL语句写完整,映射查询不需要。至于怎么使用,还是看个人喜好吧。
写着写着发现已经一下午过去了,内容确实非常多。如果有纰漏敬请指正。再次感谢Anna老师,本文的例题与代码都是Anna老师的课上资料。