当前位置:   article > 正文

MyBatis(三)——SQL映射文件_sql中使用对象接收返回值,对应java bean中缺少无参构造

sql中使用对象接收返回值,对应java bean中缺少无参构造

概述

  映射文件指导着MyBatis如何进行数据库增删改查,有着非常重要的意义。这篇博客主要写了MyBatis中简单的增删改查操作,参数的传递方式,以及自定义resultMap,实现高级结果集映射。

  
  cache –命名空间的二级缓存配置。
  cache-ref – 其他命名空间缓存配置的引用。
  resultMap – 自定义结果集映射。
  parameterMap – 已废弃!老式风格的参数映射。
  sql –抽取可重用语句块。
  insert – 映射插入语句。
  update – 映射更新语句。
  delete – 映射删除语句。
  select – 映射查询语句。

简单的增删改查

  MyBatis中的增删改查操作是通过insert、delete、update、selete元素标签实现的。这些标签所包含的属性如下所示。
这里写图片描述

一、创建EmployeeMapper接口

package com.mybatis.dao;

import com.mybatis.bean.Employee;

public interface EmployeeMapper {

    boolean addEmp(Employee employee);

    boolean deleteEmpById(Integer id);

    boolean updateEmp(Employee employee);

    Employee getEmpById(Integer id);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

二、创建和接口对应的mapper文件

<?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.mybatis.dao.EmployeeMapper">

