javaEE(SSM)-5:关联映射
[概念1]关联映射: 从多个关联表查询所需要的数据。
(1)关联,是指表与表之间的联系;关联关系有:1:1 ;1:n ;n:n 。
(2)映射是指将关联表之间的查询结果,自动进行类型转换和并存到java对象的各个对应的属性中。也就是表字段值映射到对象的属性值;
(3)关联映射通常是指关联表的查询操作,将查询结果保存到对象的基础属性及复合属性中。
通常一个表会对应定义一个java对象(pojo)来保存数据,当相关的其他表的数据,也要保存在这个对象中时,可以在该对象中定义一个包含其他对象的属性(这里称之为:复合属性/关联属性)。
例如:当查询用户时,将其对应的身份证信息(其他表)同时查询出来并保存,可以定义两个java对象:User和IDCard ,对应两表的数据;查询出User信息后,根据关联关系,再查询出IDCard信息,将IDCard的查询结果,作为User的一个属性,进行保存。
如果此时在User对象中也定义IDCard的每一个属性,那么这是冗余且不规范的、不合理的。
[概念2]关联表的查询: 分为嵌套查询(可懒加载)和嵌套结果(常用)。
(1)嵌套查询:先根据条件在一个表中查询,将查询结果映射到对象的基础属性后,再根据查询结果中的外键,在另外的关联表中继续查询,将结果映射到复合属性中。
(2)嵌套结果:使用SQL关联查询,一次性查询出结果,然后将结果分别映射到基础属性和复合属性中。
(一)1:1关联映射
【描述】
- 一对一查询,如一个用户只有一个身份证。
【要求】
- (1)用户表保存的是用户基本信息,如id,姓名,其他(如:密码、手机号码等);
- (2)身份证信息保存id、真实姓名、身份证号码、性别、地址、发证机关、有效期等。
【功能】
- 查询用户信息,同时查询身份证信息。
【思路】
-
(1)创建数据库mybatis,创建用户表user和身份证信息表idcard为简单起见,user表只包含id、username字段,使用cardid与身份证信息表相的id关联;而身份证信息表仅包含id和code。
-
(2)创建项目,在pom.xml中仅需要包含mybatis和mysql连接器的pom信息。
-
(3)创建mybatis配置文件.
-
(4)创建mapper,在mapper实现关联映射。
【实现】
1. 创建数据库mybatis
user用户表结构
id int primary key ;auto_increnmentusername varchar 20cardid int
身份证表结构
id int primary key ; auto_incrementcode int
这里自行输入必要的测试数据。
2. 创建项目mybatis
file=>new =>maven project=>勾选“create a simple project ” =>输入:>groupId:com.my
artifactId:mybatis =>完成finish。
2.1 配置pom.xml文件
4.0.0com.mymybatis0.0.1-SNAPSHOT org.mybatismybatis3.5.9mysqlmysql-connetcor-java8.0.27log4jlog4j1.2.17 org.apache.maven.plugins maven-war-plugin 3.3.2
必须添加mybatis和connectorJ依赖 (war依赖也可加进去) ;为了调试时清楚看出SQL执行情况,增加log4j的依赖
在src/main/java根目录下创建log4j.properties文件(固定文件名),将以下内容赋值到该文件。
# Global logging configuration log4j.rootLogger=ERROR, stdout # MyBatis logging configuration...#################################### 根据需要,只需要修改这里,其余地方不修改log4j.logger.mapper=TRACE #####################################log4j.logger 后面只跟包名# Console output... log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
log4j.properties是固定文件名,而且在src/main/java根目录下
log4j.logger.[这里是mapper包名]=TRACE ,表示对指定包下的mapper文件进行跟踪输出
2.2 mybatis的配置文件
在src/main/resource根目录下,创建配置文件:mybatis.xml和配置的属性文件db.properties
db.properties
driver=com.mysql.cj.jdbc.Driverurl=jdbc:mysql://localhost:3306/mybatis?serverTimezone=Asia/Shanghaiuname=rootpwd=1234
注意:确保拼写正确;特别是url中的大小写!!!
mybatis.xml
<configuration><properties resource="db.properties" /><typeAliases><package name="pojo"/></typeAliases><environments default="mysql"><environment id="mysql"><transactionManager type="JDBC"/><dataSource type="POOLED" ><property name="driver" value="${driver}" /><property name="url" value="${url}"/><property name="username" value="${uname}" /><property name="password" value="${pwd}" /></dataSource></environment></environments><mappers><mapper resource="mapper/UserMapper.xml" /><!-- !--></mappers></configuration>
mybatis.xml配置文件主要有四个部分:
(1)装载属性文件;
(2)配置pojo 别名。在后面mapper使用到pojo时,不需要使用全限定名
(3)配置数据源;;
(4)装载mapper文件!(添加mapper文件在下一步)
注意,$与#的区别
2.3 添加mapper文件
在src/main/java中,添加包:mapper ,在该包名下添加UserMapper.xml文件。
为了先测试项目各个配置是否正确(建议配置完一个测试一个,最好不要全部写好才测试!否则很难查找错误,确保每一步正确才进行下一步的实现)
<mapper namespace="mapper.UserMapper"><select id="getCount" resultType="int">select count(*) from user </select></mapper>
获取user表的数量,不需要输入参数,输出为整数,这样就暂时不需要POJO支持。
2.4 添加测试类
在src/test/java添加一个MyTest.java类,测试项目是否运行正确。
package mybatis;import java.io.IOException;import java.io.Reader;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;public class MyTest {@Testpublic void testOne() throws IOException {Reader reader=Resources.getResourceAsReader("mybatis.xml");SqlSessionFactory ssf=new SqlSessionFactoryBuilder().build(reader);SqlSession sqlSession=ssf.openSession();int count =sqlSession.selectOne("getCount");sqlSession.close();}}
2.5 测试结果
运行单元测试,如果一切无误,那么控制台将输出执行SQL的日志信息,如:
DEBUG [main] - ==> Preparing: select count(*) from userDEBUG [main] - ==> Parameters: TRACE [main] - <== Columns: count(*)TRACE [main] - <== Row: 3 【输出结果】DEBUG [main] - <== Total: 1 【执行次数】
3. 关联映射的实现-嵌套查询
3.1 添加POJO
在src/main/java添加包:pojo
添加idcard表对应的POJO(IDCard.java)
package pojo;public class IDCard {int id;String code; //public int getId() {return id;}public void setId(int id) {this.id = id;}public String getCode() {return code;}public void setCode(String code) {this.code = code;}}
添加user表对应的POJO—User.java
package pojo;public class User {int id;String username;IDCard card;//准备保存idcard表数据//public int getId() {return id;}public void setId(int id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public IDCard getCard() {return card;}public void setCard(IDCard card) {this.card = card;}}
3.2 修改UserMapper.xml文件,实现1:1映射-嵌套查询
【文件位置】src/main/java/mapper/UserMapper.xml
当查询结果包含复合属性时,在select中不能再使用reslutType属性,而是需要resultMap标记进行结果映射;
【基本语法】
。。。
实际上这里是装配当前已查询出来的列值,保存到对象的属性中
关联查询部分,当1:1关联查询时,在resultMap标记中,使用association标记。
【association标记语法】
。。。
【代码实现】
<mapper namespace="mapper.UserMapper"><select id="getCount" resultType="int">select count(*) from user</select> <select id="getById" parameterType="int" resultMap="userMap">select * fromuser where id=#{0} </select> <resultMap id="userMap" type="User"> <id property="id" column="id" /><result property="username" column="username" /> </resultMap><select id="getIDCard" parameterType="int" resultType="IDCard">select * from idcard where id=#{id}</select></mapper>
(1) resultMap中的id标记指明这个是关键字段;也可以使用result标记。
(2)property代表对象的属性;column代表列表,即select语句中的查询字段名,两者建立映射关系
(3)association标记只能用于1:1关联映射。 javaType指明复合属性的类型,property指明复合属性的属性名(POJO定义的属性和类型);这里column字段是嵌套查询的依据,外键。
(4)getIDCard查询结果将装配association中指定的属性card。
(5) association理解:根据外键,将查询结果赋值给POJO中哪个类型的哪个复合属性。
(6) resultMap理解:指定POJO类型,给它哪个属性赋值
3.3 测试运行
【测试方法】
@Testpublic void testTwo() throws IOException {Reader reader=Resources.getResourceAsReader("mybatis.xml");SqlSessionFactory ssf=new SqlSessionFactoryBuilder().build(reader);SqlSession sqlSession=ssf.openSession();User user =sqlSession.selectOne("getById",1);sqlSession.close();}
【测试结果】
主查询DEBUG [main] - ==> Preparing: select * from user where id=?DEBUG [main] - ==> Parameters: 1(Integer)TRACE [main] - <== Columns: id, username, cardidTRACE [main] - Preparing: select * from idcard where id=?DEBUG [main] - ====> Parameters: 1(Integer)TRACE [main] - <==== Columns: id, codeTRACE [main] - <==== Row: 1, 1111111DEBUG [main] - <==== Total: 1DEBUG [main] - <== Total: 1
4. 嵌套结果的实现
使用连接查询语句,一次性查询出结果,然后使用resultMap依次赋值,不同的是,association仅指定POJO类型和属性名,然后使用reslut逐个匹配赋值。
4.1 UserMapper.xml添加以下内容
select user.*,idcard.id as cardid,idcard.codefrom user,idcardwhere user.id=#{id} and user.cardid=idcard.id
(1)这种方式灵活,常用;但是不能应用懒加载。
(2)懒加载是指当需要获取复合属性时,才会执行嵌套的查询,否则不执行。
(3)实际上,可以有多个association。(多表)
4.2 测试
public void test3() throws IOException {Reader reader=Resources.getResourceAsReader("mybatis.xml");SqlSessionFactory ssf=new SqlSessionFactoryBuilder().build(reader);SqlSession sqlSession=ssf.openSession();User user =sqlSession.selectOne("byId",1);sqlSession.close();}
DEBUG [main] - ==> Preparing: select user.*,idcard.id as cardid,idcard.code from user,idcard where user.id=? and user.cardid=idcard.idDEBUG [main] - ==> Parameters: 1(Integer)TRACE [main] - <== Columns: id, username, cardid, cardid, codeTRACE [main] - <== Row: 1, zs1, 1, 1, 1111111DEBUG [main] - <== Total: 1
(二)1:n关联映射
【描述】
- 一个用户有N张订单,一张订单只属于一个用户
【功能】
- 根据用户ID,查询该用户信息的同时查询其所有订单
【要求】
- 每张订单包含的信息:id 、用户id、购物数量count,(简单起见,如:当前单价、购买日期,支付状态等。)
【思路】
-
创建订单表orders,添加订单POJO—Orders.java
-
创建Mapper实现1对多查询。
1. 订单表信息
id int primary key ;auto_increment ;//订单编号userid int ;//外键number int ;//商品数量
这里不需添加商品id,因为订单和商品之间组恰当的方式是使用N多对多实现。
2. 订单POJO—Orders.java
src/main/java/pojo/Orders.java
package pojo;public class Orders {int id;//订单idint userid;//客户IDint number;//购物数量//public int getId() {return id;}public void setId(int id) {this.id = id;}public int getUserid() {return userid;}public void setUserid(int userid) {this.userid = userid;}public int getNumber() {return number;}public void setNumber(int number) {this.number = number;}}
3. 修改User.java
为了将订单查询结果保存到User对象中,修改User.java内容为(添加了一个List属性):
package pojo;import java.util.List;public class User {int id;//客户idString username;//客户名称IDCard card;//保存idcard表数据//多了一个orderList复合属性List<Orders> orderList;//查询订单信息用 //省略了getter/setter }
4. 添加UserOrdersMapper.xml
在src/main/java/mapper包中添加映射文件:UserOrdersMapper.xml
select * from user where id=#{id} select * from orders where userid=#{id}
这里不再使用association标记,而代替的是collection标记,并且使用ofType代替javaType了,其他含义与1:1查询一致的。
5. 修改配置文件,加入新增加的mapper资源
src/main/resource/mybatis.xml配置文件,注册新创建的mapper文件
不要忘记要注册,经常会忘记写
6. 测试执行
@Testpublic void test4() throws IOException {Reader reader=Resources.getResourceAsReader("mybatis.xml");SqlSessionFactory ssf=new SqlSessionFactoryBuilder().build(reader);SqlSession sqlSession=ssf.openSession();User user =sqlSession.selectOne("getUserById",1);sqlSession.close();}
DEBUG [main] - ==> Preparing: select * from user where id=?DEBUG [main] - ==> Parameters: 1(Integer)TRACE [main] - <== Columns: id, username, cardidTRACE [main] - Preparing: select * from orders where userid=?DEBUG [main] - ====> Parameters: 1(Integer)TRACE [main] - <==== Columns: id, userid, numberTRACE [main] - <==== Row: 1, 1, 2TRACE [main] - <==== Row: 3, 1, 13TRACE [main] - <==== Row: 4, 1, 23DEBUG [main] - <==== Total: 3DEBUG [main] - <== Total: 1
7. 嵌套结果
实际上,理解了一对一,一对多的实现基本差不多,不同的是关联属性使用了collection,而且使用ofType属性指定复合属性的类型。嵌套结果依然使用连接查询,然后给各个属性赋值。
一对多查询,一般在一对一方,包含映射其他表的复合属性。
7.1 修改UserOrdersMapper.xml
在src/main/java/mapper/UserOrdersMapper.xml,添加如下内容
select user.*,orders.id as orderid ,orders.numberfrom user LEFT JOIN orders ON user.id=orders.useridwhere user.id=#{id}
这里使用了left join语法,不使用也是可以的
注意,要在配置文件注册!!!
7.2 执行结果
DEBUG [main] - ==> Preparing: select user.*,orders.id as orderid ,orders.number from user left join orders on user.id=orders.userid where user.id=?DEBUG [main] - ==> Parameters: 1(Integer)TRACE [main] - <== Columns: id, username, cardid, orderid, numberTRACE [main] - <== Row: 1, zs1, 1, 1, 2TRACE [main] - <== Row: 1, zs1, 1, 3, 13TRACE [main] - <== Row: 1, zs1, 1, 4, 23DEBUG [main] - <== Total: 3
(三)n:n关联查询
【描述】
- 一张订单有n个商品;一个商品可以存在不同的多张订单;根据订单id,找到所有商品的信息。
【功能】
- 根据订单ID,查询订单信息,以及订单中所有的商品。
【要求】
- 对于多对多的一般做法是在两者之间建立一个关联的中间表:
- 订单作为一个独立的表,商品也作为一个独立的表,两者可以使用一个中间表建立联系,中间表包含两者的关键字id,这样实际是转化为1:N关系:一个订单包含多个商品,一个商品存在于多个订单,而商品和订单都有为一个id。
【思路】
- 关键理解:根据订单id,查询订单信息;再根据订单id,在中间表找到所有商品的id(一组,1:n),商品信息包含在这一组id中(select in (一组商品id ))。
(1)创建订单表orders,及商品表,这里假定商品为书籍表book,添加订单POJO—Books.java
(2)创建中间表,book_orders,根据订单id与书籍id建立关联。
(3)创建Mapper实现1对多查询。
1. 商品表信息(book)
id int primary key ; auto_increment;//商品idbookname varchar 255 //书名price decimal //价格
2. 中间表 (book_orders)
id int primary key ; auto_increment;//本身idorderid int ;//订单idbookid int ;//书籍id
3. 添加POJO—Books.java
package pojo;public class Books {int id;//商品IDString bookname;//书名float price;//价格 //省略getter/setter}
4. 修改POJO(Orders.java),添加书籍列表信息
在查询订单的同时,查询订单中的商品
package pojo;import java.util.List;public class Orders {int id;int userid;int number;//新增加:一张订单包含哪些商品:这里的商品是书籍List<Books> books ; //省略getter/setter}
5. 新增加BooksOrdersMapper.xml,实现n:n
在src/main/java/mapper包中添加映射文件:BooksOrdersMapper.xml实现多对多查询
<mapper namespace="mapper.BooksOrdersMapper"><select id="getOrder" parameterType="int" resultMap="map">select * from orders where id=#{id}</select><resultMap id="map" type="Orders"><id property="id" column="id"/><result property="number" column="number" /></resultMap><select id="getBooks" parameterType="int" resultType="Books"> select book.* from book inner join book_orders on book.id=book_orders.bookid where book_orders.orderid =#{id}</select></mapper>
(1)有些地方使用wher id in (…),如不能查重复购买的商品这里修改为INNER JOIN.(实际上同一个订单不会出现两个同一个商品,会使用计数避免重复。)
(2)在collection标记中,传入的是订单id,但book表并没有订单的信息,是通过中间表的订单id与book表建立联系的
(3)注意:要在配置文件注册!
6. 测试结果
@Testpublic void test6() throws IOException {Reader reader=Resources.getResourceAsReader("mybatis.xml");SqlSessionFactory ssf=new SqlSessionFactoryBuilder().build(reader);SqlSession sqlSession=ssf.openSession();Orders order =sqlSession.selectOne("getOrder",1);sqlSession.close();}
DEBUG [main] - ==> Preparing: select * from orders where id=?DEBUG [main] - ==> Parameters: 1(Integer)TRACE [main] - <== Columns: id, userid, numberTRACE [main] - Preparing: select book.*,BO.id as boid,BO.bookid,BO.orderid from book INNER JOIN book_orders BO on BO.orderid=? and book.id=BO.bookidDEBUG [main] - ====> Parameters: 1(Integer)TRACE [main] - <==== Columns: id, bookname, price, boid, bookid, orderidTRACE [main] - <==== Row: 1, 三国演义, 12.60, 1, 1, 1TRACE [main] - <==== Row: 2, 水浒传, 22.00, 2, 2, 1TRACE [main] - <==== Row: 3, 红楼梦, 32.00, 3, 3, 1TRACE [main] - <==== Row: 1, 三国演义, 12.60, 5, 1, 1DEBUG [main] - <==== Total: 4DEBUG [main] - <== Total: 1
7. 更好的实现方式-嵌套结果
修改src/main/java/mapper包中添加映射文件:BooksOrdersMapper.xml
【添加查询】
select orders.*,book.id as bid,book.bookname,book.pricefrom orders left join book_orders on orders.id=book_orders.orderidleft join book on book.id=book_orders.bookidwhere orders.id =#{id}
内连接:逐个找表之间的对应关系,最后是where条件
【测试代码】
Orders order =sqlSession.selectOne("findOrder",1);
【测试结果】
DEBUG [main] - ==> Preparing: select orders.*,BO.orderid,BO.bookid,book.id as bid,book.bookname,book.price from orders INNER JOIN book_orders BO ON orders.id=BO.orderid INNER JOIN book ON BO.bookid =book.id where orders.id=?DEBUG [main] - ==> Parameters: 1(Integer)TRACE [main] - <== Columns: id, userid, number, orderid, bookid, bid, bookname, priceTRACE [main] - <== Row: 1, 1, 2, 1, 1, 1, 三国演义, 12.60TRACE [main] - <== Row: 1, 1, 2, 1, 2, 2, 水浒传, 22.00TRACE [main] - <== Row: 1, 1, 2, 1, 3, 3, 红楼梦, 32.00TRACE [main] - <== Row: 1, 1, 2, 1, 1, 1, 三国演义, 12.60DEBUG [main] - <== Total: 4