IT评测·应用市场-qidao123.com技术社区

标题: 【MyBatis】MyBatis 操作数据库 [打印本页]

作者: 一给    时间: 2025-4-6 13:20
标题: 【MyBatis】MyBatis 操作数据库


本篇文章,重要包罗以下几部分:
1. 使用MyBatis完成简单的增删改查操作, 参数传递.
2. 把握MyBatis的两种写法: 注解和XML方式
3. 把握MyBatis相干的日志配置

前言

在应用分层学习时, 我们了解到web应用程序⼀般分为三层,即:Controller、Service、Dao . 之前的案例中,请求流程如下: 欣赏器发起请求, 先请求Controller, Controller接收到请求之后, 调用Service进行业务逻辑处置惩罚, Service再调用Dao, 但是Dao层的数据是Mock的, 真实的数据应该从数据库中读取. 我们学习MySQL数据库时,已经学习了JDBC来操作数据库, 但是JDBC操作太复杂了
JDBC 操作示例回顾
下⾯的⼀个完存案例,展示了通过 JDBC 的 API 向数据库中添加⼀条记录,修改⼀条记录,查询⼀条记录的操作。
  1. -- 创建数据库
  2. create database if not exists library default character set utf8mb4;
  3. -- 使⽤数据库
  4. use library;
  5. -- 创建表
  6. create table if not exists soft_bookrack (
  7. book_name varchar(32) NOT NULL,
  8. book_author varchar(32) NOT NULL,
  9. book_isbn varchar(32) NOT NULL primary key
  10. );
复制代码
以下是 JDBC 操作的具体实现代码:
  1. package com.example.demo.mapper;
  2. import javax.sql.DataSource;
  3. import java.sql.Connection;
  4. import java.sql.PreparedStatement;
  5. import java.sql.ResultSet;
  6. import java.sql.SQLException;
  7.     public class SimpleJdbcOperation {
  8.         private final DataSource dataSource;
  9.         public SimpleJdbcOperation(DataSource dataSource) {
  10.             this.dataSource = dataSource;
  11.         }
  12.         /**
  13.          * 添加⼀本书
  14.          */
  15.         public void addBook() {
  16.             Connection connection = null;
  17.             PreparedStatement stmt = null;
  18.             try {
  19. //获取数据库连接
  20.                 connection = dataSource.getConnection();
  21.                 //创建语句
  22.                 stmt = connection.prepareStatement(
  23.                         "insert into soft_bookrack (book_name, book_author,
  24.                         book_isbn) values( ?,?,?);
  25.                 "
  26. );
  27.                 //参数绑定
  28.                 stmt.setString(1, "Spring in Action");
  29.                 stmt.setString(2, "Craig Walls");
  30.                 stmt.setString(3, "9787115417305");
  31.                 //执⾏语句
  32.                 stmt.execute();
  33.             } catch (SQLException e) {
  34.                 //处理异常信息
  35.             } finally {
  36.                 //清理资源
  37.                 try {
  38.                     if (stmt != null) {
  39.                         stmt.close();
  40.                     }
  41.                     if (connection != null) {
  42.                         connection.close();
  43.                     }
  44.                 } catch (SQLException e) {
  45.                     //
  46.                 }
  47.             }
  48.         }
  49.         /**
  50.          * 更新⼀本书
  51.          */
  52.         public void updateBook() {
  53.             Connection connection = null;
  54.             PreparedStatement stmt = null;
  55.             try {
  56.                         //获取数据库连接 connection = dataSource.getConnection();
  57.                         //创建语句 stmt = connection.prepareStatement(
  58.                 "update soft_bookrack set book_author=? where book_isbn=?;"
  59. );
  60.                                 //参数绑定 stmt.setString(1, "张卫滨");
  61.                 stmt.setString(2, "9787115417305");
  62.                                 //执⾏语句 stmt.execute(); } catch (SQLException e) {
  63.                                 //处理异常信息 } finally {
  64.                                 //清理资源
  65.                 try {
  66.                     if (stmt != null) {
  67.                         stmt.close();
  68.                     }
  69.                     if (connection != null) {
  70.                         connection.close();
  71.                     }
  72.                 } catch (SQLException e) {
  73.                     //
  74.                 }
  75.             }
  76.         }
  77.         /**
  78.          * 查询⼀本书
  79.          */
  80.         public void queryBook() {
  81.             Connection connection = null;
  82.             PreparedStatement stmt = null;
  83.             ResultSet rs = null;
  84.             Book book = null;
  85.             try {
  86.                 //获取数据库连接
  87.                 connection = dataSource.getConnection();
  88.                 //创建语句
  89.                 stmt = connection.prepareStatement(
  90.                         "select book_name, book_author, book_isbn from
  91.                         soft_bookrack where book_isbn = ? "
  92. );
  93.                 //参数绑定
  94.                 stmt.setString(1, "9787115417305");
  95.                 //执⾏语句
  96.                 rs = stmt.executeQuery();
  97.                 if (rs.next()) {
  98.                     book = new Book();
  99.                     book.setName(rs.getString("book_name"));
  100.                     book.setAuthor(rs.getString("book_author"));
  101.                     book.setIsbn(rs.getString("book_isbn"));
  102.                 }
  103.                 System.out.println(book);
  104.             } catch (SQLException e) {
  105.                 //处理异常信息
  106.             } finally {
  107.                 //清理资源
  108.                 try {
  109.                     if (rs != null) {
  110.                         rs.close();
  111.                     }
  112.                     if (stmt != null) {
  113.                         stmt.close();
  114.                     }
  115.                     if (connection != null) {
  116.                         connection.close();
  117.                     }
  118.                 } catch (SQLException e) {
  119.                         //处理异常信息
  120.                 }
  121.             }
  122.         }
  123.         public static class Book {
  124.             private String name;
  125.             private String author;
  126.             private String isbn;
  127.             //省略 setter getter ⽅法
  128.         }
  129.     }
