MyBatis 学习总结

一、入门#

1.1 什么是 MyBatis?#

  • MyBatis 是一款持久层框架(ORM 编程思想)

  • MyBatis 免除了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的过程;

  • MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的实体类(pojo:Plain Old Java Object 简单的 Java 对象)映射成数据库中的记录;

  • MyBatis 前身为 iBatis(经常在控制台看见);

  • Mybatis官方文档 : http://www.mybatis.org/mybatis-3/zh/index.html

1.2 持久化#

​ 持久化是将程序数据在持久状态和瞬时状态间转换的机制

​ 通俗的讲,就是瞬时数据(比如内存中的数据,是不能永久保存的)持久化为持久数据(比如持久化至数据库中,能够长久保存)

  • 即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是将内存中的对象存储到数据库中,或存储在磁盘、XML 等文件中;

  • JDBC就是一种持久化机制。文件IO也是一种持久化机制。

持久化的意义:

​ 由于内存的存储特性,将数据从内存中持久化至数据库,从存储成本(容量、保存时长)看,都是必要的。

1.3 持久层#

​ 即数据访问层(DAL 层),其功能主要是负责数据库的访问,实现对数据表的 CEUD 等操作。

持久层的作用是将输入库中的数据映射成对象,则我们可以直接通过操作对象来操作数据库,而对象如何和数据库发生关系,那就是框架的事情了。

1.4 第一个 MyBatis 程序#

使用环境:

​ jdk8

​ MySql 5.7

​ Maven 3.6.3

​ IEDA

项目结构:

image-20200513181536681

配置 pom.xml,添加必要的依赖:

Copy
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!--父工程 可以用idea根目录作为工程目录,也可以其为父工程,每个Module作为工程目录 优点:父工程配置一次依赖,所有子工程受用 --> <groupId>org.example</groupId> <artifactId>Mybatis</artifactId> <version>1.0-SNAPSHOT</version> <modules> <module>mybatis_01</module> </modules> <!-- 设置打包格式--> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> </dependencies> </project>

​ 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的,可通过SqlSessionFactoryBuilder 对象获得,而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。

第一步:新建 MyBatis 的核心配置文件 mybatis-config.xml

Copy
<?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"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <!-- 每一个Mapper.xml都要在项目的核心配置文件中注册映射--> <mappers> <mapper resource="yh/dao/UserMapper.xml"/> </mappers> </configuration>

第二步:编写获取 SqlSession 对象的工具类

Copy
package yh.utils; 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 java.io.IOException; import java.io.InputStream; /** * 获取SqlSession对象的工具类 * * @author YH * @create 2020-05-13 11:01 */ public class MybatisUtils { //1.获取SqlSessionFactory对象 private static SqlSessionFactory sqlSessionFactory; static { try { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } catch (IOException e) { e.printStackTrace(); } } /** * 2.获取SqlSession对象 * * @return */ public static SqlSession getSqlSession() { return sqlSessionFactory.openSession(); } }

第三步:创建对应数据表的实体类

Copy
package yh.pojo; /** * 实体类 * pojo:简单的Java对象(Plain Old Java Object) * * @author YH * @create 2020-05-13 11:36 */ public class User { private int id; private String name; private String pwd; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } public User(int id, String name, String pwd) { this.id = id; this.name = name; this.pwd = pwd; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", pwd='" + pwd + '\'' + '}'; } }

第四步:创建 Mapper 接口

Copy
package yh.dao; import yh.pojo.User; import java.util.List; /** * 持久层接口 * 在MyBatis中用Mapper替换原来的Dao * * @author YH * @create 2020-05-13 11:35 */ public interface IUserMapper { /** * 查询所有 * * @return */ List<User> selectUser(); /** * 根据id查用户 * @param id * @return */ User selectUserById(int id); /** * 修改用户信息 * @param user */ int updateUser(User user); /** * 删除用户 * @param id */ int deleteUser(int id); int addUser(User user); }

第五步:创建 Mapper.xml 文件(以前是实现类,显示是实现一个 source)

Copy
<?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"> <!--namespace命名空间:指定持久层接口--> <mapper namespace="yh.dao.IUserMapper"> <!--select标签:表名是要执行查询操作,内部填写SQL语句 id属性:指定接口中定义的方法 resultType属性:指定实体类全类名 这一对标签就像对应接口中的一个方法--> <select id="selectUser" resultType="yh.pojo.User"> select * from mybatis.user </select> <!-- #{} 就像以前的通配符,里面的id就是形参变量 parameterType:设置参数类型--> <select id="selectUserById" parameterType="int" resultType="yh.pojo.User"> select * from mybatis.user where id=#{id} </select> <update id="updateUser" parameterType="yh.pojo.User"> update mybatis.user set name=#{name},pwd=#{pwd} where id=#{id} </update> <delete id="deleteUser" parameterType="int"> delete from mybatis.user where id=#{id} </delete> <insert id="addUser" parameterType="yh.pojo.User"> insert into mybatis.user values(#{id},#{name},#{pwd}); </insert> </mapper>

每一个 Mapper.xml 都需要在 mybatis 核心配置文件中注册

第五步:编写测试类

​ 按照规范,test 目录下测试类的结构要与 java 代码结构对应

