最近在搭一个SSM的开发环境给新项目用,就写了一些测试代码,也算温习下自己对Mybatis的学习,而且这一路上还是有很多坑的。
一.Mybatis一对一插入
场景,描述NBA湖人队中队伍,教练和队员的关系
很明显,队伍和教练是一对一的关系,而队伍和队员是一对多的关系,我们先看一对一的关系。
create table coach(
name varchar2(60),
id number(10),
age number(3),
salary number(10,1),
primary key(id)
);
create table team(
team_name varchar2(60),
location varchar2(60),
id number(10),
coach_id number(10),
all_salary number(11,1),
primary key(id)
);
team表里面有一个coach_id字段,但是这里并没有设置成外键,因为现在的应用都趋向于去外键,在程序中用代码来控制和确保一致性,这是为了减轻数据库的压力,而把压力放到应用服务器端,不过当然在程序中控制一致性肯定没有数据库控制的好(虽然说数据库其实也是程序呀),但是在不是那么强力的要求强一致性的情况下,还是可以选择不要外键。
package com.wangcc.ssm.entity;
import java.io.Serializable;
public class Coach implements
Serializable{
private String name;
private Integer id;
private int age;
public Coach() {}
public Coach(String name, int age, float salary) {
super();
this.name = name;
this.age = age;
this.salary = salary;
}
private float salary;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public float getSalary() {
return salary;
}
public void setSalary(float salary) {
this.salary = salary;
}
}
package com.wangcc.ssm.entity;
import java.io.Serializable;
import java.util.List;
public class Team implements Serializable{
private String teamName;
private String location;
private Integer id;
private Coach coach;
private List<Player> players;
public List<Player> getPlayers() {
return players;
}
public void setPlayers(List<Player> players) {
this.players = players;
}
public Team () {}
public Team(String teamName, String location, Coach coach, List<Player> players, float allSalary) {
super();
this.teamName = teamName;
this.location = location;
this.coach = coach;
this.players = players;
this.allSalary = allSalary;
}
public Team(String teamName, String location, float allSalary,Coach coach) {
super();
this.teamName = teamName;
this.location = location;
this.allSalary = allSalary;
this.coach=coach;
}
public Coach getCoach() {
return coach;
}
public void setCoach(Coach coach) {
this.coach = coach;
}
private float allSalary;
public String getTeamName() {
return teamName;
}
public void setTeamName(String teamName) {
this.teamName = teamName;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public float getAllSalary() {
return allSalary;
}
public void setAllSalary(float allSalary) {
this.allSalary = allSalary;
}
}
这里的实体类我都写了好几个构造方法,其实目的只是为了测试的时候更方便点而已(主要是测试TypeHandler的时候),一般情况下是不需要的,这里细心的同学会发现,我显式的写出了实体类的空构造方法,这是为什么呢,后面会讲到。
对应的Mapper接口和Mapper文件,Mapper接口和Mapper文件中的namespace需要相同,这样才能在XML配置文件中为动态代理过程中找到相应的方法实际执行的sql语句,以及去将sql补全,替换参数注入等工作。
<?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.wangcc.ssm.dao.CoachDao">
<select id="getCoachById" parameterType="Integer" resultType="Coach">
select * from coach where id=#{id}
</select>
<insert id="insertCoach" parameterType="Coach">
<!--
AFTER代表在insert语句之后执行
这里我之前一直犯了一个错误
这是把主键返回给JavaBean中的相应属性,为keyProperty对应的属性值,这里为id,而不是说这个方法的返回值就是主键
Oracle AFTER BEFORE都可以,有小伙伴说必须用BEFORE,但是我试了BEFORE和AFTER都可以 -->
<selectKey resultType="integer" order="AFTER" keyProperty="id">
select test_coach.currval from dual
</selectKey>
insert into coach (id,name,age,salary) values(test_coach.nextval,#{name},#{age},#{salary})
</insert>
</mapper>
package com.wangcc.ssm.dao;
import com.wangcc.ssm.entity.Coach;
public interface CoachDao {
public Coach getCoachById(Integer id);
public Integer insertCoach(Coach coach);
}
<?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.wangcc.ssm.dao.TeamDao">
<resultMap type="Team" id="TeamMap">
<result property="teamName" column="team_name" />
<result property="loaction" column="loaction" />
<result property="id" column="id"/>
<result property="allSalary" column="all_salary"/>
<!-- association 和 collection 有顺序要求-->
<association property="coach" column="coach_id" select="com.wangcc.ssm.dao.CoachDao.getCoachById"></association>
<!-- ofType的作用 还有javaType 这里最好还是用javaType="arraylist"-->
<collection property="players" ofType="Player" column="id" select="com.wangcc.ssm.dao.PlayerDao.selectByTeamId">
</collection>
</resultMap>
<select id="getTeamById" parameterType="Integer" resultMap="TeamMap">
select * from team where id=#{id}
</select>
<insert id="insertTeam" parameterType="Team">
<selectKey resultType="integer" order="AFTER" keyProperty="id">
select test_team.currval from dual
</selectKey>
insert into team (id,team_name,location,all_salary,coach_id) values (test_team.nextval,#{teamName},#{location},#{allSalary},#{coach.id})
</insert>
</mapper>
package com.wangcc.ssm.dao;
import com.wangcc.ssm.entity.Team;
public interface TeamDao {
public Team getTeamById(Integer id);
public void insertTeam(Team team);
}
我们先看一对一的插入,这里我们想先插入Coach的数据,再将coachId得到,然后再与Team一同插入数据库。
首先,我们知道coachId作为主键,我们要么通过UUID得到,要么就是数据库自增长,当然还有其他方式。也就是说很大的可能,coachId在我们的Java程序中不是由我们自己得到值然后去插入的,而是数据库自动插入,不需要Java程序去提供数据。那么这个时候我们怎么才能得到我们要的coachId呢,Mybatis有一个很好的功能支持了这一想法的实现:selectKey。我们看CoachMapper.xml
<insert id="insertCoach" parameterType="Coach">
<!--
AFTER代表在insert语句之后执行
这里我之前一直犯了一个错误
这是把主键返回给JavaBean中的相应属性,为keyProperty对应的属性值,这里为id,而不是说这个方法的返回值就是主键
Oracle AFTER BEFORE都可以,有小伙伴说必须用BEFORE,但是我试了BEFORE和AFTER都可以 -->
<selectKey resultType="integer" order="AFTER" keyProperty="id">
select test_coach.currval from dual
</selectKey>
insert into coach (id,name,age,salary) values(test_coach.nextval,#{name},#{age},#{salary})
</insert>
selectKey用于insert中,他的作用是把主键返回给JavaBean中的相应属性,相应属性对应为keyProperty对应的属性值,需要指定resultType,然而insert中是不能有resultType的,默认为int(rowcount),但是所有的select都必须有result的。selectKey中的order属性有AFTER和BEFORE,代表在insert操作之后和之前进行。我这里用的是Oracle数据库(最近要切换数据库了,得用mysql啦)。网上有人说,用Oracle必须指定order为BEFORE,然而我经过实验,发现前后都可以。Oracle一般用sequence序列做主键,达到自增长效果。其实如果说是只能AFTER的话,我倒是觉得还有点道理,因为对Oracle数据而言,在一个session中,必须,先执行test_coach.nextval,再执行test_coach.currval,否则是会报错的。不能直接执行test_coach.currval,这里为什么可以使用BEFORE呢,难道意思就是每一条语句对应一个Session吗,这个我们调试源码就能知道了。
好,现在我们通过selectkey的使用得到了coachId。
这样我们就可以进行对Team的插入了。
我们看Team的实体类,我们发现,虽然我们插入数据库的coachId,但是实体类中并没有coachId属性,而是有一个Coach属性。那是怎么将coachId插入数据库的呢,我们看xml配置文件。
<insert id="insertTeam" parameterType="Team">
<selectKey resultType="integer" order="AFTER" keyProperty="id">
select test_team.currval from dual
</selectKey>
insert into team (id,team_name,location,all_salary,coach_id) values (test_team.nextval,#{teamName},#{location},#{allSalary},#{coach.id})
</insert>
我们发现coach_id对应了coach.id,我们只需要把coach赋值给Team就可以了,Mybatis会通过反射在coach.id读取出相应的值插入。
我们看相关测试代码。
package com.wangcc.test;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;
import org.apache.log4j.Logger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.alibaba.fastjson.JSON;
import com.wangcc.ssm.entity.Coach;
import com.wangcc.ssm.entity.Player;
import com.wangcc.ssm.entity.Team;
import com.wangcc.ssm.service.CoachService;
import com.wangcc.ssm.service.PlayerService;
import com.wangcc.ssm.service.TeamService;
//不支持junit4.4,需要更高版本的junit
//http://blog.csdn.net/zacry/article/details/37052973 http://blog.csdn.net/bruce128/article/details/9792283
@RunWith(SpringJUnit4ClassRunner.class) //表示继承了SpringJUnit4ClassRunner类
@ContextConfiguration(locations = {"classpath:mybatis-spring.xml"})
public class SpringMybatisTest {
private static Logger logger = Logger.getLogger(SpringMybatisTest.class);
@Resource
private PlayerService playerService = null;
@Resource
private CoachService coachService;
@Resource
private TeamService teamService;
@Test
public void test1() {
Player player=new Player(24,14);
playerService.insert(player);
// System.out.println(user.getUserName());
// logger.info("值:"+user.getUserName());
logger.info(JSON.toJSONString(player));
}
@Test
public void testGet() {
Player player=playerService.selectById(24);
if(player.getName()!=null&&player.getName().equals("")) {
System.out.println("SUCCESS!");
}
}
@Test
public void testGetInt() {
Player player=playerService.selectById(14);
System.out.println(player.getAge());
}
@Test
public void testOneToOne() {
Coach coach=new Coach("phil jackson", 79, 11111.1f);
coachService.insertCoach(coach);
System.out.println(coach.getId());
Team team=new Team("lakerss", "los angeles", 1112334.1f, coach);
teamService.insertTeam(team);
}
@Test
public void testGetOneToOne() {
Team team=teamService.getTeamById(5);
logger.info(JSON.toJSONString(team));
Coach coach=team.getCoach();
logger.info(JSON.toJSONString(coach));
}
@Test
public void testInsertTeam() {
Coach coach=new Coach("phil jackson1", 80, 11111.1f);
coachService.insertCoach(coach);
logger.info(JSON.toJSONString(coach));
Team team=new Team("my lakerss1", "los angeles", 1112334.1f, coach);
teamService.insertTeam(team);
logger.info(JSON.toJSONString(team));
Player player=new Player("kobe paul", 32, team.getId());
playerService.insert(player);
Player player1=new Player("paul", 32, team.getId());
playerService.insert(player1);
}
@Test
public void testGetTeam() {
Team team=teamService.getTeamById(7);
logger.info(JSON.toJSONString(team));
Coach coach=team.getCoach();
logger.info(JSON.toJSONString(coach));
List<Player> players=team.getPlayers();
logger.info(JSON.toJSONString(players));
}
}
@Test
public void testOneToOne() {
Coach coach=new Coach("phil jackson", 79, 11111.1f);
coachService.insertCoach(coach);
System.out.println(coach.getId());
Team team=new Team("lakerss", "los angeles", 1112334.1f, coach);
teamService.insertTeam(team);
}
这个方法就是一对一插入的测试方法。
我们看这个测试类,还是有一些地方可以扯一下的。
刚开始我在pom文件配置的junit的版本是4.4,然后发现@RunWith(SpringJUnit4ClassRunner.class) 报错,查找资料发现是SpringJUnit4ClassRunner不支持4.4junit版本,你需要用更新的版本,然后将junit依然文件version改为4.12。
@ContextConfiguration注解,目前也没有去深究他,看样子是在Junit框架中帮我们完成了自动加载相关配置文件的功能。(相关配置文件上一篇TypeHandler中有)
这里Service的注解不是@Autowired自动注入,而是@Resource。
这两种有什么异同呢,简单的说,他们两个都可以要来装入bean,也都可以用在属性或者setter方法上。
不同的地方呢:
- @Autowired是属于Spring的注解,而且是按照类型匹配的,也就是说当如果你是在对接口使用这个注解的时候,如果他只有一个实现类那还好说,直接用这个注解就好,反正也就一个这种类型的Bean存在,但是有多个实现类的话,这个注解就要打出gg了,因为这个注解是没有name属性滴,他只有一个属性,那就是required,默认为true(必须要求依赖对象必须存在,如果要允许null 值,可以设置它的required属性为false),但是Spring这么牛逼的框架不允许这种低级的GG出现,这个时候虽然@Autowired一个人是hold不住了,但是我可以给他一个辅助选手呀,那就是@Qualifier用来指定名称,这个时候相应的接口实现类就不是一个简单的在类上用一个@Service注解或者@Repository就可以了,这个时候就必须要在这些注解上写上对应的name,要不然无法匹配。
- @Resource是属于Java EE的注解,这个注解和@Autowired不同,认安照名称进行装配,名称可以通过name属性进行指定, 如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在setter方法上默认取属性名进行装配。 当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。
二.一对一查询
看完了一对一插入,我们再来看看一对一查询。
我们来看看TeamMapper.xml的相关配置文件
<resultMap type="Team" id="TeamMap">
<result property="teamName" column="team_name" />
<result property="loaction" column="loaction" />
<result property="id" column="id"/>
<result property="allSalary" column="all_salary"/>
<!-- association 和 collection 有顺序要求-->
<association property="coach" column="coach_id" select="com.wangcc.ssm.dao.CoachDao.getCoachById"></association>
<!-- ofType的作用 还有javaType 这里最好还是用javaType="arraylist"-->
<collection property="players" ofType="Player" column="id" select="com.wangcc.ssm.dao.PlayerDao.selectByTeamId">
</collection>
</resultMap>
<select id="getTeamById" parameterType="Integer" resultMap="TeamMap">
select * from team where id=#{id}
</select>
我们看到这段代码中有resultMap的出现,而CoachMapper.xml中并没有resultMap出现,是上面造成了这个区别呢。
其实并不是CoachMapper.xml没有resultMap,在CoachMapper.XML中我们的select属性中是resultType,其实CoachMapper.XML是隐式的实现了给出了一个resultMap,只不过他的property和column是相同的而已,也就是说,如果数据库中的字段,并不和Coach实体类中的属性值都刚好一一对应的话,是一定要显式的给出resultMap的话,因为你不给出这种关系的话,Mybatis无法通过反射得到column和property的关系,这就意味着我们无法将JavaBean中的属性变成sql语句中?对应的参数的值(ps.setObject(value),最近也在着手写仿Mybatis的框架,可能先在还描述的不是很清楚,其实就是你无法绑定参数了,必须让Mybatis得到column和property的关系)。
我们看resultMap中的具体内容,首先我们不看collection(这个一对多的时候再说)。我们发现除了最基本的result,我们还有一个association,这个属性表示一对一的关系,我们可以看到他比result多一个属性,select(在ibatis中,一对 一和一对多不需要association,collection,select就放在result属性中,其实就我目前看来,似乎mybatis要比ibatis要繁琐一些,再比如result中的nullvalue属性的取消,使用TypeHandler来代替,不过,当然mybatis肯定是在进步的,只是我目前学的太少而已,才没有感受到他的强大)。在这里,我们的select对应的就是可以通过column来查询到Coach的select语句。经过这样的配置,我们便可以成功的来查询数据。
三.实践中的错误
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: Error instantiating class com.wangcc.ssm.entity.Team with invalid types () or values (). Cause: java.lang.NoSuchMethodException: com.wangcc.ssm.entity.Team.<init>()
当我为实体类创建了一个有参的构造方法后,我再运行测试类,发现报这个错,很明显(),构造方法出错了,是缺少了无参构造方法。为什么一定要有无参构造方法呢,其实很好理解,首先,我们应该知道ORM框架的核心就是反射和动态代理,后来用了注解之后再可以加一个注解。
这些Java基础技术真的是非常的有用。而这里需要无参构造方法,肯定是和Mybatis通过反射得到实体类对象有关。
因为如果没有无参构造方法,无法通过反射来得到对象,class.newInstance();报错,因为没有无参构造方法,
我们能够通过loadClass得到Class