复制代码
从上述代码和操作流程可以看出,对于 JDBC 来说,整个操作⾮常的繁琐,我们不但要拼接每⼀个参 数,⽽且还要按照模板代码的⽅式,一步步的操作数据库,而且在每次操作完,还要⼿动关闭毗连 等,而所有的这些操作步调都必要在每个⽅法中重复书写. 那有没有⼀种⽅法,可以更简单、更⽅便的 操作数据库呢? 答案是肯定的,这就是我们要学习 MyBatis 的真正缘故原由,它可以帮助我们更方便、更快速的操作数据库.
一、什么是MyBatis?

   • MyBatis是⼀款良好的 持久层框架,用于简化JDBC的开发。
• MyBatis本是 Apache的⼀个开源项目iBatis,2010年这个项目由apache迁移到了google code,而且改名为MyBatis 。2013年11月迁移到Github。
  在上⾯我们提到⼀个词:持久层
• 持久层:指的就是持久化操作的层, 通常指数据访问层(dao), 是用来操作数据库的.

简单来说 MyBatis 是更简单完成程序和数据库交互的框架,也就是更简单的操作和读取数据库工具接下来,我们就通过⼀个入门程序,让各人感受⼀下通过Mybatis如何来操作数据库
二、MyBatis入门

Mybatis操作数据库的步调:
2.1、准备工作

2.1.1 创建工程

创建springboot工程,并导入mybatis的起步依靠、mysql的驱动包

   Mybatis 是⼀个持久层框架, 具体的数据存储和数据操作还是在MySQL中操作的, 所以必要添加MySQL驱动
  项目工程创建完成后,主动在pom.xml⽂件中,导⼊Mybatis依靠和MySQL驱动依靠
  1. <!--Mybatis 依赖包-->
  2. <dependency>
  3. <groupId>org.mybatis.spring.boot</groupId>
  4. <artifactId>mybatis-spring-boot-starter</artifactId>
  5. <version>2.3.1</version>
  6. </dependency>
  7. <!--mysql驱动包-->
  8. <dependency>
  9. <groupId>com.mysql</groupId>
  10. <artifactId>mysql-connector-j</artifactId>
  11. <scope>runtime</scope>
  12. </dependency>