Copy
package yh.dao; import org.apache.ibatis.session.SqlSession; import org.junit.Test; import yh.pojo.User; import yh.utils.MybatisUtils; import java.util.List; /** * 测试类 * * @author YH * @create 2020-05-13 12:13 */ public class UserMapperTest { @Test public void selectUser() { //1.获取SqlSession对象(用来执行SQL,像以前用的ProperStatement) SqlSession session = MybatisUtils.getSqlSession(); //通过反射从获取对象 IUserMapper mapper = session.getMapper(IUserMapper.class); //调用实例方法 List<User> users = mapper.selectUser(); for (User u : users) { System.out.println(u); } session.close(); } @Test public void selectUserById(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); User user = mapper.selectUserById(2); System.out.println(user); session.close(); } @Test public void updateUser(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); IUserMapper mapper = sqlSession.getMapper(IUserMapper.class); User user = new User(2, "熊大", "123"); int i = mapper.updateUser(user); if(i > 0){ System.out.println("修改成功"); } //增删改查操作需要提交事务 sqlSession.commit(); sqlSession.close(); } @Test public void deleteUser(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); IUserMapper mapper = sqlSession.getMapper(IUserMapper.class); int i = mapper.deleteUser(3); if(i > 0){ System.out.println("删除成功"); } //增删改查操作需要提交事务 sqlSession.commit(); sqlSession.close(); } @Test public void insterUser(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); IUserMapper mapper = sqlSession.getMapper(IUserMapper.class); User user = new User(5, "熊二", "321"); int i = mapper.addUser(user); if(i > 0){ System.out.println("插入成功"); } //增删改查操作需要提交事务 sqlSession.commit(); sqlSession.close(); } }

查询所有结果:

image-20200513181313304

数据表数据:

image-20200513181405082

  • MyBatis 应用的增删增删改操作需要提交事务,传统 JDBC 的增删改操操作中,连接对象被创建时,默认自动提交事务,在执行成功关闭数据库连接时,数据就会自动提交。

  • 万能 Map

    用 map 集合来保存执行 SQL 所需的参数,多个参数时也可用 Map 或使用注解。

    应用场景:假如通过 new 对象作为参数,调用修改方法,new一个对象需要填上构造器的所有参数,而我们可能只需要用到其中一个,就很麻烦,而使用 map 可制造任意参数,key 为参数名,value 为参数值:

    image-20200514152828420

    三种传参方式:

    ​ 直接传值 如...method(int id),可以直接在 sql 中取id

    ​ 对象作为参数传递 如...method(User user),直接在 sql 中去对象的属性即可;

    ​ Map 作为参数 如...method(Map<String,Object> map),直接在 sql 中取出 key 即可

  • 可能出现的问题:

    • 分析错误异常信息,从下往上读
    • 找不到资源异常:

    image-20200513182310138

    原因:Maven 会对静态资源过滤,即在image-20200513182525296 java 目录下的非 java 代码都不编译

    解决:在 pom.xml 中配置resources:

    Copy
    <!-- build中配置resources标签,针对无法找到java目录下的资源问题--> <build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> </resources> </build>
    • Mapper.xml 没有在核心配置文件(mybatis-config.xml 中)注册,解决添加如下配置:

      Copy
      <mappers> <mapper resource="yh/dao/UserMapper.xml"/> </mappers>
    • 其他问题往往是出现在与数据库连接的配置上,如 url 配置等

    • 补充:

      模糊查询写法:

      1. 在 Java 代码层面,传参数值的时候写通配符 % %

        Copy
        List<User> users = mapper.getUserLike("%李%");
      2. 在 sql 拼接中使用通配符

        Copy
        select * from mybatis.user where name like "%"#{value}"%"

二、XML 配置#

MyBatis 核心配置文件顶层结构:

image-20200514192454972

​ 添加对应元素时需要按照顺序来(如配置 properties 元素,要将其放在最上面);

属性(properties)#

​ 我们在配置数据源(DataSource)的时候设置的 driver、url、username 等都可以使用配置属性的形式获取(这样数据源就可以以一个固定模板呈现,数据源修改是方便些)。设置:

Copy
<properties resource="db.properties"> <!-- 同时可以在内部设置属性(隐私性更好)--> <property name="username" value="root"/> <property name="password" value="root"/> </properties>

读取外部可动态改变的 properties文件,不用修改主配置类就可以实现动态配置
Mybatis读取配置的顺序:
​ 先读取properties元素体内属性;再读取resources/url属性中指定属性文件;最后读取作为方法参数传递的属性;**
​ 读取到同名属性时,先读取到的倍后读取到的覆盖;
所以,这几个位置读取属性的优先级
作为方法参数传递的属性 > resources/url属性中指定属性文件 > properties元素体内属性

通过标识被读取的数据,DataSource中可以直接通过name引用,如下:

Copy
<dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource>

​ 配置 JDBC 连接所需参数值的都用变量代替了。

设置(setting)#

类型别名(typeAliases)#

​ 别名使用数据库都了解过,字符段太长了可使用别名代替;同理,Mybatis 中尝尝应用类的全限定类名(全类名),往往都很长,可以使用别名的形式让我们引用全类名更加便利:

类型别名的作用域:当前 xml 文件中

Copy
<typeAliases> <!-- 给指定全类名的类其别名--> <typeAlias type="yh.pojo.User" alias="User"/> <!-- 给指定包下所有的JavaBean起别名,别名为其类名首字母小写-> <package name="yh.dao"/> </typeAliases>

​ 配置后,使用全限定类名的地方都可以用别名替换(如,User 可以使用在任何使用 yh.pojo.User 的地方。

注意:给包起别名,默认别名是包下JavaBean的首字母小写,如果这个JavaBean有注解的话,则别名为其注解值,如下:

Copy
@Alias("User1") public class User { ... }

Mybatis 中给常见的 Java 类型内建了别名,整体规律:基本类型别名为在前面加一个下划线 '_';而包装类的别名为其对应的基本类型名(也就是说我们使用 int 作为 resultType 参数值时,实际使用的是 Integer,所以我们设置方法参数时,最好也是用 Integer,没报错应该是自动装箱的功劳)。

映射器(Mappers)#

有四种方式:

Copy
<mappers> <!-- 指定相对于类路径的资源引用(推荐使用)--> <mapper resource="yh/dao/UserMapper.xml"/> <!-- 使用完全限定资源定位符(URL) --> <mapper url="file:///var/mapper/UserMapper.xml"/> <!-- 使用映射器接口实现类的完全限定类名 前提:接口名与xml文件名名相同--> <mapper class="yh.dao.IUserMapper"/> <!-- 将包内的映射器接口实现全部注册为映射器 前提:接口名与xml文件名名相同--> <package name="yh.dao"/> </mappers>

这些配置会告诉 MyBatis 去哪里找映射文件,剩下的细节就应该是每个 SQL 映射文件了.

生命周期 和 作用域(Scope)#

作用域和生命周期类别至关重要,因为错误的使用会导致非常严重的并发问题

一次完整的生命周期:

image-20200515075608317

  • SqlSessionFactoryBuilder
    • 一旦创建了 SQLSessionFactory 工厂对象,就不再需要了
    • 作用域:局部方法变量
  • SqlSessionFactory
    • 一旦被创建,在运行期间就一直存在
    • 作用域:全局(应用作用域)
    • 最简单的就是使用单例模式或者静态单例模式。
    • 就像数据库中的连接池,有请求就给一个连接。同理多线程时,有请求就创建一个 SqlSession,示意图如下:

image-20200515075721482

  • SqlSession

    • 每个线程都有独有的 SqlSession 实例,其线程时不安全的,因此不能被共享(多例)

    • 作用域:请求或方法作用域

    • 如在 web 中,一次请求就打开一个 SqlSession,返回一个响应后,就关闭它,如:

      Copy
      try (SqlSession session = sqlSessionFactory.openSession()) { // 你的应用逻辑代码 }

三、XML 映射器#

  • cache – 该命名空间的缓存配置。
  • cache-ref – 引用其它命名空间的缓存配置
  • resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
  • sql – 可被其它语句引用的可重用语句块。
  • insert – 映射插入语句。
  • update – 映射更新语句。
  • delete – 映射删除语句。
  • select – 映射查询语句。

结果映射(resultMap)#

​ 解决列名与 JavaBean 属性名不匹配问题。

​ ResultMap 设计思想:对简单语句做到零配置,对复杂语句,只需要描述语句间的关系就行了。

方式一:零配置

​ 如前面提到的万能 Map 将列映射到 HashMap 的键上,由resultType 属性指定以及直接映射到对象(即映射到 ResultSet ,如:resultType="User")这些都是简单的映射,MyBatis 底层会自动创建一个 ResultMap,再根据属性名来映射列到 JavaBean 的属性上

通过 SQL 语句设置别名也可以实现匹配

方式二:描述语句间的关系

  1. 先在 Mapper.xml 文件的 mapper 标签内显示配置resultMap
Copy
<!--id属性:此resultMap的标识,供引用语句指定 type属性:映射JavaBean全类名(可用别名)--> <resultMap id="userResultMap" type="yh.pojo.User"> <result column="id" property="id"/> <result column="name" property="name"/> <!-- 以上两条是匹配的可以省略。就写不匹配的那个属性,如下--> <result column="password" property="pwd"/> </resultMap>
  1. 在引用它的语句中设置 resultMap 属性即可:
Copy
<select id="selectUsers" resultMap="userResultMap"> select id, name, password from user where id = #{id} </select>

四、日志#

日志工厂#

​ MyBatis 核心配置文件中的 settings 元素的 logImpl 属性用于 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。

参数值:

​ SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING

  • STDOUT_LOGGING:标准日志输出

在核心文件 mybatis-config.xml 中进行如下配置:

Copy
<settings> <!--标准的日志工程实现--> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings>

标准日志输出(STDOUT_LOGGING)测试结果:

image-20200515113141505

LOG4J#

  • Java 日志框架,通过它可以控制日志信息输送的目的地为控制台、文件还是 GUI 组件等;

  • 可以控制每一条日志的输出格式;

  • 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程;

  • 通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

  • 配置:

    • 添加依赖

      Copy
      <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
    • resource资源目录下新建 log4j.properties 文件

      Copy
      #将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码 log4j.rootLogger=DEBUG,console,file #控制台输出的相关设置 log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.Target = System.out log4j.appender.console.Threshold=DEBUG log4j.appender.console.layout = org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=[%c]-%m%n #文件输出的相关设置 log4j.appender.file = org.apache.log4j.RollingFileAppender log4j.appender.file.File=./log/yh.log log4j.appender.file.MaxFileSize=10mb log4j.appender.file.Threshold=DEBUG log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n #日志输出级别 log4j.logger.org.mybatis=DEBUG log4j.logger.java.sql=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.ResultSet=DEBUG log4j.logger.java.sql.PreparedStatement=DEBUG
    • 配置 log4j 为日志的实现

      Copy
      <settings> <!-- log4j日志实现--> <setting name="logImpl" value="LOG4J"/> </settings>

测试结果:

image-20200515124238271

  • 简单使用:在要输出日志的类中加入相关语句:

    1. 在要使用 Log4j 的类中,导入包 org.apache.log4j.Logger

    2. 然后获取日志对象,参数为当前类的 class,如下:

      Copy
      static Logger logger = Logger.getLogger(UserMapperTest.class);
    3. 获取对象后,在要使用它的方法中通过日志对象根据需要的日志级别调用对应方法,参数为需要提示的信息(像以前使用 print 输出提示),如下:

      Copy
      logger.info("我是info级别的日志消息"); logger.debug("我是debug级别的日志消息"); logger.error("我是error级别的日志消息");
    4. 测试结果:

      控制台输出:

      image-20200515130233690

      输出到 file 文件中:

      image-20200515130310585

      image-20200515130338845

五、分页#

使用 Limit 分页#

​ 分页核心由 SQL 完成。

  1. 接口中定义:

    Copy
    /** * 查询结果集分页 * @param map * @return */ List<User> selectUserLimit(Map<String,Integer> map);
  2. Mapper.xml

    Copy
    <select id="selectUserLimit" parameterType="map" resultMap="userResultMap"> select * from mybatis.user limit #{index},#{num} </select>
  3. 测试

    Copy
    @Test public void testLimit(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); HashMap<String,Integer> map = new HashMap<>(); map.put("index",1); map.put("num",2); List<User> users = mapper.selectUserLimit(map); for(User u : users){ System.out.println(u.toString()); } session.close(); }
  4. 结果

    image-20200516100937740

RowBounds 分页(了解)#

​ 不使用 SQL 实现分页。

  1. 接口

    Copy
    /** * 查询结果集分页 * @param map * @return */ List<User> selectUserLimit(Map<String,Integer> map);
  2. mybatis.xml

    Copy
    <select id="selectUserLimit" resultMap="userResultMap"> select * from mybatis.user </select>
  3. 测试

    Copy
    @Test public void testRowBounds(){ SqlSession session = MybatisUtils.getSqlSession(); //RowBounds实现 RowBounds rowBounds = new RowBounds(1,2); //通过java代码层面实现分页 List<User> userList = session.selectList("yh.dao.IUserMapper.selectUserLimit",null,rowBounds); //遍历输出:略... session.clise(); }

分页插件#

MyBatis 分页插件:PageHelper

官方文档:https://pagehelper.github.io/

五、使用注解#

简单查询:

  1. MyBatis 中使用注解可以省去实现接口的 xml 文件,直接加一条注解语句,如下:
Copy
public interface IUserMapper { @Select("select * from user") List<User> selectUser(); }
  1. 而在 mybatis 核心配置文件中的 mappers 元素中注册绑定接口:
Copy
<mappers> <mapper class="yh.dao.IUserMapper"/> </mappers>
  1. 测试:
Copy
@Test public void testAnnSelect(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); List<User> users = mapper.selectUser(); for (User user : users) { System.out.println(user.toString()); } session.close(); }

结果:

image-20200516111507461

MyBatis 中,简单的 sql 语句可使用注解映射,复杂的最好用 xml 配置,否则难上加难;不要拘泥于某种方式,可两者配合着使用。

注解实现简单 CRUD#

注意:MybatisUtils 工具类做了以下修改:

Copy
/** * 获取SqlSession对象 * @return */ public static SqlSession getSqlSession(){ //造对象并设置其自动提交事务 return sqlSessionFactory.openSession(true); }

demo 的结构:

image-20200516144455481

  1. 编写接口,使用注解

    Copy
    package yh.dao; import org.apache.ibatis.annotations.*; import yh.pojo.User; import java.util.List; /** * @author YH * @create 2020-05-15 10:51 */ public interface IUserMapper { /** * 添加用户 * @param user */ @Insert("insert into user(id,name,pwd) values(#{id},#{name},#{password})") int addUser(User user); /** * 删除用户 * @param id * @return */ @Delete("delete from user where id=#{id}") int seleteUser(@Param("id") int id); /** * 修改用户 * @param user * @return */ @Update("update user set name=#{name},pwd=#{password} where id=#{id}") int updateUser(User user); /** * 查询所有 * @return */ @Select("select * from user") List<User> selectUser(); }

关于 @Param() 注解:#

  • 基本类型的参数或者 String 类型需要加上
  • 引用类型不需要加
  • 如果只有一个基本类型,可以可以省略(建议加上)
  • 我们在 SQL 中引用的,以@Param()中的设定为准(如参数变量与设定不同时)
  1. 而在 mybatis 核心配置文件中的 mappers 元素中注册绑定接口:

    Copy
    <mappers> <mapper class="yh.dao.IUserMapper"/> </mappers>

    {} 与 ${}:常用前者(sql 解析时会加上" ",当成字符串解析;后者传入数据直接显示在生成的 sql 中,无法防止 SQL 注入。

  2. 测试

    Copy
    package yh.dao; import org.apache.ibatis.session.SqlSession; import org.junit.Test; import yh.pojo.User; import yh.utils.MybatisUtils; import java.util.List; /** * @author YH * @create 2020-05-16 10:39 */ public class testAnn { @Test public void addUser(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); int i = mapper.addUser(new User(6, "葫芦娃", "12333")); //由于工具类中设置了自动提交事务,所以这边可以省略 if (i > 0) { System.out.println("增加成功"); } session.close(); } @Test public void seleteUser(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); int i = mapper.seleteUser(1); //由于工具类中设置了自动提交事务,所以这边可以省略 if (i > 0) { System.out.println("删除成功"); } session.close(); } @Test public void updateUser(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); int i = mapper.updateUser(new User(4, "奥特曼", "11111111")); if(i > 0){ System.out.println("修改成功"); } session.close(); } @Test public void testAnnSelect(){ SqlSession session = MybatisUtils.getSqlSession(); IUserMapper mapper = session.getMapper(IUserMapper.class); List<User> users = mapper.selectUser(); for (User user : users) { System.out.println(user.toString()); } session.close(); } }

复杂查询#

多对一#

​ 如多个学生被一个老师教,对学生而言是多对一的关系,那么怎么对它们进行查询呢?

如何通过查询学生表,同时获取到他的老师的信息?

​ 单纯用 sql 语句的话,连接查询、子查询很简单就可以实现,但是要怎么在 mybatis 中实现呢?两种方式:

前言

image-20200516221623474

多对一情况下,两个 JavaBean 的属性:

Copy
public class Student { private int id; private String name; /** * 多个学生同一个老师,即多对一 */ private Teacher teacher; //略... }
Copy
public class Teacher { private int id; private String name; //略... }

方式一:按照查询嵌套处理(子查询)

​ 先查询出所有学生的信息,根据查询出来的tid(外键),寻找对应的老师。具体实现:

学生接口的 Mapper.xml:

Copy
<mapper namespace="yh.dao.IStudentMapper"> <select id="selectStudents" resultMap="StudentResultMap"> select * from mybatis.student </select> <resultMap id="StudentResultMap" type="yh.pojo.Student"> <result property="id" column="id"/> <result property="name" column="name"/> <!--上面的语句也可以省略(JavaBean的属性名和表的字段名可以匹配) 复杂的属性需要单独处理: 处理对象:association 处理集合:collection --> <association property="teacher" column="tid" javaType="yh.pojo.Teacher" select="selectTeachers"/> </resultMap> <select id="selectTeachers" resultType="yh.pojo.Teacher"> select * from mybatis.teacher where id=#{tid} </select> </mapper>

​ 使用子查询实现,自然需要两次查询,关键就是如何将两次查询关联起来,这就用到 mpper 元素的子标签:association 元素,property 为实体类对应的属性,column 为表中对应的字段(外键),javaType :查询结果对应的 JavaBean 全类名,select 关联查询语句

测试:

Copy
@Test public void selectStudent(){ SqlSession session = MybatisUtils.getSqlSession(); IStudentMapper mapperS = session.getMapper(IStudentMapper.class); List<Student> students = mapperS.selectStudents(); for (Student student : students) { System.out.println(student.toString()); } session.close(); }

结果:image-20200517090758241

方式二:按照结果嵌套处理(对连接查询的结果表进行处理)

​ 先执行sql语句进行连接查询,获取结果(表),再通过对结果表的处理实现mybatis中多对一查询

学生接口的 Mapper.xml 配置:

Copy
<mapper namespace="yh.dao.IStudentMapper"> <select id="selectStudents2" resultMap="StudentResultMap2"> select s.id sid,s.name sname,t.id tid,t.name tname from mybatis.student s join mybatis.teacher t on s.tid=t.id </select> <!--通过上面查询的结果表进行处理--> <resultMap id="StudentResultMap2" type="yh.pojo.Student"> <result property="id" column="sid"/> <result property="name" column="sname"/> <!--这个属性为对象,所以进行复杂处理,在子标签中对该对象做相对表的映射--> <association property="teacher" javaType="yh.pojo.Teacher"> <!--注意:结果表就相当于对应的数据库表,column元素的值为结果表的字段--> <result property="id" column="tid"/> <result property="name" column="tname"/> </association> </resultMap> </mapper>

需要清楚的一个概念:

java 程序处理数据库查询时,是相对于执行 sql 查询结果所产生结果表,而不是数据库中实际的表。根据结果表中的字段(如起别名,则结果表对应字段就为该别名),mybatis 才能进行相关的映射。

测试代码:

Copy
@Test public void selectStudent2(){ SqlSession session = MybatisUtils.getSqlSession(); IStudentMapper mapperS = session.getMapper(IStudentMapper.class); List<Student> students = mapperS.selectStudents2(); for (Student student : students) { System.out.println(student.toString()); } session.close(); }

结果:image-20200517102101861

在此说明:程序是相对于查询结果产生的表进行映射的。如果都没有查询某个字段,那么结果表中自然没有,对应的 JavaBean 实例也不能赋值。

如上例,查询语句为:select s.id sid,s.name sname,t.name tname(查询老师 id 的参数去掉了),那么结果如下:

image-20200517102541278

老师的 id 属性获取不到了(因为查询结果表中没有)。

一对多#

​ 如一个老师教了多个学生,对于老师来说就是一对多的关系。

​ 那么如何通过一个老师,去查询它对应学生的信息呢?

前言

一对多情况下两个 JavaBean 的属性设置:

Copy
public class Teacher { private int id; private String name; /** * 一个老师拥有多个学生,一对多 */ private List<Student> students; //略... }
Copy
public class Student { private int id; private String name; private int tid; //略... }

与多对一类似,同样的两种方式:

方式一:嵌套查询

​ 从 sql 查询角度看,实际采用的子查询方式,通过指定老师的编号去匹配学生信息。

老师接口的 Mapper.xml 中的配置:

Copy
<mapper namespace="yh.dao.ITeacherMapper"> <select id="selectTeacherById2" resultMap="TeacherResultMap2"> select * from mybatis.teacher where id=#{id} </select> <resultMap id="TeacherResultMap2" type="Teacher"> <id property="id" column="id"/> <collection property="students" javaType="ArrayList" ofType="Students" column="id" select="selectStudents"/> </resultMap> <select id="selectStudents" resultType="Student"> select * from mybatis.student where tid=#{id} </select> </mapper>

​ 在查询老师信息的结果集 resultMap 元素中映射属性复杂类型(集合)时,再进行查询操作(嵌套),最终实现一对多查询。

方式二:按结果嵌套查询

​ 使用连接查询获取一个带有对应学生信息结果表,从而实现映射处理。

老师接口中定义的方法:

Copy
/** * 查询指定id的老师信息,以及其学生的信息 * @param id * @return */ Teacher selectTeacherById(@Param("id") int id);

老师接口的 Mapper.xml 中的配置

Copy
<mapper namespace="yh.dao.ITeacherMapper"> <select id="selectTeacherById" parameterType="int" resultMap="TeacherResultMap"> select t.id tid,t.name tname,s.id sid,s.name sname,s.tid tid from mybatis.teacher t inner join mybatis.student s on t.id=s.tid where t.id=#{id} </select> <resultMap id="TeacherResultMap" type="Teacher"> <result property="id" column="tid"/> <result property="name" column="tname"/> <!--复杂的属性我们需要单独处理 对象:association 集合:collection javaType:用于指定所指类属性的类型 ofType:用于指定类的集合属性中的泛型类型 --> <collection property="students" ofType="Student"> <result property="id" column="sid"/> <result property="name" column="sname"/> <result property="tid" column="tid"/> </collection> </resultMap> </mapper>

​ 一对多相对于多对一,用的集合,集合属性的映射处理需要用到 collection 元素,且指定集合泛型类型使用 ofType 属性,其他基本相同。

测试:

Copy
@Test public void selectTeacher2(){ SqlSession session = MybatisUtils.getSqlSession(); ITeacherMapper mapper = session.getMapper(ITeacherMapper.class); Teacher teacher = mapper.selectTeacherById(1); System.out.println(teacher.toString()); session.close(); }

结果:image-20200517143705863

小结#

  • resultMap 结果映射

    • 常用属性
      • id 属性:当前命名空间中的一个唯一标识,用于标识一个结果映射。
      • type 属性:类的完全限定名, 或者一个类型别名
    • idresult 元素的属性:
      • property
      • column
      • javaType
    • association :关联 - 【多对一】
      • javaType:用于指定所指类属性的类型(全类名或类型别名)
    • collection :集合 -【一对多】
      • 用于指定类的集合属性中的泛型所用类型(全类名或类型别名)

    慢 SQL:执行效率很低的 sql 语句。

    相关 MySQL 内容:MySQL 引擎、InnoDB 底层原理、索引及其优化

六、动态 SQL#

​ 根据不同的条件,生成不同的 SQL 语句,在编译时无法确定,只有等程序运行起来,执行过程中才能确定的 SQL语句为动态 SQL(在 SQL 层面执行一个逻辑代码)

数据表如下:

image-20200518203650704

if#

​ 常用于根据判断条件包含 where 子句的一部分,条件成立,被包含的部分得以执行,反之不执行。如:

Copy
<!--使用if实现动态查询--> <select id="getBlog" parameterType="Map" resultType="Blog"> select * from mybatis.blog where 1=1 <if test="title != null"> and title=#{title} </if> <if test="author != null"> and author=#{author} </if> </select>

​ 根据是否传入参数控制是否增加筛选条件,如下:

Copy
@Test public void test2(){ SqlSession session = MybatisUtils.getSqlSession(); BlogMapper mapper = session.getMapper(BlogMapper.class); Map<String, String> map = new HashMap<>(); //控制参数的有无,实现动态SQL效果 // map.put("title","Java如此简单"); map.put("author","熊大"); List<Blog> blogs = mapper.getBlog(map); for (Blog blog : blogs) { System.out.println(blog.toString()); } //未提交事务是因为在MybatisUtils工具类中开启了自动提交 session.close(); }

choose、when、otherwise#

choose when otherwise 类似于 java 中的 switch caseotherwise ,不同的是这里判断条件设置在 when 元素中,符合其条件的,就执行其所包含的 sql 语句。如下:

Copy
<!--使用choose、when、otherwise实现动态查询--> <select id="findActiveBlogLike" parameterType="Map" resultType="Blog"> select * from mybatis.blog where 1=1 <choose> <when test="title != null"> and title like #{title} </when> <when test="author != null"> and author like #{author} </when> <otherwise> title=#{title} </otherwise> </choose> </select>

​ 传入的 title 就按照 title 的查找,传入了 author 就按 author 查找,两者都没有就用 otherwise 元素中的,如两个元素都传入了,那个 when 元素先执行就用哪个。如此例就是执行 title 的查找,测试代码如下:

Copy
@Test public void test3(){ SqlSession session = MybatisUtils.getSqlSession(); BlogMapper mapper = session.getMapper(BlogMapper.class); Map<String, String> map = new HashMap<>(); //传入两个参数,也只执行先执行的那个 map.put("title","Java%"); map.put("author","熊%"); List<Blog> blogs = mapper.findActiveBlogLike(map); for (Blog blog : blogs) { System.out.println(blog.toString()); } //未提交事务是因为在MybatisUtils工具类中开启了自动提交 session.close(); }

trim、where、set#

where 元素只会在子元素返回任何内容的情况下(有符合条件的子句时)才插入 “WHERE” 子句。且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。

Copy
<select id="findActiveBlogLike2" parameterType="Map" resultType="Blog"> select * from mybatis.blog # 被包含的子元素成立的情况下,插入where以及子元素包含的条件,且去除and前缀 <where> <if test="title != null"> and title like #{title} </if> <if test="author != null"> and author like #{author} </if> </where> </select>

set 元素用于动态包含需要更新的列(条件成立的列),忽略其它不更新的列(条件不成立的列)。在首行插入 set 关键字,并会删掉额外的逗号(最后一个更新字段不要逗号),如下:

Copy
<update id="updateBlog" parameterType="Map"> update mybatis.blog # 在首行插入 `set` 关键字,并会删掉额外的逗号 <set> <if test="id != null">id=#{id},</if> <if test="author != null">author=#{author},</if> <if test="createTime != null">create_time=#{createTime},</if> <if test="views != null">views=#{views}</if> </set> where title=#{title} </update>

测试代码:

Copy
@Test public void test5() { SqlSession session = MybatisUtils.getSqlSession(); BlogMapper mapper = session.getMapper(BlogMapper.class); Map<String, Object> map = new HashMap<>(); map.put("title","Java如此简单"); map.put("author","熊大"); map.put("views","10000"); map.put("createTime",new Date()); int i = mapper.updateBlog(map); if (i > 0) { System.out.println("修改成功"); } }

trim 元素用于自定义 where、set 的功能:

prefix 属性:用于覆盖的前缀

prefixOverride 属性:被覆盖的前缀

与上面用到的 where 等价的自定义 trim 元素:

Copy
<trim prefix="WHERE" prefixOverrides="AND |OR "> ... </trim>

与上面用到的 set 等价的自定义 trim 元素:

Copy
<trim prefix="SET" suffixOverrides=","> ... </trim>

注意:我们覆盖了后缀值设置,并且自定义了前缀值。

SQL 片段#

将 SQL 语句中一些功能的部分抽取出出来,方便复用

  1. 使用 sql 标签抽取公共部分,如:

    Copy
    <sql id="if-title-author"> <if test="title != null"> title = #{title} </if> <if test="author != null"> and author = #{author} </if> </sql>
  2. 在需要使用的地方使用 Include 标签引用即可:

    Copy
    <select id="queryBlogIF" parameterType="Map" resyltType="Blog"> select * from mybatis.blog <where> <include refid="if-title-author"></include> </where> </select>

    注意:1.最好基于表单来定义 SQL 片段;

    ​ 2.片段中不要存在 where 标签;

foreach#

用于指定一个集合进行遍历或指定开头与结尾的字符串以及集合项迭代之间的分隔符

coolection 属性:指示传递过来用于遍历的集合

item 属性:集合当前遍历出来的元素

index 属性:索引(当前迭代的序号)

open 属性:指定开始符号

close 属性:指定结束符号

separator 属性:指定分隔符

元素内包裹的是通过遍历集合参数,之间用分隔符拼接,两头拼接开始结束符,说白了就是一个 sql 语句拼接的过程,拼接的 sql 长度,取决于所传递的集合长度。示例如下:

Mapper.xml:

Copy
<!--foreach--> <select id="findBlogForeach" parameterType="Map" resultType="Blog"> select * from mybatis.blog <where> <foreach collection="ids" item="id" open="(" close=")" separator="or"> id=#{id} </foreach> </where> </select>

测试代码:

Copy
@Test public void test6(){ SqlSession session = MybatisUtils.getSqlSession(); BlogMapper mapper = session.getMapper(BlogMapper.class); Map<String, Object> map = new HashMap<>(); List<Integer> ids = new ArrayList<>(); //要查询哪一条主句,就将它的id放进集合中,由foreach遍历,拼接成sql语句,实现动态SQL效果 ids.add(1); ids.add(3); map.put("ids",ids); List<Blog> blogForeach = mapper.findBlogForeach(map); for (Blog foreach : blogForeach) { System.out.println(foreach.toString()); } session.close(); }

测试结果:image-20200518205909960

注意:

​ 我们表中的 id 字段是 varchar 类型的,而我们向集合中添加的数据是 Integer 类型,但是也能作为判断条件,原因是: MySQL 会进行隐式类型转换(TypeHandler),但是需要注意,有些数据库不支持隐式转换,需要手动转换;

​ 前面说了动态 SQL 实际就是一个拼接 SQL 的过程,我们只需按照 SQL 的格式,去排列组合就可以了,所以必要的一些空格也需要留意(新版本的 mybatis 貌似已经帮我们留意了)。

缓存#

简介#

  1. 什么是缓存?
    • 存在内存中的临时数据
    • 将用户经常查询的数据放在缓存(内存)中,用户查询数据就不用去磁盘(关系型数据库)上查询,而直接从缓存中查询,从而提高查询效率,提升高并发系统性能。
  2. 为什么使用缓存?
    • 提升读取数据的速度的同时,减少和数据库的交互次数,减少系统开销,提升了效率。
  3. 什么样的数据能使用缓存
    • 经常查询并且不经常改变的数据。目的就是提高效率,如果数据经常变化还要去设置缓存,适得其反。

Mybatis 缓存#

  • MyBatis 中默认定义了:一级缓存二级缓存
    • 一级缓存SqlSession 级别的缓存(也称为本地缓存),即从获取 SqlSession 到 SqlSession 被回收期间的缓存。默认情况下,只有有一级缓存开启。
    • 二级缓存:基于 namespace 级别的缓存,即当前 Mapper.xml 范围的缓存,需要手动开启配置。
    • MyBatis 中定义了缓存接口 Cache,可以通过实现 Cache 接口来自定义二级缓存。

一级缓存#

​ 与数据库同一次会话(SqlSession)期间查询到的数据会放在本地缓存中;再需要获取相同数据时,直接从缓存中拿。测试如下:

  1. 在 mybatisConfig.xml 中开启日志,方便观察执行过程

    Copy
    <!-- 配置设置--> <settings> <!--标准的日志工程实现--> <setting name="logImpl" value="STDOUT_LOGGING"/> <!-- log4j实现--> <!-- <setting name="logImpl" value="LOG4J"/>--> </settings>
  2. 接口中定义方法

    Copy
    /** * 根据id查用户 * @param id * @return */ User findUserById(@Param("id") Integer id);
  3. Mapper.xml 配置

    Copy
    <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="yh.dao.UserMapper"> <select id="findUserById" parameterType="int" resultType="User"> select * from mybatis.user where id=#{id} </select> </mapper>
  4. 测试代码

    Copy
    @Test public void test1(){ SqlSession session = MybatisUtils.getSqlSession(); UserMapper mapper = session.getMapper(UserMapper.class); //同一次会话(SqlSession)中,第一次执行查询 User user1 = mapper.findUserById(2); System.out.println("user1" + user1.toString()); //同一次会话(SqlSession)中,第二次执行查询 User user2 = mapper.findUserById(2); System.out.println("user2" + user2.toString()); //比较查询获取的JavaBean实例地址是否相同(是否是同一个对象) System.out.println(user1 == user2); session.close(); }
  5. 对结果日志进行分析

    image-20200519121713815

    ​ 从程序执行看,第二次调用查询时,没有与数据库进行交互,而两次查询所获的 JavaBean 对象实例地址比较结果为 true,所以可断定第二次查询的数据不是从数据库获取的,而是从本地缓存(内存)获取的,这也就是一级缓存的作用。

  • 缓存失效的情况:
    • 查询不同的数据
    • 进行了增删改操作,缓存会刷新(因为原来的数据可能发生了改变)
    • 手动清理缓存:SqlSession.clearCache();

一级缓存默认是开启的,只在一次 SQLSession 中有效,也就是获取连接到关闭连接期间;一级缓存就是一个 Map(key 记录 sql,value 记录对应的查询结果)。

二级缓存#

​ 二级缓存也称为全局缓存,一级缓存作用域太低了(基于 SqlSession 级别),所以诞生了二级缓存(基于 namespace 级别的缓存),整个 Mapper ,对应一个二级缓存;

  • 工作机制
    • 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;如果当前会话关闭了,这个会话的一级缓存就没了;如开启了二级缓存,会话关闭时,一级缓存中的数据会被保存到二级缓存中;
    • 新的会话查询信息就可以从二级缓存中获取内容;
    • 不同 mapper 查出的数据会放在自己对应的缓存中(同样是用 Map 保存,keymapper 标识,value 为其二级缓存数据)。
  1. 开启全局缓存需要对 Mybatis 核心配置文件的 settings 元素中进行如下配置 :

    Copy
    <!--显示地开启全局缓存--> <setting name="cacheEnabled" value="true"/>
  2. 在要使用二级缓存的 SQL 映射(Mapper.xml)中添加一个标签:

    Copy
    <cache/>

    也可以在其中自定义一些参数:

    Copy
    <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
  3. 测试代码

  4. 结果分析

    给 mapper 开启二级缓存前查询结果可以清楚地看出两次从与数据库交互的过程:

    image-20200519172743825

    而开启了 mapper 缓存后:

    image-20200519174741542

    ​ 只与数据库进行了一次交互,但是通过添加<cache/> 标签的方式查询时,两个查询对象的比较结果确是 false,因为使用无参 <cache/> 标签时,未序列化就会报对象序列化异常,而序列化后对通过序列码比价对象肯定是不同的,所以结果为 false。而使用自定义 <cache/> 标签属性时,结果为 true

    image-20200519182757633

    规范:实体类定义时需要实现序列化接口。

缓存查询顺序#

​ 用户进行查询,先先查询二级缓存中是否有对应缓存,有 返回查询结果,无 再去一级缓存查询,有 返回结果,无 去数据库查询;

​ 在查询到数据返回结果时,存在一个缓存过程(将数据存入内存),从数据库查询会根据当前开启的缓存级别,将数据存入级别高的缓存中;在一级缓存中查询到结果时(说明在二级缓存中没有查到),返回数据时,一级缓存会将数据缓存进二级缓存,最后返回结果。

一级缓存提交前提是当前会话关闭,否则不会将缓存送入二级缓存。

流程图:

image-20200519175805775

如果第一次会话的缓存没有提交,则第一次会话中查询的缓存都不能从二级缓存中查询出来,只有第一次缓存提交后,后续的会话查询才能使用二级缓存的结果。

在 mapper 中可通过设置 select 标签的 useCache 属性确定是否需要缓存 ;CUD 标签则通过 flush 属性指定执行后是否刷新缓存。

自定义缓存与 EhCache#

​ EhCache 是一种广泛使用的开源 Java 分布式缓存,主要面向通用缓存。

​ 导入依赖使用,在 mapper 中指定我们使用的 ehcache 缓存实现:

Copy
<!--在当前Mapper.xml中使用耳机缓存--> <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
Copy
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false"> <diskStore path="./tmpdir/Tmp_EhCache"/> <defaultCache eternal="false" maxElementsInMemory="10000" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="1800" timeToLiveSeconds="259200" memoryStoreEvictionPolicy="LRU"/> <cache name="cloud_user" eternal="false" maxElementsInMemory="5000" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="1800" timeToLiveSeconds="1800" memoryStoreEvictionPolicy="LRU"/> </ehcache>
posted @   "无问西东"  阅读(298)  评论(0编辑  收藏  举报
编辑推荐:
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 用 C# 插值字符串处理器写一个 sscanf
阅读排行:
· DeepSeek智能编程
· 精选4款基于.NET开源、功能强大的通讯调试工具
· [翻译] 为什么 Tracebit 用 C# 开发
· 腾讯ima接入deepseek-r1,借用别人脑子用用成真了~
· DeepSeek崛起:程序员“饭碗”被抢,还是职业进化新起点?
网络创业项目 123how出海导航
点击右上角即可分享
微信分享提示
CONTENTS