    <!--boolean addEmp(Employee employee);-->
    <insert id="addEmp" parameterType="com.mybatis.bean.Employee">
        insert into tbl_employee(last_name,email,gender)
        values(#{lastName},#{email},#{gender})
    </insert>

    <!-- boolean deleteEmpById(Integer id); -->
    <delete id="deleteEmpById">
        delete from tbl_employee where id=#{id}
    </delete>

    <!-- boolean updateEmp(Employee employee);  -->
    <update id="updateEmp">
        update tbl_employee
        set last_name=#{lastName},email=#{email},gender=#{gender}
        where id=#{id}
    </update>

    <!--Employee getEmpById(Integer id);-->
    <select id="getEmpById" resultType="com.mybatis.bean.Employee" parameterType="Integer" >
        select id,last_name,email,gender from tbl_employee where id = #{id}
    </select>


</mapper>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

三、测试类

    @Test
    public void test03() throws IOException{

        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        //1、获取到的SqlSession不会自动提交数据
        SqlSession openSession = sqlSessionFactory.openSession();

        try{
            EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
            //测试添加
            Employee employee = new Employee(null, "jerry4",null, "1");
            mapper.addEmp(employee);
            System.out.println(employee.getId());

            //测试修改
            Employee employee1 = new Employee(1, "Tom", "jerry@atguigu.com", "0");
            boolean updateEmp = mapper.updateEmp(employee1);
            System.out.println(updateEmp);
            //测试删除
            mapper.deleteEmpById(2);
            //2、手动提交数据
            openSession.commit();
        }finally{
            openSession.close();
        }

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

主键生成方式

  若数据库支持自动生成主键的字段(比如 MySQL和 SQL Server),则可以设置
useGeneratedKeys=”true”,然后再把keyProperty 设置到目标属性上。

    <insert id="addEmp" parameterType="com.mybatis.bean.Employee"
        useGeneratedKeys="true" keyProperty="id" databaseId="mysql">
        insert into tbl_employee(last_name,email,gender) 
        values(#{lastName},#{email},#{gender})
    </insert>
  • 1
  • 2
  • 3
  • 4
  • 5

  mysql支持自增主键,自增主键值的获取是利用statement.getGenreatedKeys();
  useGeneratedKeys=”true”:使用自增主键获取主键值策略。
  keyProperty:指定对应的主键属性,也就是mybatis获取到主键值以后,将这个值封装给javaBean的哪个属性。
  
  问题:Oracle不支持自增,Oracle使用序列来模拟自增。每次插入的数据的主键是从序列中拿到的值,那么该如何获取到这个值呢?
  而对于不支持自增型主键的数据库(例如Oracle),则可以使用 selectKey 子元素:
selectKey 元素将会首先运行,id 会被设置,然后插入语句会被调用。

这里写图片描述

    <insert id="addEmp" parameterType="com.mybatis.bean.Employee">

        <selectKey keyProperty="id" order="BEFORE" resultType="Integer">
            <!-- 编写查询主键的sql语句 -->
            select EMPLOYEES_SEQ.nextval from dual
        </selectKey>

        <!-- 插入时的主键是从序列中拿到的 -->
        insert into employees(EMPLOYEE_ID,LAST_NAME,EMAIL)
        values(#{id},#{lastName},#{email})

    </insert>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

  keyProperty:查出的主键值封装给javaBean的哪个属性
  order=”BEFORE”:当前sql在插入sql之前运行
      AFTER:当前sql在插入sql之后运行
  resultType:查出的数据的返回值类型
  
  BEFORE运行顺序:
  1.先运行selectKey查询id的sql;查出id值封装给javaBean的id属性
  2.在运行插入的sql;就可以取出id属性对应的值
  AFTER运行顺序:
  1.先运行插入的sql(从序列中取出新值作为id);
  2.再运行selectKey查询id的sql;
  

参数(Parameters)传递

单个参数

单个参数:mybatis不会做特殊处理,#{参数名/任意名}:取出参数值。

多个参数

多个参数:mybatis会做特殊处理。多个参数会被封装成 一个map,
    key:param1…paramN,或者参数的索引也可以
    value:传入的参数值
#{}就是从map中获取指定的key的值;

注意,以下操作会抛出异常:
操作:
  方法:public Employee getEmpByIdAndLastName(Integer id,String lastName);
  取值:#{id},#{lastName}
异常:
  org.apache.ibatis.binding.BindingException:
  Parameter ‘id’ not found.
  Available parameters are [1, 0, param1, param2]

解决方法:
  【命名参数】:明确指定封装参数时map的key;@Param(“id”),@Param(“lastName”)
  

public Employee getEmpByIdAndLastName(@Param("id")Integer id,
@Param("lastName")String lastName);
  • 1
  • 2

  多个参数会被封装成 一个map。
  key:使用@Param注解指定的值
  value:参数值
  #{指定的key}取出对应的参数值
 

POJO

如果多个参数正好是我们业务逻辑的数据模型,我们就可以直接传入pojo;
#{属性名}:取出传入的pojo的属性值

Map

如果多个参数不是业务模型中的数据,没有对应的pojo,不经常使用,为了方便,我们也可以传入map。
#{key}:取出map中对应的值

示例代码:

    Employee getEmpByMap(Map<String, Object> map);
  • 1
    <select id="getEmpByMap" parameterType="map" resultType="com.mybatis.bean.Employee">
        SELECT * FROM tbl_employee WHERE id = #{id} and last_name=#{lastName}
    </select>
  • 1
  • 2
  • 3
    @Test
    public void test04() throws IOException{

        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        SqlSession openSession = sqlSessionFactory.openSession();
        try{
            EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
            Map<String, Object> map = new HashMap<>();
            map.put("id", 1);
            map.put("lastName", "tom");
            Employee employee = mapper.getEmpByMap(map);

            System.out.println(employee);

        }finally{
            openSession.close();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

Employee [id=1, lastName=tom, email=912324@qq.com, gender=0]

TO

如果多个参数不是业务模型中的数据,但是经常要使用,推荐来编写一个TO(Transfer Object)数据传输对象。

Page{
    int index;
    int size;
}
  • 1
  • 2
  • 3
  • 4

一些取值的示例:

public Employee getEmp(@Param("id") Integer id, String lastName);
  • 1

取值:id==>#{id/param1} lastName==>#{param2}

public Employee getEmp(Integer id,@Param("e")Employee emp);
  • 1

取值:id==>#{param1} lastName===>#{param2.lastName/e.lastName}

特别注意:如果是Collection(List、Set)类型或者是数组,也会特殊处理。也是把传入的list或者数组封装在map中。
key:Collection(collection),如果是List还可以使用这个key(list),数组(array)

public Employee getEmpById(List< Integer > ids);
  • 1

取值:取出第一个id的值: #{list[0]}

总结

参数多时会封装map,为了不混乱,我们可以使用@Param来指定封装时使用的key。
#{key}就可以取出map中的值。

参数值的获取

#{}和 ${}都可以获取map中的值或者pojo对象属性的值;他们有什么区别呢?

#{}和 ${}参数值获取的区别

#{}:是以预编译的形式,将参数设置到sql语句中,可以防止sql注入
${}:取出的值直接拼装在sql语句中,会有安全问题;大多情况下,我们去参数的值都应该去使用#{}。

select * from tbl_employee where id=${id} and last_name=#{lastName}
  • 1

Preparing: select * from tbl_employee where id=2 and last_name=?

注: 在< settings >中增加

<setting name="logImpl" value="STDOUT_LOGGING" />
  • 1

可以打印出执行的SQL语句。

在原生jdbc不支持占位符的地方我们就可以使用${}进行取值。比如分表、排序、按照年份分表拆分表名。。。

select * from ${year}_salary where xxx;
select * from tbl_employee order by ${f_name} ${order}
  • 1
  • 2

参数位置支持的属性

这里写图片描述
jdbcType通常需要在某种特定的条件下被设置:
  在我们数据为null的时候,有些数据库可能不能识别mybatis对null的默认处理。比如Oracle(报错):
  JdbcType的OTHER类型:无效的类型;因为mybatis对所有的null都映射的是原生Jdbc的OTHER类型,oracle不能正确处理。
  
解决方法:
由于全局配置中:jdbcTypeForNull=OTHER,oracle不支持。所以有两种办法:
1、#{email,jdbcType=NULL};
2、改全局配置:jdbcTypeForNull=NULL

<setting name="jdbcTypeForNull" value="NULL"/>
  • 1

select元素

Select元素来定义查询操作。

 <select id="getEmpById" resultType="com.mybatis.bean.Employee" parameterType="Integer" >
        select id,last_name,email,gender from tbl_employee where id = #{id}
    </select>
  • 1
  • 2
  • 3

  Id:唯一标识符。用来引用这条语句,需要和接口的方法名一致
  parameterType:参数类型。 可以不传,MyBatis会根据TypeHandler自动推断。
  resultType:返回值类型。别名或者全类名,如果返回的是集合,定义集合中元
素的类型。不能和resultMap同时使用。

resultType:如果返回的是一个集合,要写集合中元素的类型

//查找所有男性员工
List<Employee> getEmpList(int gender);
  • 1
  • 2
    <select id="getEmpList" parameterType="Integer" resultType="com.mybatis.bean.Employee">
        select * from tbl_employee WHERE gender = #{gender};
    </select>
  • 1
  • 2
  • 3

接下来看另一个小案例:
  查找所有男性员工,将找到的所有员工封装成一个Map,Map的key为员工id,Map的value为员工的类。
  

    @MapKey("id")
    Map<Integer, Employee> getEmpMap(int gender);
  • 1
  • 2

@MapKey:告诉mybatis封装这个map的时候使用哪个属性作为map的key

    <select id="getEmpMap" parameterType="Integer" resultType="com.mybatis.bean.Employee" >
        select * from tbl_employee WHERE gender = #{gender};
    </select>
  • 1
  • 2
  • 3

select元素的其他属性

这里写图片描述

自动映射

  1、全局setting设置,autoMappingBehavior默认是PARTIAL,开启自动映射
的功能。唯一的要求是列名和javaBean属性名一致。如果autoMappingBehavior设置为null则会取消自动映射。
  数据库字段命名规范,POJO属性符合驼峰命名法,如A_COLUMN->aColumn,我们可以开启自动驼峰命名规则映射功能,mapUnderscoreToCamelCase=true。
  2、自定义resultMap,实现高级结果集映射。

resultMap

resultMap介绍

  自定义resultMap,实现高级结果集映射。
  resultMap的基本作用是:建立SQL查询结果字段与实体属性的映射关系信息。
  
resultMap的一些属性值如下所示:
  constructor:类在实例化时, 用来注入结果到构造方法中。
    idArg:ID 参数; 标记结果作为 ID 可以帮助提高整体效能。
    arg:注入到构造方法的一个普通结果。
  id :一个 ID 结果; 标记结果作为 ID 可以帮助提高整体效能
  result:注入到字段或 JavaBean 属性的普通结果。
  
  association :一个复杂的类型关联,许多结果将包成这种类型。
 
  collection :复杂类型的集。
  
  discriminator :使用结果值来决定使用哪个结果映射。
    case:基于某些值的结果映射。
    

id和result

  id 和 result 映射一个单独列的值到简单数据类型(字符串,整型,双精度浮点数,日期等)的属性或字段。
  
  这里写图片描述
自定义某个javaBean的封装规则示例代码:

    <resultMap type="com.mybatis.bean.Employee" id="MyEmp">
        <!--指定主键列的封装规则
        id定义主键会底层有优化;
        column:指定哪一列
        property:指定对应的javaBean属性
          -->
        <id column="id" property="id"/>
        <!-- 定义普通列封装规则 -->
        <result column="last_name" property="lastName"/>
        <!-- 其他不指定的列会自动封装:我们只要写resultMap就把全部的映射规则都写上。 -->
        <result column="email" property="email"/>
        <result column="gender" property="gender"/>
    </resultMap>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

  column:指定哪一列
  property:指定对应的javaBean属性
  我们只要写resultMap就把全部的映射规则都写上。

    <select id="getEmpById"  resultMap="MyEmp">
        select * from tbl_employee where id=#{id}
    </select>
  • 1
  • 2
  • 3

association

  由于POJO中的属性可能会是一个对象。所以,association常用来对复杂对象映射。
  
场景一:
  查询Employee的同时查询员工对应的部门,Employee===Department,一个员工有与之对应的部门信息;
  可以使用联合查询,并以级联属性的方式封装对象。
  

<resultMap type="com.mybatis.bean.Employee" id="MyEmp">
        <id column="id" property="id"/>
        <result column="last_name" property="lastName"/>
        <result column="email" property="email"/>
        <result column="gender" property="gender"/>
        <result column="dept_id" property="dept.id"/>
        <result column="dept_name" property="dept.departmentName"/>
    </resultMap>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

  使用association标签定义对象的封装规则
  

   <resultMap type="com.mybatis.bean.Employee" id="MyEmp2">
        <id column="id" property="id"/>
        <result column="last_name" property="lastName"/>
        <result column="email" property="email"/>
        <result column="gender" property="gender"/>

        <!--  association可以指定联合的javaBean对象
        property="dept":指定哪个属性是联合的对象
        javaType:指定这个属性对象的类型[不能省略]
        -->
        <association property="dept" javaType="com.mybatis.bean.Department">
            <id column="dept_id" property="id"/>
            <result column="dept_name" property="departmentName"/>
        </association>
    </resultMap>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

注意: 如果出现以下异常

Cause: org.apache.ibatis.executor.ExecutorException: No constructor found in com.mybatis.bean.Employee matching [java.lang.Integer, java.lang.String, java.lang.String, java.lang.String, java.lang.Integer, java.lang.Integer, java.lang.String]
  • 1

说明在POJO类中缺少了无参的构造函数。

public class Employee {

    private Integer id;
    private String lastName;
    private String email;
    private String gender;

    private Department dept;

    public Employee() {
    }

    public Employee(Integer id, String lastName, String email, String gender, Department dept) {
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = gender;
        this.dept = dept;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
public class Department {

    private Integer id;
    private String departmentName;

    public Department() {
    }

    public Department(Integer id, String departmentName) {
        this.id = id;
        this.departmentName = departmentName;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

  每个类至少要有一个构造函数,如果你自己构建了一个带有参数的构造函数而没有再显示的写出无参的构造函数也是可以的,不过当你尝试通过一个无参的构造函数来构建(new)时,此时编译器才会报错,因为找不到这个无参的构造函数。也就是说当一个类你没有给他构造函数,则编译器会自动补上一个无参的,若有的话就不会,你需要显示将此无参的构造函数写出来。
  所以在创建某个实体类的时候,应该养成一个好习惯,显示构造一个无参构造函数,这样就会避免后面遇到的奇葩问题了。
  
  association-分段查询
  
   使用association进行分步查询:
   1、先按照员工id查询员工信息
   2、根据查询员工信息中的d_id值去部门表查出部门信息
   3、部门设置到员工中;

     <resultMap type="com.mybatis.bean.Employee" id="MyEmpByStep">
        <id column="id" property="id"/>
        <result column="last_name" property="lastName"/>
        <result column="email" property="email"/>
        <result column="gender" property="gender"/>
        <!-- association定义关联对象的封装规则
            select:表明当前属性是调用select指定的方法查出的结果
            column:指定将哪一列的值传给这个方法

            流程:使用select指定的方法(传入column指定的这列参数的值)查出对象,并封装给property指定的属性
         -->
        <association property="dept" 
            select="com.mybatis.dao.DepartmentMapper.getDeptById"
            column="d_id">
        </association>
     </resultMap>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

Collection——集合类型

  collection定义关联集合类型的属性的封装规则 。
场景二:
  查询部门的时候将部门对应的所有员工信息也查询出来。
  

public class Department {

    private Integer id;
    private String departmentName;
    private List<Employee> emps;


    public Department() {
    }

    public Department(Integer id, String departmentName) {
        this.id = id;
        this.departmentName = departmentName;
    }

    public Department(Integer id, String departmentName, List<Employee> emps) {
        this.id = id;
        this.departmentName = departmentName;
        this.emps = emps;
    }

    public List<Employee> getEmps() {
        return emps;
    }
    public void setEmps(List<Employee> emps) {
        this.emps = emps;
    }
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getDepartmentName() {
        return departmentName;
    }
    public void setDepartmentName(String departmentName) {
        this.departmentName = departmentName;
    }
    @Override
    public String toString() {
        return "Department [id=" + id + ", departmentName=" + departmentName
                + "]";
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
    <select id="getDeptByIdPlus" resultMap="MyDept">
        SELECT d.dept_id did,d.dept_name dept_name,
                e.id eid,e.last_name last_name,e.email email,e.gender gender
        FROM tbl_dept d
        LEFT JOIN tbl_employee e
        ON d.dept_id=e.d_id
        WHERE d.dept_id=#{id}
    </select>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
    <!--嵌套结果集的方式,使用collection标签定义关联的集合类型的属性封装规则  -->
    <resultMap type="com.mybatis.bean.Department" id="MyDept">
        <id column="did" property="id"/>
        <result column="dept_name" property="departmentName"/>
        <!-- 
            collection定义关联集合类型的属性的封装规则 
            ofType:指定集合里面元素的类型
        -->
        <collection property="emps" ofType="com.mybatis.bean.Employee">
            <!-- 定义这个集合中元素的封装规则 -->
            <id column="eid" property="id"/>
            <result column="last_name" property="lastName"/>
            <result column="email" property="email"/>
            <result column="gender" property="gender"/>
        </collection>
    </resultMap>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

discriminator——鉴别器

   鉴别器:mybatis可以使用discriminator判断某列的值,然后根据某列的值改变封装行为封装Employee。
  
场景三:
   根据id查询员工信息,如果查出的是女生,就把部门信息查询出来。如果是男生,不查询部门信息,且把last_name这一列的值赋值给email。
  

   <resultMap type="com.mybatis.bean.Employee" id="MyEmpDis">
        <id column="id" property="id"/>
        <result column="last_name" property="lastName"/>
        <result column="email" property="email"/>
        <result column="gender" property="gender"/>
        <!--
            鉴别器:mybatis可以使用discriminator判断某列的值,然后根据某列的值改变封装行为
            column:指定判定的列名
            javaType:列值对应的java类型  -->
        <discriminator javaType="string" column="gender">
            <!--女生  resultType:指定封装的结果类型;不能缺少。/resultMap-->
            <case value="0" resultType="com.mybatis.bean.Employee">
                <!--
                    使用association进行分步查询:
                    select:表明当前属性是调用select指定的方法查出的结果
                    column:指定将哪一列的值传给这个方法
                -->
                <association property="dept"
                             select="com.mybatis.dao.DepartmentMapper.getDeptById"
                             column="d_id">
                </association>
            </case>
            <!--男生 ;如果是男生,把last_name这一列的值赋值给email; -->
            <case value="1" resultType="com.mybatis.bean.Employee">
                <id column="id" property="id"/>
                <result column="last_name" property="lastName"/>
                <result column="last_name" property="email"/>
                <result column="gender" property="gender"/>
            </case>
        </discriminator>
    </resultMap>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/241478
推荐阅读
相关标签
  

闽ICP备14008679号