MyBatis框架
官方文档地址: https://mybatis.org/
什么是框架
框架类似于脚手架,建筑时候帮助你更快的完成建造任务,单纯的框架不能用来当房子用,但是辅助盖楼也是很有必要的,能够节省更多的时间。另一种说法就是框架提供了一系列的模板代码,只需要很少的代码来实现基础功能,最早时候通过JDBC连接数据库,每次都写加载驱动,连接数据库,写SQL语句,执行语句,处理结果集,关闭连接,多麻烦呀,所以框架就提供了一个入口,写上驱动名和相关的参数就能进行SQL操作了。
我还有一层理解,就是框架中集成了大量的代码,通过反射等各种方式,对性能有些影响,但是对于后期维护起来更加方便,所以这个问题能难为到我这种强迫症,毕竟现在都是框架横行的时代,不用也不行啊,所以权衡性能和维护难度吧。【这里是我的疑问,大佬可以帮我解答以下】
软件的三层架构
- 数据持久化存储层:主要和数据库打交道的
- 业务层:主要实现业务功能的
- 表现层:与用户进行交互的,并且能与业务层联系
DAO层与业务层联系,业务层与表现层联系,这样的架构应该是目前主流的规范。
MVC设计思想
最早我对三层构架和MVC模型总是混淆,MVC是一种设计思想,他们确实有相似的地方,Model 是模型,模型拥有多种处理任务,模型可以对多个视图提供数据,可以有效的减少代码量。View是视图,与前端页面关联最大,一般都是由网页或者是客户端界面组成。Controller是控制器,控制器接收用户请求,并且完成对模型的指向,
MyBatis入门
MyBatis是一个持久层框架,提供了与数据库交互的方法,能够减少大量代码,主要写一些SQL语句就可以了,通过XML配置,这样维护起来也更加方便了,与代码混淆在一起就会感到很臃肿。
MyBatis依赖
MyBatis虽然可以操作数据库,但是也需要MySQL的驱动支持,所以在使用MyBatis时,仍要导入MySQL的驱动,以Maven项目为例,依赖如下所示:
<dependencies>
<!--mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.3</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
</dependencies>
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>
<environments default="development"> <!--可以设置多个环境,选择的时development-->
<environment id="development"> <!--development环境-->
<transactionManager type="JDBC"/> <!--事务处理方式为JDBC-->
<dataSource type="POOLED"> <!--通过池连接-->
<property name="driver" value="com.mysql.jdbc.Driver"/> <!--MySQL驱动全限定名-->
<property name="url" value="jdbc:mysql:///db_mybatis"/> <!--数据库链接地址-->
<property name="username" value="root"/><!--用户名-->
<property name="password" value="root"/><!--密码-->
</dataSource>
</environment>
</environments>
<mappers> <!--映射文件配置-->
<!--单一文件-->
<mapper resource="UserMapper.xml"/>
<!--扫描包下的映射文件-->
<package name="com.alc.mapper"/>
<!--当你没有写xml,使用的时实现类,用这个-->
<mapper class="com.alc.DoctorMapper"/>
</mappers>
</configuration>
核心配置文件标签
在资源目录创建一个数据库连接的配置文件:文件名(db.properties)
db.drvierName=com.mysql.jdbc.Driver
db.url=jdbc:mysql:///db
db.username=root
db.password=root
核心配置文件中读取上面的配置文件
<properties resource="db.properties"></properties>
<!--读取上面配置文件-->
<dataSource type="POOLED">
<!--property name值为固定的, value:可以根据需求自定义-->
<!--获取值的方法也和el表达式类似-->
<property name="driver" value="${db.drvierName}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</dataSource>
映射文件
<?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="userMapper"> <!--代表命名空间,目前理解成一个类就行了,后期与接口对接,直接写个接口不用写实现类就能执行SQL语句了-->
<select id="selectUser" resultType="com.alc.entity.User"> <!-- select 选择标签, id为唯一标识,与接口名对应 resultType为返回值类型-->
select * from user
</select>
<!--insert:添加标签;parameterType:参数类型-->
<insert id="addUser" parameterType="com.alc.entity.User"> <!--参数类型为User,也就是User中的变量名,一定要有get方法嗷-->
insert into user values(#{id},#{name},#{age},#{sex},#{birthday})
</insert>
</mapper>
类型的别名
就像上面的resultType写的都是全限定名,每次这么写太累了,所以可以给他们起个别名。在核心配置文件中,添加别名标签和属性。如下所示:
<typeAliases>
<!--type:类的全限定名;alias:别名-->
<typeAlias type="com.alc.entity.User" alias="u"/>
</typeAliases>
这样就可以在映射文件中直接使用u来代表我的User类了。
<select id="selectById" resultType="u" parameterType="java.lang.Integer">
select * from user where id=#{id}
</select>
迫不及待的运行
maven提供了测试类,我们可以在测试类中写代码,看一下是否能够执行呢?[当前没有使用接口方式,只有核心配置文件、映射文件和实体类]
@Test
public void query() throws IOException {
//alt+enter:补全返回值类型
//读取mybatis的核心配置文件
InputStream stream = Resources.getResourceAsStream("mybatisConfig.xml");
//获取SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(stream);
//获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//args:映射文件中的namespace.操作标签的id值
List<User> list = sqlSession.selectList("userMapper.selectUser");
//输出结果
list.forEach(System.out::print);
//关闭连接对象
sqlSession.close();
}
@Test
public void add() throws IOException {
//读取mybatis的核心配置文件
InputStream stream = Resources.getResourceAsStream("mybatisConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(stream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User u=new User();
u.setName("Matrix");
u.setAge(23);
u.setSex("男");
u.setBirthday(LocalDate.now());
//args1:映射文件中的namespace.操作标签的id值;args2:保存的对象
sqlSession.insert("userMapper.addUser",u);
//提交事务
sqlSession.commit();
sqlSession.close();
}
小任务
如果多次操作数据库,读取配置文件,创建SqlSessionFactory代码也挺繁琐的,可不可以提取一个工具类出来呢?
CURD
标签
- SELECT
- UPDATE
- INSERT
- DELETE
也就是标签名为这些,这些标识告诉MyBatis你要执行的操作是什么,从而对结果集进行处理,例如:
<mapper namespace="userMapper">
<!--select:查询标签; id:唯一标识; resultType:返回结果类型-->
<select id="selectUser" resultType="com.alc.entity.User">
/*sql语句*/
select * from user
</select>
<!--insert:添加标签;parameterType:参数类型-->
<insert id="addUser" parameterType="com.alc.entity.User">
insert into user values(#{id},#{name},#{age},#{sex},#{birthday})
</insert>
<!--update:更新-->
<update id="updateUser" parameterType="com.alc.entity.User">
update user set name=#{name},age=#{age},sex=#{sex},birthday=#{birthday} where id=#{id}
</update>
<!--delete:删除-->
<delete id="deleteUser" parameterType="java.lang.Integer">
delete from user where id=#{id}
</delete>
<!--根据主键查询某一条数据-->
<select id="selectById" resultType="com.alc.entity.User" parameterType="java.lang.Integer">
select * from user where id=#{id}
</select>
</mapper>
基于接口开发
基于接口开发,不需要写实现类,直接写xml就可以了,减少了代码量,但是也有他的规范,规范如下。
- 映射文件中的namespace属性值==接口类的全限定名
- 映射文件中CRUD标签中的id属性与接口类方法名一致
- 映射文件中的参数类型和返回值类型要与方法一致
MyBatis-API
- Resources 通过调用此方法可以读取到核心配置文件
- Resources.getResourceAsStream(“myBatisConfig.xml”)
- SqlSessionFactory 通过工厂对象创建连接对象
- SqlSessionFactory.openSession();
- SqlSession 进行语句执行的实例方法
- SqlSession.* * *
理解:核心配置文件相当于资金,拿着钱买了个工厂,工厂的参数就是钱,现在由工厂了就开启流水线,流水线相当于一个产品(SqlSession),然后让产品去做他该做的事。
ResultMap
当实体类和数据库中字段不匹配时,需要添加对应法则,告诉MyBatis那个列对应的时那个变量。
<resultMap id="userInfo" type="u">
<!--result:结果; property:实体类中的属性名;column:查询的字段名-->
<result property="sname" column="name"/>
</resultMap>
<select id="selectById" resultMap="userInfo">
select * from user where id=#{id}
</select>
多参数查询
通常情况下,我们的查询语句都附加了条件,有些可能是多个,比如登录,我们有一个实体类User,其中有两个参数,一个是username,另一个是password, 这样我们可以封装到实体类中进行sql查询,(user.username user.password)这样的方式,不难看出,这样的方式只有一个参数,这个参数是实体对象,但是如果查询参数不具有实体类或者是不想封装,也可以通过多种参数的方式进行查询。
我们开启这个项目,需要导入依赖文件,并且配置MyBatis的核心配置文件,数据库部分就不过多介绍了,官方网站上有中文解释,文章前也有相关的模板。在这里呢,我们在映射标签中需要定义mappers,因为项目中很少有一个映射,所以建议试用package进行扫描,这样就不需要后期的一个一个的添加了,也避免了忘记添加映射而无法正常运行。
<mappers>
<package name="com.alc.mapper"/>
</mappers>
例如: 根据姓名和年龄查询用户信息;这时候,我们分析一下我们需要那些参数和返回值类型。返回值类型是一个List列表,所以接口需要以List进行定义,而参数有多种方法定义。如下所示:
//1.根据性别与年龄查询用户信息
List<User>selectBySexAndAge(String sex,Integer age);
//2.根据性别与年龄查询用户信息
List<User>selectBySexAndAge(User user);
//3.根据性别与年龄查询用户信息
List<User>selectBySexAndAge(Map<String,Object>map);
//4.根据性别与年龄查询用户信息
List<User>selectBySexAndAge(@Param("sex") String sex, @Param("age") Integer age);
本文就以这样的接口定义,这里给出了四种方式,路径为com.alc.mapper.UserMapper,这里只提供了映射接口,但是需要映射文件,也就是xml文件,需要在resources文件中添加相同的路径,(这里于IDEA 的编译方式有关,如果可以编译java文件夹下的非编译文件,就可以放在java文件夹下了,为了规范,我们需要在resources文件夹下定义)。
目录定义如下所示嗷
-java
|-com
|-alc
|-mapper
|-UserMapper.java
|-UserMapper.xml # 如果这样,需要添加编译目录,不符合规范,不建议这么做
…
-resources
|-com
|-alc
|-mapper
|-UserMapper.xml # 注意 目录的定义要与映射java文件的目录一致。。。
然后我们就到了关键的映射定义部分了。
<?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.alc.mapper.UserMapper">
<select id="selectBySexAndAge1" resultType="com.alc.entity.User"> <!-- arg0 表示第一个参数 param1 表示第一个参数 以此类推 -->
SELECT * FROM user
<where>
sex = #{arg0} and age = #{param2}
</where>
</select>
<select id="selectBySexAndAge2" resultType="com.alc.entity.User">
SELECT * FROM user
<where>
sex = #{sex} and age = #{age}
</where>
</select>
<select id="selectBySexAndAge3" resultType="com.alc.entity.User">
SELECT * FROM user
<where>
sex = #{sex} and age = #{age}
</where>
</select>
<select id="selectBySexAndAge4" resultType="com.alc.entity.User">
SELECT * FROM user
<where>
sex = #{sex} and age = #{age}
</where>
</select>
</mapper>
测试类如下所示:建议对上面的代码进行截图,以贴图的方式,进行对比,这样更容易理解这些代码了。
import com.alc.entity.User;
import com.alc.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MyBatisTest {
private static UserMapper mapper = null;
static {
try {
InputStream stream = Resources.getResourceAsStream("mybatisConfig.xml");
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(stream);
SqlSession session = build.openSession();
mapper = session.getMapper(UserMapper.class);
} catch (IOException e) {
e.printStackTrace();
}
} // 整合以下模板代码,测试的时候直接试用就好了。
@Test
public void T1(){
List<User> rst = mapper.selectBySexAndAge1("男", 38);
//直接通过参数索引获取,所以不需要特殊处理。
rst.forEach(System.out::println);
System.out.println("T1");
}
@Test
public void T2(){
User user = new User();
user.setSex("男");user.setAge(38);
//因为封装到实体对象中了,所以需要传递一个对象过去,具体内容存放在在该对象中
List<User> rst = mapper.selectBySexAndAge2(user);
rst.forEach(System.out::println);
System.out.println("T2");
}
@Test
public void T3(){
Map params = new HashMap();
params.put("sex","女");params.put("age",28);
//以map集合的方式存储查询参数,我们都知道,map集合的key是唯一的,所以mybatis会解析map,对其中的参数进行查询
List rst = mapper.selectBySexAndAge3(params);
rst.forEach(System.out::println);
System.out.println("T3");
}
@Test
public void T4(){
List<User> rst = mapper.selectBySexAndAge4("女", 28);
//通过别名去取,也就是接口中的@Param中的值,不妨改变以下试试看。
rst.forEach(System.out::println);
System.out.println("T4");
}
}
------------------------------------------------------- T E S T S ------------------------------------------------------- Running MyBatisTest User(id=1, name=����, age=38, sex=��, birthday=Wed Nov 11 11:12:13 CST 2020) T1 User(id=1, name=����, age=38, sex=��, birthday=Wed Nov 11 11:12:13 CST 2020) T2 User(id=2, name=ij��ʦ, age=28, sex=Ů, birthday=Mon Dec 21 14:32:12 CST 2020) T3 User(id=2, name=ij��ʦ, age=28, sex=Ů, birthday=Mon Dec 21 14:32:12 CST 2020) T4 Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.633 sec Results : Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
已经通过了。
模糊查询
映射xml中需要做调整,我们经过上面的代码不难看出来,mybatis也就是进行字符串的拼接,但是有没有发现,我们对参数的传入都是通过‘#’这个符号进行拼接的,在这里引入一个知识点,’#’ 这个符号是对sql语句进行预编译的,MyBatis将会转义其中的字符,如果使用’$'符号,将会当作字符串来处理,不会进行预编译。
<select id="selectLikeName" resultType="com.alc.entity.User">
select * from user where name like #{name}
</select> <!--这种方式需要在java代码中手动添加'%' 不是很方便,但是可以解决模糊查询的问题-->
<select id="selectLikeName" resultType="com.alc.entity.User">
select * from user where name like "%"#{name}"%"
</select> <!--这种方式看起来很奇怪,但是mysql可以使用,因为没有用过Oracle,不知道Oracle是否支持这种方式-->
<select id="selectLikeName" resultType="com.alc.entity.User">
select * from user where name like '%${name}%'
</select><!--进行sql语句的拼接,存在sql注入问题-->
<select id="selectLikeName" resultType="com.alc.entity.User">
select * from user where name like CONCAT(CONCAT('%',#{name}),'%')
</select><!--这种方式是使用了SQL中的字符串拼接函数,CONCAT(AC,DB)==>"ACBD"-->
Log4j
在SQL执行过程中,我们可以通过日志对执行过程进行查看,这里可以使用log4j来实现这个操作
-
添加依赖
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>2.0.0-alpha1</version> </dependency>
-
log4j.properties 配置
log4j.rootLogger=info,stdout,D ### 输出到控制台 ### log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE}%5p%c{1}:%L-%m%n ### 输出到日志文件 ### log4j.appender.D=org.apache.log4j.DailyRollingFileAppender log4j.appender.D.File=D:/logs/log.log log4j.appender.D.Append=true log4j.appender.D.Threshold=DEBUG log4j.appender.D.layout=org.apache.log4j.PatternLayout log4j.appender.D.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss}[ %t:%r ]-[%p]%m%n
⚠️ 文件名不能变,只能是log4j.properties
-
配置mybatis核心配置文件 (添加这条配置)
<settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings>
获取添加数据信息
想象一个场景,在高并发的情况下,多人操作数据库,我们向用户表中插入一条数据,由于ID是自动生成,我们如果需要这个ID,进行其他表中的关联操作,这就需要在执行完成后对ID进行回取。
以上一个场景为例,我们需要在xml中进行设置,把并且id返回给我们的java程序,代码如下所示。
<!--useGeneratedKeys:true:支持在添加数据的时候,获取注解的值;
keyProperty:将查询到主键值赋值给某一个属性
-->
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user VALUE(NULL,#{name},#{password},#{sex},#{age},#{birthday})
</insert>
通过上面的代码我们可以看到,insert添加了两个属性,并且缺少了parameterType属性,parameterType这个属性一般情况是可以省略不写的,但是为了后期维护的便捷以及见名知意,还是写上比较好,mybatis会自动识别,但是为了规范以及代码的可读性,还是写上比较合理。
看到这里,我们还没有获取到id值,实际上这里只是对于配置的书写,然而获取仍需要在java代码中实现,比如我们要向用户表中添加一条数据。
User u = new User(); //定义一个用户实体对象
u.setName("老崔");
u.setSex("男");
u.setBirthday(new Date());
u.setPassword("123");
u.setAge(34);
//上面我们没有对ID进行赋值。
Integer rst = mapper.insertUser(u); // 调用添加接口映射
System.out.println(rst + "," + u.getId()); //rst为1代表插入成功,id为刚刚添加数据的id值,切记,前提是id字段自动增长
session.commit();//最后不要忘记提交嗷。
根据日志文件,我们分析一下。
2021-03-28T11:22:15.793612Z 11 Connect study@localhost on study using TCP/IP
2021-03-28T11:22:15.798292Z 11 Query /* mysql-connector-java-5.1.49 ( Revision: ad86f36e100e104cd926c6b81c8cab9565750116 ) */SELECT @@session.auto_increment_increment AS auto_increment_increment, @@character_set_client AS character_set_client, @@character_set_connection AS character_set_connection, @@character_set_results AS character_set_results, @@character_set_server AS character_set_server, @@collation_server AS collation_server, @@collation_connection AS collation_connection, @@init_connect AS init_connect, @@interactive_timeout AS interactive_timeout, @@license AS license, @@lower_case_table_names AS lower_case_table_names, @@max_allowed_packet AS max_allowed_packet, @@net_buffer_length AS net_buffer_length, @@net_write_timeout AS net_write_timeout, @@performance_schema AS performance_schema, @@query_cache_size AS query_cache_size, @@query_cache_type AS query_cache_type, @@sql_mode AS sql_mode, @@system_time_zone AS system_time_zone, @@time_zone AS time_zone, @@transaction_isolation AS transaction_isolation, @@wait_timeout AS wait_timeout
2021-03-28T11:22:15.820716Z 11 Query SET character_set_results = NULL
2021-03-28T11:22:15.821021Z 11 Query SET autocommit=1
2021-03-28T11:22:15.825516Z 11 Query SET autocommit=0 # 默认是手动提交(手动事务管理)
2021-03-28T11:22:15.842043Z 11 Query select @@session.transaction_read_only
2021-03-28T11:22:15.842416Z 11 Query INSERT INTO user VALUE(NULL,'老崔','123','男',34,'2021-03-28 19:22:15.57') # 添加
2021-03-28T11:22:15.846378Z 11 Query commit
在上面没有进行执行 SELECT LAST_INSERT_ID(); 这条语句。还有另一种方法,但是代码量稍微大一点,并且我个人认为不太严谨,因此就仅以介绍,不做详解。
<!--
keyProperty:查询到的主键值赋值给哪一个属性名;
keyColumn:查询的主键对应的表中的字段名
resultType:查询的主键的数据类型
order: BEFORE/AFTER在进行添加动作之前还是之后进行查询
-->
<insert id="add" parameterType="u">
<selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
select LAST_INSERT_ID()
</selectKey>
insert into user values(#{id},#{name},#{password},#{age},#{sex},#{birthday})
</insert>
再精简一点
当我们进行查询或者其他操作的时候,还是需要一些模板SQL语句,当然,有些时候参数也是模板代码,所以可以使用sql标签。
<sql id="sel">
SELECT * FROM user
</sql>
但是我们定义完了,如何去使用呢?
<****(select) id="***">
<include refid="sel"/> where ***
</****(select)>
高级映射
表与表的关联关系
- 多对一
- 一对一
- 一对多
- 多对多
多对一和一对多,我的理解是相对的,例如老师与学生之间的关系,一个老师教多个学生,多个学生被一个老师教,一对多,多对一,是参照关系的不同,因此不要对高级映射关系有畏惧之心。
多对一
以学生作为"参照物",一个学生对应多个老师,因此再定义学生类的时候,需要把老师定义在学生中,数据表中,学生中要包含老师的id值,这样就存在了对应关系了。
映射文件的配置(方式一):
学生映射
<resultMap id="stuResult" type="com.alc.entity.Student">
<id property="id" column="sid"/>
<result property="name" column="sname"></result>
<result property="sex" column="ssex"></result>
<result property="age" column="sage"></result>
<!--association:多对一/一对一的配置
javaType:设置当前这个属性的java类型-->
<association property="teacher" javaType="com.alc.entity.Teacher">
<id property="id" column="tid"/>
<result property="name" column="tname"></result>
<result property="sex" column="tsex"></result>
<result property="age" column="tage"></result>
</association>
</resultMap>
<select id="findById" resultMap="stuResult">
SELECT
s.id sid,
s.NAME sname,
s.sex ssex,
s.age sage,
t.id tid,
t.NAME tname,
t.sex tsex,
t.age tage
FROM
student s,
teacher t
WHERE
s.teacher_fk = t.id
AND s.id = #{id}
</select>
方式二:
学生映射
<resultMap id="stuResult" type="com.alc.entity.Student">
<!--多对一配置
association:多对一关联配置
property:实体类中起关联作用的属性名
column:查询到的数据库表中起关联作用的字段名
select:调用在别的mapper中定义的查询: namespace.id
-->
<association property="teacher" column="teacher_fk" select="com.alc.mapper.TeacherMapper.findById"/>
</resultMap>
<select id="findById" resultMap="stuResult">
select * from student where id=#{id}
</select>
老师映射
<mapper namespace="com.alc.mapper.TeacherMapper">
<select id="findById" resultType="com.alc.entity.Teacher">
select * from teacher where id=#{id}
</select>
</mapper>
相比来说,两种方式都可以实现相同的作用,但是明显后者SQL语句比较少,前者但是更能体现出其中的关系。
一对多
根据上面的多对一,我们依然可以通过老师与学生之间的关系产生一对多的模型,一个老师对应多个学生,表结构不需要改变,但是在实体定义的时候,老师中的学生需要通过一个列表的方式存储,也就是说,在老师的实体中,包含其所教的所有学生信息。
这样的方式,是以老师为参照物的。
实现方式一:(老师映射)
<resultMap id="teacherResult" type="com.alc.one2many.Teacher">
<id property="id" column="tid"/>
<result property="name" column="tname"></result>
<result property="sex" column="tsex"></result>
<result property="age" column="tage"></result>
<!--
collection:对多配置
ofType:集合中存放的元素数据类型
-->
<collection property="students" ofType="com.alc.one2many.Student">
<id property="id" column="sid"/>
<result property="name" column="sname"></result>
<result property="sex" column="ssex"></result>
<result property="age" column="sage"></result>
</collection>
</resultMap>
<select id="findById" resultMap="teacherResult">
SELECT
s.id sid,
s.NAME sname,
s.sex ssex,
s.age sage,
t.id tid,
t.NAME tname,
t.sex tsex,
t.age tage
FROM
student s,
teacher t
WHERE
s.teacher_fk = t.id
AND t.id = #{id}
</select>
实现方式二:(老师映射)
<resultMap id="teacherResult" type="com.alc.one2many.Teacher">
<!--select:调用StudentMapper中定义的findByTeacherId方法-->
<collection property="students" column="id" select="com.alc.dao.StudentMapper.findByTeacherId"></collection>
</resultMap>
<select id="findById" resultMap="teacherResult">
select * from teacher where id=#{id}
</select>
学生映射
<mapper namespace="com.alc.dao.StudentMapper">
<select id="findByTeacherId" resultType="com.alc.one2many.Student">
select * from student where teacher_fk=#{id}
</select>
</mapper>
多对多
这层概念有些混乱,但是,简单的说,多对多就是一对多和多对一的叠加版,以商城系统为例,一个订单可以对应多个商品,订单详情中包含订单和商品信息,一个商品可以存在于多个订单中,一个订单也可以包含多个商品。
实际中用的不是很多,其实我很不喜欢这样的复杂查询,我认为DAO层就应该是一些简单的SQL查询,其余的由服务层处理。
动态SQL
-
if 标签
-
属性:test
- 值:条件
-
<if test="name !=null and name !=''"> where name like #{name} </if>
-
-
choose标签
-
属性:无
-
子标签 :when
- 属性:test
- 值:条件
-
<when test="sex!=null and sex!=''"> sex=#{sex} </when> <when test="age!=null"> age>#{age} </when> <otherwise> <!--相当于switch中的default--> 1=1 </otherwise>
-
-
where 标签
-
属性:无
-
子标签:无
-
作用:自动填充where
-
select * from user <where> <if test="name !=null and name !=''"> name like #{name} </if> </where>
如果测试条件成立,则添加where
如果不成立,就不添加where
-
-
set标签
-
属性:无
-
子标签:无
-
作用:在更新时根据字段添加逗号,不需要手动添加,
-
update user <set> <if test="name!=null"> name=#{name} </if> <if test="age!=null"> age=#{age} </if> <if test="sex!=null"> sex=#{Sex} </if> </set> where id=#{id}
-
-
foreach标签
-
遍历传入的参数,生成长语句,类似于批量操作
-
属性:collection
- 值:array 表示传入的是数组
- 值:collection 表示传入的是集合(Map)
-
属性:item
- 值:遍历过程中单一元素存放的变量
-
属性:open
- 值:遍历前添加的字串
-
属性:close
- 值:遍历后添加的字串
-
属性:separator
- 值:每次遍历中间分割的符号
-
<insert id="batchInsert"> /*批量数据添加*/ insert into user values <foreach collection="collection" item="user" open="(" close=")" separator="),("> #{user.id},#{user.name},#{user.age},#{user.sex},#{user.birthday} </foreach> </insert>
-
注解形式
不建议使用这种方式,毕竟java文件太长的话,维护起来还是比较麻烦,并且编译在class中。
方法就是在映射接口中添加注解标签,语句放在注解里,如下所示。
@Select("SELECT * FROM user WHERE id = #{id}")
User findById(int id);
相应的也有Update Delete Insert标签。
当进行多表查询时,还需要借助另一个标签Results,其中分为一对多和多对一两种场景。接下来分别演示。
一对多
学生映射文件:
//根据教师id查询学生信息
@Select("select * from student where teacher_fk=#{id}")
List<Student>selectByTeacherId(int id);
教师映射文件:
//根据教师的id查询教师信息及其对应的学生信息
@Select("select * from teacher where id=#{id}")
@Results(value={
@Result(id = true,property = "id",column = "id"),//@Result(id = true)(表示这个值是表中的主键)
@Result(property ="name" ,column ="name"), //<result property="" column=""></result>
@Result(property ="sex" ,column ="sex"),
@Result(property ="age" ,column ="age"),
@Result(property = "students",column = "id",
many =@Many(select = "com.alc.mapper.StudentMapper.selectByTeacherId"),fetchType=FetchType.LAZY)
})
Teacher selectById(int id);
多对一
学生映射文件:
//根据教师id查询学生信息
@Select("select * from student where id=#{id}")
@Results(value={
@Result(id = true,property = "id",column = "id"),
@Result(property ="name" ,column ="name"),
@Result(property ="sex" ,column ="sex"),
@Result(property ="age" ,column ="age"),
@Result(property = "teacher",column = "id",
one =@One(select = "com.alc.mapper.TeacherMapper.selectById"),fetchType=FetchType.LAZY)
})
Student QueryById(int id);
教师映射文件:
//根据教师的id查询教师信息及其对应的学生信息
@Select("select * from teacher where id=#{id}")
Teacher selectById(int id);
一级缓存
默认情况下,一级缓存是打开的,当进行查询操作时,将会把结果存放在缓存中,当进行增删改操作,会清空缓存中的数据,避免总是查询数据库,降低系统效率。实际上,缓存中的数据存放在sqlSession对象中。
二级缓存
二级缓存默认关闭,如果开启,需要在核心配置文件中的settings标签中定义cacheEnabled为true,还需要在映射文件中添加<cache/>标签,二级缓存通常存放一些不经常改变的数据。通俗的说,他们作用域不太一样,一级缓存在sqlsession对象中,而二级缓存对当前的映射生效。当某一条语句不希望使用缓存,则在映射标签中添加属性useCache为false。
延迟加载(懒加载)
当进行多表查询时,有些情况下时不需要即使查询第二张表,如果数据量较大,就会降低用户体验,当用户需要第二张表数据时,再进行查询。
应用场景:用户实体中存在该用户的部门信息,但是页面中只有在查看详情以及编辑时需要,这个时候就可以使用懒加载策略。
<resultMap id="UserResult" type="com.alc.one2many.User">
<collection property="department" column="did" select="com.alc.dao.DeptMapper.findByDid" fetchType="lazy">
</collection>
</resultMap>
<select id="findById" resultMap="UserResult">
select * from user where id=#{id}
</select>
<!--fetchType:默认的情况下是eager,如果设置为lazy,就会起到延迟记载的效果-->
逆向工程
MyBatis提供了逆向工程,逆向工程可以通过数据库的连接,自动生成实体类、映射文件、以及相应的实现方法。
以下是实现逆向的依赖文件
<dependencies>
<!--逆向工程需要的jar-->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.7</version>
</dependency>
<!--mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.3</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--junit测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
以下是配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3">
<!--实体类实现序列化接口-->
<plugin type="org.mybatis.generator.plugins.SerializablePlugin" />
<!--生成实体类中的toString方法-->
<plugin type="org.mybatis.generator.plugins.ToStringPlugin" />
<commentGenerator>
<!--关闭注释-->
<property name="suppressAllComments" value="true"></property>
</commentGenerator>
<!-- 设定数据库连接 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/db_mybatis"
userId="root"
password="root">
</jdbcConnection>
<!-- 生成 实体类 存放的位置 -->
<javaModelGenerator targetPackage="com.alc.entity" targetProject=".\src\main\java">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>
<!-- 生成的映射文件的位置 -->
<sqlMapGenerator targetPackage="com.alc.mapper" targetProject=".\src\main\resources">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>
<!-- 生成的接口的存放位置 -->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.alc.mapper" targetProject=".\src\main\java">
<property name="enableSubPackages" value="true" />
</javaClientGenerator>
<!-- 设定反向生成的表 -->
<table tableName="goods"></table>
<table tableName="student"></table>
<table tableName="teacher"></table>
<table tableName="user"></table>
</context>
</generatorConfiguration>
以下是测试文件
@Test
public void generator() {
try {
List<String> warnings = new ArrayList<String>();
boolean overwrite = true;
File configFile = new File("配置文件路径");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator MyBatisGenerator =
new MyBatisGenerator(config, callback, warnings);
MyBatisGenerator.generate(null);
} catch (Exception e) {
e.printStackTrace();
}
}
异常
如果爆BindingException异常,检查下面的配置是否正确。
- 接口文件名和映射文件名是否一致
- 不需要写实现类,按照基于接口开发的规则
- 映射文件路径和java接口路径是否一致
- 是否在resources下创建的文件夹,不要习惯性的以’.'创建层级结构,资源文件中不能以‘.’作为目录分割