复制代码
2.1.2、数据准备

  1. -- 创建数据库
  2. DROP DATABASE IF EXISTS mybatis_test;
  3. CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4;
  4. -- 使⽤数据数据
  5. USE mybatis_test;
  6. -- 创建表[⽤⼾表]
  7. DROP TABLE IF EXISTS userinfo;
  8. CREATE TABLE `userinfo` (
  9. `id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
  10. `username` VARCHAR ( 127 ) NOT NULL,
  11. `password` VARCHAR ( 127 ) NOT NULL,
  12. `age` TINYINT ( 4 ) NOT NULL, `gender` TINYINT ( 4 ) DEFAULT '0' COMMENT '1-男 2-⼥ 0-默认',
  13. `phone` VARCHAR ( 15 ) DEFAULT NULL,
  14. `delete_flag` TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',
  15. `create_time` DATETIME DEFAULT now(),
  16. `update_time` DATETIME DEFAULT now(),
  17. PRIMARY KEY ( `id` )
  18. ) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
  19. -- 添加⽤⼾信息
  20. INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
  21. VALUES ( 'admin', 'admin', 18, 1, '18612340001' );
  22. INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
  23. VALUES ( 'zhangsan', 'zhangsan', 18, 1, '18612340002' );
  24. INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
  25. VALUES ( 'lisi', 'lisi', 18, 1, '18612340003' );
  26. INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
  27. VALUES ( 'wangwu', 'wangwu', 18, 1, '18612340004' );
复制代码
创建对应的实体类 UserInfo
   实体类的属性名与表中的字段名⼀⼀对应
  1. import lombok.Data;
  2. import java.util.Date;
  3. @Data
  4. public class UserInfo {
  5. private Integer id;
  6. private String username;
  7. private String password;
  8. private Integer age;
  9. private Integer gender;
  10. private String phone;
  11. private Integer deleteFlag;
  12. private Date createTime;
  13. private Date updateTime;
  14. }
复制代码
2.2、配置数据库毗连字符串

Mybatis中要毗连数据库,必要数据库相干参数配置
   • MySQL驱动类
• 登录名
• 密码
• 数据库毗连字符串
  如果是application.yml⽂件, 配置内容如下
  1. # 数据库连接配置
  2. spring:
  3. datasource:
  4. url: jdbc:mysql://127.0.0.1:3306/mybatis_test?
  5. characterEncoding=utf8&useSSL=false
  6. username: root
  7. password: root
  8. driver-class-name: com.mysql.cj.jdbc.Driver
复制代码
  留意事项: 如果使用MySQL 是 5.x 之前的使用的是"com.mysql.jdbc.Driver",如果是⼤于 5.x 使用的是“com.mysql.cj.jdbc.Driver”.
  如果是application.properties文件, 配置内容如下:
  1. #驱动类名称
  2. spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  3. #数据库连接的url
  4. spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mybatis_test?
  5. characterEncoding=utf8&useSSL=false
  6. #连接数据库的⽤⼾名
  7. spring.datasource.username=root
  8. #连接数据库的密码
  9. spring.datasource.password=root
复制代码
2.3、写持久层代码

在项目中, 创建持久层接口UserInfoMapper

在UserInfo接口中编写以下代码
  1. import com.example.demo.model.UserInfo;
  2. import org.apache.ibatis.annotations.Mapper;
  3. import org.apache.ibatis.annotations.Select;
  4. import java.util.List;
  5. @Mapper
  6. public interface UserInfoMapper {
  7. //查询所有⽤⼾
  8. @Select("select username, `password`, age, gender, phone from userinfo")
  9. public List<UserInfo> queryAllUser();
  10. }
复制代码
  Mybatis的持久层接口规范⼀般都叫 XxxMapper
@Mapper注解:表示是MyBatis中的Mapper接口
• 程序运行时, 框架会主动生成接口的实现类对象(代理对象),并给交Spring的IOC容器管理
• @Select注解:代表的就是select查询,也就是注解对应方法的具体实现内容
  2.4 单位测试

在创建出来的SpringBoot⼯程中,在src下的test目录下,已经目动帮我们创建好了测试类 ,我们可以直接使用这个测试类来进行测试
  1. import com.example.demo.mapper.UserInfoMapper;
  2. import com.example.demo.model.UserInfo;
  3. import org.junit.jupiter.api.Test;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.boot.test.context.SpringBootTest;
  6. import java.util.List;
  7.     @SpringBootTest
  8.     class DemoApplicationTests {
  9.         @Autowired
  10.         private UserInfoMapper userInfoMapper;
  11.         @Test
  12.         void contextLoads() {
  13.             List<UserInfo> userInfoList = userInfoMapper.queryAllUser();
  14.             System.out.println(userInfoList);
  15.         }
  16.     }
复制代码
  测试类上添加了注解 @SpringBootTest,该测试类在运⾏时,就会⾃动加载Spring的运行情况. 我们通过@Autowired这个注解, 注入我们要测试的类, 就可以开始进行测试了
  运行结果如下:

返回结果中, 可以看到, 只有SQL语句中查询的列对应的属性才有赋值
三、MyBatis的基础操作

上⾯我们学习了Mybatis的查询操作, 接下来我们学习MyBatis的增, 删, 改操作
在学习这些操作之前, 我们先来学习MyBatis日志打印
3.1 打印日志

在Mybatis当中我们可以借助日志, 查看到sql语句的执行、执行传递的参数以及执行结果 在配置文件中进行配置即可
  1. mybatis:
  2.   configuration: # 配置打印 MyBatis⽇志
  3.           log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
复制代码
如果是application.properties, 配置内容如下:
  1. #指定mybatis输出⽇志的位置, 输出控制台
  2. mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
复制代码
留意: 后续配置项, 默认只提供⼀种, 请自行进行配置项转换
重新运行程序, 可以看到SQL执行内容, 以及传递参数和执行结果

   ①: 查询语句
②: 传递参数及类型
③: SQL执行结果
  3.2、参数传递

需求: 查找id=4的用户,对应的SQL就是: select * from userinfo where id=4
  1. @Select("select username, `password`, age, gender, phone from userinfo where id= 4 ")
  2. UserInfo queryById();
复制代码
但是这样的话, 只能查找id=4 的数据, 所以SQL语句中的id值不能写成固定数值,必要变为动态的数值解决方案:在queryById方法中添加⼀个参数(id),将⽅法中的参数,传给SQL语句 使用 #{} 的方式获取方法中的参数
  1. @Select("select username, `password`, age, gender, phone from userinfo where id= #{id} ")
  2. UserInfo queryById(Integer id);
复制代码
  如果mapper接口方法形参只有⼀个普通类型的参数,#{…} ⾥⾯的属性名可以随便写,如:#{id}、#{value}。建媾和参数名保持一致
  添加测试用例
  1. @Test
  2. void queryById() {
  3. UserInfo userInfo = userInfoMapper.queryById(4);
  4. System.out.println(userInfo);
  5. }
复制代码
运行结果:

也可以通过 @Param , 设置参数的别名, 如果使用@Param 设置别名, #{…}内里的属性名必须和@Param 设置的⼀样
  1. @Select("select username, `password`, age, gender, phone from userinfo where id= #{userid} ")
  2. UserInfo queryById(@Param("userid") Integer id);
复制代码
3.3、增(Insert)

SQL 语句:
  1. insert into userinfo (username, `password`, age, gender, phone) values("zhaoliu","zhaoliu",19,1,"18700001234")
复制代码
把SQL中的常量更换为动态的参数
Mapper接口:
  1. @Insert("insert into userinfo (username, `password`, age, gender, phone) values (#{username},#{password},#{age},#{gender},#{phone})")
  2. Integer insert(UserInfo userInfo);
复制代码
直接使用UserInfo对象的属性名来获取参数
测试代码:
  1. @Test
  2. void insert() {
  3. UserInfo userInfo = new UserInfo();
  4. userInfo.setUsername("zhaoliu");
  5. userInfo.setPassword("zhaoliu");
  6. userInfo.setGender(2);
  7. userInfo.setAge(21);
  8. userInfo.setPhone("18612340005");
  9. userInfoMapper.insert(userInfo);
  10. }
复制代码
运行后, 观察数据库执行结果
如果设置了 @Param 属性, #{…} 必要使用参数属性来获取
  1. @Insert("insert into userinfo (username, `password`, age, gender, phone) values (#{userinfo.username},#{userinfo.password},#{userinfo.age},#{userinfo.gender},#{userinfo.phone})")
  2. Integer insert(@Param("userinfo") UserInfo userInfo);
复制代码
返回主键
Insert 语句默认返回的是受影响的行数,但有些情况下, 数据插入之后, 还必要有后续的关联操作, 必要获取到新插入数据的id
   ⽐如订单体系
当我们下完订单之后, 必要关照物流体系, 库存体系, 结算体系等, 这时间就必要拿到订单ID
  如果想要拿到自增id, 必要在Mapper接⼝的⽅法上添加⼀个Options的注解
  1. @Options(useGeneratedKeys = true, keyProperty = "id")
  2. @Insert("insert into userinfo (username, age, gender, phone) values (#{userinfo.username},#{userinfo.age},#{userinfo.gender},#{userinfo.phone})")
  3. Integer insert(@Param("userinfo") UserInfo userInfo);
复制代码
  • useGeneratedKeys:这会令MyBatis使用JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(好比:像 MySQL 和 SQL Server 这样的关系型数据库管理体系的主动递增字段),默认值:false.
• keyProperty:指定可以或许唯⼀识别对象的属性,MyBatis 会使用getGeneratedKeys 的返回值或insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)
  测试数据:
  1.   @Test
  2.     void insert() {
  3.         UserInfo userInfo = new UserInfo();
  4.         userInfo.setUsername("zhaoliu");
  5.         userInfo.setPassword("zhaoliu");
  6.         userInfo.setGender(2);
  7.         userInfo.setAge(21);
  8.         userInfo.setPhone("18612340005");
  9.         Integer count = userInfoMapper.insert(userInfo);
  10.         System.out.println("添加数据条数:" + count + ", 数据ID:" + userInfo.getId());
  11.     }
复制代码
运行结果:

留意: 设置 useGeneratedKeys=true 之后, 方法返回值依然是受影响的行数, 自增id 会设置在上述 keyProperty 指定的属性中.
3.4、 删(Delete)

SQL语句:
  1. delete from userinfo where id=6
复制代码
把SQL中的常量更换为动态的参数
Mapper接口:
  1. @Delete("delete from userinfo where id = #{id}")
  2. void delete(Integer id);
复制代码
3.5、改(Update)

SQL 语句:
  1. update userinfo set username="zhaoliu" where id=5
复制代码
把SQL中的常量更换为动态的参数
Mapper接口:
  1. @Update("update userinfo set username=#{username} where id=#{id}")
  2. void update(UserInfo userInfo);
复制代码
3.6、查(Select)

我们在上⾯查询时发现, 有几个字段是没有赋值的, 只有Java对象属性和数据库字段⼀模⼀样时, 才会进行赋值 接下来我们多查询⼀些数据
  1. @Select("select id, username, `password`, age, gender, phone, delete_flag, create_time, update_time from userinfo")
  2. List<UserInfo> queryAllUser();
复制代码
查询结果:

从运行结果上可以看到, 我们SQL语句中, 查询了delete_flag, create_time, update_time, 但是这几个属性却没有赋值
   MyBatis 会根据方法的返回结果进行赋值.
方法用对象 UserInfo接收返回结果, MySQL 查询出来数据为⼀条, 就会主动赋值给对象.
方法用List接收返回结果, MySQL 查询出来数据为⼀条或多条时, 也会主动赋值给List.
但如果MySQL 查询返回多条, 但是方法使⽤UserInfo接收, MyBatis执行就会报错
  缘故原由分析:
当主动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性(忽略巨细写)。这意味着如果发现了 ID 列和 id 属性,MyBatis 会将列 ID 的值赋给 id 属性
解决办法:
3.6.1、起别名

在SQL语句中,给列名起别名,保持别名和实体类属性名⼀样
  1. @Select("select id, username, `password`, age, gender, phone, delete_flag as deleteFlag,create_time as createTime, update_time as updateTime from userinfo")
  2. public List<UserInfo> queryAllUser();
复制代码
3.6.2 结果映射

  1.    @Select("select id, username, `password`, age, gender, phone, delete_flag,
  2.             create_time, update_time from userinfo")
  3.             @Results({
  4.             @Result(column = "delete_flag", property = "deleteFlag"),
  5.     @Result(column = "create_time", property = "createTime"),
  6.     @Result(column = "update_time", property = "updateTime")
  7. })
  8.         List<UserInfo> queryAllUser();
复制代码
如果其他SQL, 也盼望可以复用这个映射关系, 可以给这个Results界说⼀个名称
  1. @Select("select id, username, `password`, age, gender, phone, delete_flag,
  2. create_time, update_time from userinfo")
  3. @Results(id = "resultMap",value = {
  4. @Result(column = "delete_flag",property = "deleteFlag"),
  5. @Result(column = "create_time",property = "createTime"),
  6. @Result(column = "update_time",property = "updateTime")
  7. })
  8. List<UserInfo> queryAllUser();
  9. @Select("select id, username, `password`, age, gender, phone, delete_flag, create_time, update_time " +
  10. "from userinfo where id= #{userid} ")
  11. @ResultMap(value = "resultMap")
  12. UserInfo queryById(@Param("userid") Integer id);
复制代码
3.6.3 开启驼峰命名

通常数据库列使用蛇形命名法进行命名(下划线分割各个单词), 而Java 属性⼀般遵照驼峰命名法约定.为了在这两种命名方式之间启用主动映射,必要将 mapUnderscoreToCamelCase 设置为 true。
  1. mybatis:
  2. configuration:
  3.   map-underscore-to-camel-case: true #配置驼峰⾃动转换
复制代码
驼峰命名规则: abc_xyz => abcXyz
• 表中字段名:abc_xyz
• 类中属性名:abcXyz
Java 代码不做任那边置惩罚
  1. @Select("select id, username, `password`, age, gender, phone, delete_flag as deleteFlag, create_time as createTime, update_time as updateTime from userinfo")
  2. public List<UserInfo> queryAllUser();
复制代码
添加上述配置, 运行代码:

四、MaBatis XML配置文件

Mybatis的开发有两种方式:
上面学习了注解的方式, 接下来我们学习XML的方式
   使用Mybatis的注解方式,重要是来完成⼀些简单的增删改查功能. 如果必要实现复杂的SQL功能,建议使用XML来配置映射语句,也就是将SQL语句写在XML配置文件中.
  MyBatis XML的方式必要以下两步:
4.1、配置毗连字符串和MyBatis

此步调必要进行两项设置,数据库毗连字符串设置和 MyBatis 的XML文件配置。
如果是application.yml文件, 配置内容如下:
  1. # 数据库连接配置
  2. spring:
  3. datasource:
  4.   url: jdbc:mysql://127.0.0.1:3306/mybatis_test?
  5. characterEncoding=utf8&useSSL=false
  6. username: root
  7. password: root
  8. driver-class-name: com.mysql.cj.jdbc.Driver
  9. # 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件
  10. mybatis:
  11. mapper-locations: classpath:mapper/**Mapper.xml
复制代码
如果是application.properties⽂件, 配置内容如下:
  1. #驱动类名称
  2. spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  3. #数据库连接的url
  4. spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mybatis_test?
  5. characterEncoding=utf8&useSSL=false
  6. #连接数据库的⽤⼾名
  7. spring.datasource.username=root
  8. #连接数据库的密码
  9. spring.datasource.password=root
  10. # 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件mybatis.mapper-locations=classpath:mapper/**Mapper.xml
复制代码
4.2、写持久层代码

持久层代码分两部分

4.2.1、添加 mapper 接口

数据持久层的接口界说:
  1. import com.example.demo.model.UserInfo;
  2. import org.apache.ibatis.annotations.Mapper;
  3. import java.util.List;
  4. @Mapper
  5. public interface UserInfoXMlMapper {
  6. List<UserInfo> queryAllUser();
  7. }
复制代码
4.2.1、添加 UserInfoXMLMapper.xml

数据持久成的实现,MyBatis 的固定 xml 格式:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  3. "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.mapper.UserInfoMapper">
  4. </mapper>
复制代码
创建UserInfoXMLMapper.xml, 路径参考yml中的配置

查询所有用户的具体实现 :
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC"-//mybatis.org//DTD Mapper 3.0//EN"
  3.         "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.demo.mapper.UserInfoXMlMapper">
  4. <select id="queryAllUser" resultType="com.example.demo.model.UserInfo">
  5.         select username,`password`,age,gender,phone from userinfo
  6. </select>
  7. </mapper>
复制代码
以下是对以上标签的阐明:
• 标签:必要指定 namespace 属性,表示命名空间,值为 mapper 接⼝的全限定名,包括全包名.类名。
• 查询标签:是用来执行数据库的查询操作的:
◦ id :是和 Interface (接口)中界说的方法名称⼀样的,表示对接口的具体实现方法。
◦ resultType :是返回的数据类型,也就是开头我们界说的实体类

4.2.3、单位测试

  1. @SpringBootTest
  2. class UserInfoMapperTest {
  3.     @Autowired
  4.     private UserInfoMapper userInfoMapper;
  5.     @Test
  6.     void queryAllUser() {
  7.         List<UserInfo> userInfoList = userInfoMapper.queryAllUser();
  8.         System.out.println(userInfoList);
  9.     }
  10. }
复制代码
运行结果如下:

4.3 增删改查操作

接下来,我们来实现⼀下用户的增长、删除和修改的操作.
4.3.1、增(Insert)

UserInfoMapper接口:
  1. Integer insertUser(UserInfo userInfo);
复制代码
UserInfoMapper.xml实现:
  1. <insert id="insertUser">
  2. insert into userinfo (username, `password`, age, gender, phone) values (#
  3. {username}, #{password}, #{age},#{gender},#{phone})
  4. </insert>
复制代码
如果使用@Param设置参数名称的话, 使用方法和注解类似
UserInfoMapper接口:
  1. Integer insertUser(@Param("userinfo") UserInfo userInfo)
复制代码
UserInfoMapper.xml实现:
  1. <insert id="insertUser">
  2. insert into userinfo (username, `password`, age, gender, phone) values(#{userinfo.username},#{userinfo.password},#{userinfo.age},
  3. #{userinfo.gender},#{userinfo.phone})
  4. </insert>
复制代码
返回自增 id
接口界说稳定, Mapper.xml 实现 设置useGeneratedKeys 和keyProperty属性
  1. <insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
  2. insert into userinfo (username, `password`, age, gender, phone) values
  3. (#{userinfo.username},#{userinfo.password},#{userinfo.age},#
  4. {userinfo.gender},#{userinfo.phone})
  5. </insert>
复制代码
4.3.2 删(Delete)

UserInfoMapper接口:
  1. Integer deleteUser(Integer id);
复制代码
UserInfoMapper.xml实现:
  1. <delete id="deleteUser">
  2. delete from userinfo where id = #{id}
  3. </delete>
复制代码
4.3.3 改(Update)

UserInfoMapper接口:
  1. Integer updateUser(UserInfo userInfo);
复制代码
UserInfoMapper.xml实现:
  1. <update id="updateUser">
  2. update userinfo set username=#{username} where id=#{id}
  3. </update>
复制代码
4.3.4 查(Select)

同样的, 使用XML 的方式进行查询, 也存在数据封装的问题 我们把SQL语句进行简单修改, 查询更多的字段内容
  1. <select id="queryAllUser" resultType="com.example.demo.model.UserInfo">
  2. select id, username,`password`, age, gender, phone, delete_flag,
  3. create_time, update_time from userinfo
  4. </select>
复制代码
运行结果:

结果表现: deleteFlag, createTime, updateTime 也没有进行赋值
解决办法和注解类似:
. 起别名
. 结果映射
. 开启驼峰命名
此中1,3的解决办法和注解⼀样,不再多说, 接下来看下xml如果来写结果映射:
Mapper.xml:
  1. <resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
  2. <id column="id" property="id"></id>
  3. <result column="delete_flag" property="deleteFlag"></result>
  4. <result column="create_time" property="createTime"></result>
  5. <result column="update_time" property="updateTime"></result>
  6. </resultMap><select id="queryAllUser" resultMap="BaseMap">
  7.         select id,username,`password`,age,gender,phone,delete_flag,
  8.         create_time,update_time from userinfo
  9. </select>
复制代码

五、其他查询操作

5.1 多表查询

多表查询和单表查询类似, 只是SQL差别而已
5.1.1、准备工作

上面建了⼀张用户表, 我们再来建⼀张文章表, 进行多表关联查询. 文章表的uid, 对应用户表的id.
数据准备:
  1. -- 创建⽂章表
  2. DROP TABLE IF EXISTS articleinfo;
  3. CREATE TABLE articleinfo (
  4. id INT PRIMARY KEY auto_increment,
  5. title VARCHAR ( 100 ) NOT NULL,
  6. content TEXT NOT NULL,
  7. uid INT NOT NULL,
  8. delete_flag TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',
  9. create_time DATETIME DEFAULT now(),
  10. update_time DATETIME DEFAULT now()
  11. ) DEFAULT charset 'utf8mb4';
  12. -- 插⼊测试数据
  13. INSERT INTO articleinfo ( title, content, uid ) VALUES ( 'Java', 'Java正⽂', 1
  14. );
复制代码
对应Model:
  1. import lombok.Data;
  2. import java.util.Date;
  3. @Data
  4. public class ArticleInfo {
  5. private Integer id;
  6. private String title;
  7. private String content;
  8. private Integer uid;
  9. private Integer deleteFlag;
  10. private Date createTime;
  11. private Date updateTime;
  12. }
复制代码
5.1.2、数据查询

需求: 根据uid查询作者的名称等相干信息
SQL:
  1. SELECT
  2. ta.id,
  3. ta.title,
  4. ta.content,
  5. ta.uid,
  6. tb.username,
  7. tb.age,
  8. tb.gender
  9. FROM
  10. articleinfo ta
  11. LEFT JOIN userinfo tb ON ta.uid = tb.id
  12. WHERE
  13. ta.id =1
复制代码
补充实体类:
  1. @Data
  2. public class ArticleInfo {
  3.     private Integer id;
  4.     private String title;
  5.     private String content;
  6.     private Integer uid;
  7.     private Integer deleteFlag;
  8.     private Date createTime;
  9.     private Date updateTime;
  10.     //⽤⼾相关信息
  11.     private String username;
  12.     private Integer age;
  13.     private Integer gender;
  14. }
复制代码
接口界说:
  1. import com.example.demo.model.ArticleInfo;
  2. import org.apache.ibatis.annotations.Mapper;
  3. @Mapper
  4. public interface ArticleInfoMapper {
  5.     @Select("SELECT
  6.             ta.id, ta.title, ta.content, ta.uid, tb.username, tb.age, tb.gender" +
  7.             "FROM articleinfo ta LEFT JOIN userinfo tb ON ta.uid = tb.id "+
  8.             "WHERE ta.id = #{id}")
  9.     ArticleInfo queryUserByUid(Integer id);
  10. }
复制代码
  如果名称不⼀致的, 接纳ResultMap, 大概别名的⽅式解决, 和单表查询⼀样Mybatis 不分单表还是多表, 重要就是三部分: SQL, 映射关系和实体类
通过映射关系, 把SQL运行结果和实体类关联起来.
  5.2、#{} 和 ${}

MyBatis 参数赋值有两种方式, 咱们前⾯使用了 #{} 进行赋值, 接下来我们看下二者的区别
5.2.1 #{} 和${} 使用

  1. @Select("select username, `password`, age, gender, phone from userinfo where id= #{id} ")
  2. UserInfo queryById(Integer id);
复制代码
观察我们打印的日志

发现我们输出的SQL语句:
  1. select username, `password`, age, gender, phone from userinfo where id= ?
复制代码
我们输⼊的参数并没有在后面拼接,id的值是使用 ? 进行占位. 这种SQL 我们称之为"预编译SQL"
   MySQL 课程 JDBC编程使用的就是预编译SQL, 此处不再多说.
  我们把 #{} 改成 ${} 再观察打印的日志:
  1. @Select("select username, `password`, age, gender, phone from userinfo where id= ${id} ")
  2. UserInfo queryById(Integer id);
复制代码

可以看到, 这次的参数是直接拼接在SQL语句中了.
2. 接下来我们再看String类型的参数
  1. @Select("select username, `password`, age, gender, phone from userinfo where username= #{name} ")
  2. UserInfo queryByName(String name);
复制代码
观察我们打印的日志, 结果正常返回:

我们把 #{} 改成 ${} 再观察打印的日志:
  1. @Select("select username, `password`, age, gender, phone from userinfo where username= ${name} ")
  2. UserInfo queryByName(String name);
复制代码

可以看到, 这次的参数依然是直接拼接在SQL语句中了, 但是字符串作为参数时, 必要添加引号 ‘’ , 使 ⽤ ${} 不会拼接引号 ‘’ , 导致程序报错.
修改代码如下:
  1. @Select("select username, `password`, age, gender, phone from userinfo where username= '${name}' ")
  2. UserInfo queryByName(String name);
复制代码
再次运行, 结果正常返回

从上面两个例子可以看出:
#{} 使用的是预编译SQL, 通过 ? 占位的方式, 提前对SQL进行编译, 然后把参数填充到SQL语句 中. #{} 会根据参数类型, 主动拼接引号 ‘’ .
${} 会直接进行字符更换, ⼀起对SQL进行编译. 如果参数为字符串, 必要加上引号 ‘’
   参数为数字类型时, 也可以加上, 查询结果稳定, 但是大概会导致索引失效, 性能降落
  5.2.2、#{} 和 ${}区别

#{} 和 ${} 的区别就是预编译SQL和即时SQL的区别
   简单回顾:
当客户发送⼀条SQL语句给服务器后, 大致流程如下:
1.解析语法和语义, 校验SQL语句是否正确
2.优化SQL语句, 订定执行操持
3.执行并返回结果 、
⼀条 SQL如果走上述流程处置惩罚, 我们称之为 Immediate Statements(即时 SQL)
  

预编译SQL,编译⼀次之后会将编译后的SQL语句缓存起来,后⾯再次执行这条语句时,不会再次编译 (只是输入的参数差别), 省去了解析优化等过程, 以此来提高服从
   由于没有对用户输⼊进行充实检查,而SQL⼜是拼接而成,在用户输入参数时,在参数中添加⼀些SQL关键字,达到改变SQL运行结果的目的,也可以完成恶意攻击
  sql 注⼊代码: ’ or 1='1
先来看看SQL注入的例子:
  1. @Select("select username, `password`, age, gender, phone from userinfo where username= '${name}' ")
  2. List<UserInfo> queryByName(String name);
复制代码
测试代码:
正常访问情况:
  1. @Test
  2. void queryByName() {
  3. List<UserInfo> userInfos = userInfoMapper.queryByName("admin");
  4. System.out.println(userInfos);
  5. }
复制代码
结果运行正常

SQL注入场景:
  1. @Test
  2. void queryByName() {
  3.         List<UserInfo> userInfos = userInfoMapper.queryByName("' or 1='1");
  4.         System.out.println(userInfos);
  5. }
复制代码
结果依然被正确查询出来了, 此中参数 or被当做了SQL语句的⼀部分

可以看出来, 查询的数据并不是自己想要的数据. 所以用于查询的字段,只管使用 #{} 预查询的方式
   SQL注⼊是⼀种⾮常常见的数据库攻击本事, SQL注入漏洞也是网络世界中最普遍的漏洞之⼀. 如果发生在用户登录的场景中, 密码输人为 ’ or 1='1 , 就大概完成登录(不是⼀定会发生的场景, 必要看登录代码如何写)
  5.3、排序功能

从上面的例子中, 可以得出结论: ${} 会有SQL注⼊的风险, 所以我们只管使用#{}完成查询
既然云云, 是不是 ${} 就没有存在的必要性了呢?
当然不是
接下来我们看下${}的使⽤场景

Mapper实现
  1. @Select("select id, username, age, gender, phone, delete_flag, create_time,
  2. update_time " +
  3. "from userinfo order by id ${sort} ")
  4. List<UserInfo> queryAllUserBySort(String sort);
复制代码
使用 ${sort} 可以实现排序查询, 而使用 #{sort} 就不能实现排序查询了.
   留意: 此处 sort 参数为String类型, 但是SQL语句中, 排序规则是不必要加引号 ‘’ 的, 所以此时的${sort} 也不加引号
  我们把 ${} 改成 #{}
  1. @Select("select id, username, age, gender, phone, delete_flag, create_time,
  2. update_time " +
  3. "from userinfo order by id #{sort} ")
  4. List<UserInfo> queryAllUserBySort(String sort);
复制代码
运行结果:

可以发现, 当使用 #{sort} 查询时, asc 前后主动给加了引号, 导致 sql 错误
   #{} 会根据参数类型判断是否拼接引号 ‘’
如果参数类型为String, 就会加上引号
  除此之外, 还有表名作为参数时, 也只能使用 ${}
5.4、like查询

like 使用 #{} 报错
  1. @Select("select id, username, age, gender, phone, delete_flag, create_time,
  2. update_time " +
  3. "from userinfo where username like '%#{key}%' ")
  4. List<UserInfo> queryAllUserByLike(String key);
复制代码
把 #{} 改成 $ {} 可以正确查出来, 但是${}存在SQL注入的问题, 所以不能直接使用 ${}.
解决办法: 使用 mysql 的内置函数 concat() 来处置惩罚,实现代码如下:
  1. @Select("select id, username, age, gender, phone, delete_flag, create_time,
  2. update_time " +
  3. "from userinfo where username like concat('%',#{key},'%')")
  4. List<UserInfo> queryAllUserByLike(String key);
复制代码
六、数据库毗连池

在上⾯Mybatis的讲解中, 我们使⽤了数据库毗连池技术, 避免频繁的创建毗连, 烧毁毗连 ,下面我们来了解下数据库毗连池
6.1、介绍

数据库毗连池负责分配、管理和释放数据库毗连,它允许应⽤程序重复使⽤⼀个现有的数据库毗连,而不是再重新建立⼀个.

没有使用数据库毗连池的情况: 每次执行SQL语句, 要先创建⼀个新的毗连对象, 然后执行SQL语句, SQL语句执行完, 再关闭毗连对象释放资源. 这种重复的创建毗连, 烧毁毗连比较消耗资源
使用数据库毗连池的情况: 程序启动时, 会在数据库毗连池中创建⼀定数量的Connection对象, 当客户请求数据库毗连池, 会从数据库毗连池中获取Connection对象, 然后执行SQL, SQL语句执行完, 再把Connection归还给毗连池
优点:
6.2 使用

常见的数据库毗连池:
• C3P0
• DBCP
• Druid
• Hikari
目前比较流行的是 Hikari, Druid
  1. <dependency>
  2. <groupId>com.alibaba</groupId>
  3. <artifactId>druid-spring-boot-starter</artifactId>
  4. <version>1.1.17</version>
  5. </dependency>
复制代码
运行结果:

   • Druid毗连池是阿里巴巴开源的数据库毗连池项目
• 功能强大,性能良好,是Java语言最好的数据库毗连池之⼀
  七、动态SQL

动态 SQL 是Mybatis的强大特性之⼀,可以或许完成差别条件下差别的 sql 拼接。
7.1、< if >标签

在注册用户的时间,大概会有这样⼀个问题,如下图所示:

注册分为两种字段:必填字段和非必填字段,那如果在添加用户的时间有不确定的字段传入,程序应该如何实现呢? 这个时间就必要使用动态标签来判断了,好比添加的时间性别 gender 为⾮必填字段,具体实现如下:
接口界说:
  1. Integer insertUserByCondition(UserInfo userInfo);
复制代码
Mapper.xml实现:
  1. <insert id="insertUserByCondition">
  2.          INSERT INTO userinfo (
  3.          username,
  4.          `password`,
  5.          age,
  6.          <if test="gender != null">
  7.                  gender,
  8.                  </if>
  9.          phone)
  10.          VALUES (
  11.          #{username},
  12.          #{age},
  13.          <if test="gender != null">
  14.                  #{gender},
  15.          </if>
  16.         #{phone})
  17. </insert>
复制代码
  留意 test 中的 gender,是传⼊对象中的属性,不是数据库字段
Q: 可不可以不进行判断, 直接把字段设置为null呢?
A: 不可以, 这种情况下, 如果gender字段有默认值, 就会设置为默认值
  7.2 、< trim >标签

之前的插入用户功能,只是有⼀个 gender 字段大概是选填项,如果有多个字段,⼀般考虑使用标签结合标签,对多个字段都采取动态生成的方式。
标签中有如部属性:
• prefix:表示整个语句块,以prefix的值作为前缀
• suffix:表示整个语句块,以suffix的值作为后缀
• prefixOverrides:表示整个语句块要去撤除的前缀
• suffixOverrides:表示整个语句块要去撤除的后缀
调整 Mapper.xml 的插入语句为:
  1. <insert id="insertUserByCondition">
  2.          INSERT INTO userinfo
  3.          <trim prefix="(" suffix=")" suffixOverrides=",">
  4.         <if test="username !=null">
  5.                  username,
  6.         </if>
  7.          <if test="password !=null">
  8.                  `password`,
  9.         </if>
  10.         <if test="age != null">
  11.                  age,
  12.          </if>
  13.         <if test="gender != null">
  14.                  gender,
  15.          </if>
  16.          <if test="phone != null">
  17.                  phone,
  18.          </if>
  19. </trim>
  20. VALUES
  21. <trim prefix="(" suffix=")" suffixOverrides=",">
  22.          <if test="username !=null">
  23.                  #{username},
  24.         </if>
  25.          <if test="password !=null">
  26.                  #{password},
  27.         </if>
  28.          <if test="age != null">
  29.                  #{age},
  30.          </if>
  31.          <if test="gender != null">
  32.                  #{gender},
  33.         </if>
  34.          <if test="phone != null">
  35.                  #{phone}
  36.          </if>
  37. </trim>
  38. </insert>
复制代码
在以上 sql 动态解析时,会将第⼀个部分做如下处置惩罚:
• 基于 prefix 配置,开始部分加上 (
• 基于 suffix 配置,结束部分加上 )
• 多个构造的语句都以 , 结尾,在末了拼接好的字符串还会以 , 结尾,会基于
suffixOverrides 配置去掉末了⼀个 ,
• 留意 < if test=“username !=null” > 中的 username 是传⼊对象的属性
7.3、< where >标签

看下面这个场景, 体系会根据我们的筛选条件, 动态组装where 条件

这种如何实现呢?
接下来我们看代码实现:
需求: 传入的用户对象,根据属性做where条件查询,用户对象中属性不为 null 的,都为查询条件. 如username 为 “a”,则查询条件为 where username=“a”
原有SQL
  1. SELECT
  2. *
  3. FROM
  4. userinfo
  5. WHERE
  6. age = 18
  7. AND gender = 1
  8. AND delete_flag =0
复制代码
接口界说:
  1. List<UserInfo> queryByCondition();
复制代码
Mapper.xml实现:
  1.   <select id="queryByCondition" resultType="com.example.demo.model.UserInfo">
  2.         select id, username, age, gender, phone, delete_flag, create_time,
  3.         update_time
  4.         from userinfo
  5.         <where>
  6.             <if test="age != null">
  7.                 and age = #{age}
  8.             </if>
  9.             <if test="gender != null">
  10.                 and gender = #{gender}
  11.             </if>
  12.             <if test="deleteFlag != null">
  13.                 and delete_flag = #{deleteFlag}
  14.             </if>
  15.         </where>
  16.     </select>
复制代码
  < where > 只会在子元素有内容的情况下才插⼊where⼦句,而且会主动去除子句的开头的AND或OR
以上标签也可以使⽤ < trim prefix=“where” prefixOverrides=“and” > 更换, 但是此种 情况下, 当子元素都没有内容时, where关键字也会保存
  1.4 、< set >标签

需求: 根据传入的用户对象属性来更新用户数据,可以使用标签来指定动态内容. 接口界说: 根据传⼊的用户id 属性,修改其他不为 null 的属性
  1. Integer updateUserByCondition(UserInfo userInfo);
复制代码
Mapper.xml
  1.   <update id="updateUserByCondition">
  2.         update userinfo
  3.         <set>
  4.             <if test="username != null">
  5.                 username = #{username},
  6.             </if>
  7.             <if test="age != null">
  8.                 age = #{age},
  9.             </if>
  10.             <if test="deleteFlag != null">
  11.                 delete_flag = #{deleteFlag},
  12.             </if>
  13.         </set>
  14.         where id = #{id}
  15.     </update>
复制代码
   :动态的在SQL语句中插⼊set关键字,并会删掉额外的逗号. (用于update语句中) 以上标签也可以使用 < trim prefix=“set” suffixOverrides=“,” > 更换。
  7.5 < foreach>标签

对聚集进行遍历时可以使用该标签。标签有如部属性:
• collection:绑定方法参数中的聚集,如 List,Set,Map或数组对象
• item:遍历时的每⼀个对象
• open:语句块开头的字符串
• close:语句块结束的字符串
• separator:每次遍历之间间隔的字符串需求: 根据多个userid, 删除用户数据
接口方法:
  1. void deleteByIds(List<Integer> ids);
复制代码
ArticleMapper.xml 中新增删除 sql:
  1. <delete id="deleteByIds">
  2.         delete from userinfo
  3.         where id in
  4.         <foreach collection="ids" item="id" separator="," open="(" close=")">
  5.             #{id}
  6.         </foreach>
  7.     </delete>
复制代码
7.6 < include >标签

问题分析: • 在xml映射文件中配置的SQL,偶然大概会存在许多重复的片断,此时就会存在许多冗余的代码

我们可以对重复的代码片断进行抽取,将其通过 < sql > 标签封装到⼀个SQL片断,然后再通过
< include > 标签进行引用。
• < sql> :界说可重用的SQL片断
• < include> :通过属性refid,指定包罗的SQL片断
  1. <sql id="allColumn">
  2.         id, username, age, gender, phone, delete_flag, create_time, update_time
  3. </sql
复制代码
通过 < include > 标签在原来抽取的地方进行引用。操作如下:
  1.         <select id="queryAllUser" resultMap="BaseMap">
  2.         select
  3.         <include refid="allColumn"></include>
  4.         from userinfo
  5.     </select>
  6.     <select id="queryById" resultType="com.example.demo.model.UserInfo">
  7.         select
  8.         <include refid="allColumn"></include>
  9.         from userinfo where id= #{id}
  10.     </select>
复制代码
八、 总结

7.1 MySQL 开发企业规范

   MySQL 在 Windows 下不区分巨细写, 但在 Linux 下默认是区分巨细写. 因此, 数据库名, 表名, 字段名都不允许出现任何大写字母, 避免添枝加叶
正例: aliyun_admin, rdc_config, level3_name
反例: AliyunAdmin, rdcConfig, level_3_name
     id 必为主键, 类型为 bigint unsigned, 单表时⾃增, 步⻓为 1
create_time, update_time 的类型均为 datetime 类型, create_time表⽰创建时间,
update_time表⽰更新时间 有同等含义的字段即可, 字段名不做强制要求
       7.2 #{} 和${} 区别

以上就是本文全部内容,感谢各位可以或许看到末了,盼望各人可以有所收获!创作不易,盼望各人多多支持!
末了,各人再见!祝好!我们下期见!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com技术社区 (https://dis.qidao123.com/) Powered by Discuz! X3.4