Mybatis
Mybatis
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。
MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。
MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
入门
导入依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
创建mapper接口
@Mapper
public interface EmployeeMapper {
Employee getEmpById(Integer id);
}
创建mapper的实现文件(.xml)
<?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接口的全类名
-->
<mapper namespace="com.chs.mapper.EmployeeMapper">
<!--
id: mapper接口的方法名
resultType: 返回值类型的全类名
-->
<select id="getEmpById" resultType="com.chs.bean.Employee">
select id,emp_name empName,age,emp_salary empSalary from t_emp where id = #{id}
</select>
</mapper>
配置application.properties文件:说明mapper.xml文件的位置
mybatis.mapper-locations=classpath:mapper/**.xml #扫描resourcex/mapper下的全部.xml文件
mybatis.configuration.map-underscore-to-camel-case=true #开启驼峰映射为下划线格式 ==> empName == emp_name
logging.level.com.chs.mapper.**=debug #控制台打印mybatis的sql语句
MyBatis 设置自动回填id字段
useGeneratedKeys:jdbc规范中提供了此设置,mybatis对此也进行了实现
keyProperty:指定主键的属性名
<insert id="insertEmployee" useGeneratedKeys="true" keyProperty="empId">
insert into t_emp(emp_name,emp_salary)
values(#{empName},#{empSalary})
</insert>
MyBatis 参数传递
#{}:底层使用 PreparedStatement 方式,SQL预编译后设置参数,无SQL注入攻击风险
${}:底层使用 Statement 方式,SQL无预编译,直接拼接参数,有SQL注入攻击风险
使用时机:
参数传递,都使用 #{}
如果要对表名、列名进行动态设置,只能使用 ${} 进行 sql 拼接。
参数取值
传参形式 | 示例 | 取值方式 |
---|---|---|
单个参数 - 普通类型 | getEmploy(Long id) | # |
单个参数 - List类型 | getEmploy(List |
# |
单个参数 - 对象类型 | addEmploy(Employee e) | # |
单个参数 - Map类型 | addEmploy(Map<String,Object> m) | # |
多个参数 - 无@Param | getEmploy(Long id,String name) | #{变量名} //新版兼容 |
import org.apache.ibatis.annotations.Param;
public interface UserMapper {
//
User addEmp(Employee emp);
}
//对应的 XML 映射器
<insert id="addEmp"> //直接使用
insert into t_emp(emp_name,age,emp_salary) values (#{empName},#{age},#{empSalary})
</insert>
@Param
在 MyBatis 中,
@Param
注解用于指定映射器方法中的参数名称。这在方法有多个参数时特别有用,可以在 SQL 查询中引用这些参数。
@Param
的用法概述
- 目的:将方法参数绑定到 SQL 查询参数。
- 使用:允许为参数命名,以便在 SQL 语句中引用。
@Mapper
public interface UserMapper {
// 根据用户 ID 和名称查找用户
User findUserByIdAndName(@Param("userId") int id, @Param("userName") String name);
}
//对应的 XML 映射器
<select id="findUserByIdAndName" parameterType="map" resultType="User">
SELECT * FROM users
WHERE id = #{userId} AND name = #{userName}
</select>
@Mapper
public interface EmployeeMapper {
void addEmp(@Param("emp") Employee employee);
}
<insert id="addEmp">
insert into t_emp(emp_name,age,emp_salary) values (#{emp.empName},#{emp.age},#{emp.empSalary})
</insert>
多个参数 - 有@Param | getEmploy(@Param(“id”)Long id,@Param(“name”)String name) | # |
---|---|---|
扩展: | getEmploy(@Param(“ext”)Map<String,Object> m, @Param(“ids”)List @Param(“emp”)Employ e) |
#{ext.age}, #{ids[0]}、 #{emp.age} |
MyBatis 结果封装
返回普通数据
返回基本类型、普通对象 都只需要在 resultType 中声明返回值类型全类名即可
<select id="countEmp" resultType="java.lang.Long">
select count(*) from t_emp
</select>
<select id="getEmp" resultType="com.atguigu.mybatis.entity.Employee">
select * from t_emp where id = #{id}
</select>
对象封装建议全局开启驼峰命名规则:mapUnderscoreToCamelCase = true;
返回List、Map
List:resultType 为集合中的
元素类型
<select id="getAll" resultType="com.chs.bean.Employee">
select * from t_emp;
</select>
Map:resultType 为 map中的
元素类型
,配合 @MapKey 指定哪一列的值作为Map 的 key,Map 的 Value 为这一行数据的完整信息
//mapper接口
@MapKey("id")
Map<Integer, Emp> getAllMap();
// 实际保存的不是 Employee,是 HashMap 需要自己封装成Employee
//xml文件
<select id="getAll" resultType="com.chs.bean.Employee">
select * from t_emp;
</select>
自定义结果集 - ResultMap
数据库的字段 如果和 Bean的属性 不能一一对应时
如果符合驼峰命名,则开启驼峰命名规则就可以自动映射
自定映射规则:
1.如果在resultMap中映射主表对象时名称相同时可以省略不写。2.当映射从表对象时(一对一/一对多)需要
都配置手动映射
否则所有的映射都失效。3.autoMapping="true”==》只对当前对象有效。
当
autoMapping
设置为true
时,框架会自动根据对象属性和数据库表字段之间的名称匹配来进行映射。4.mybatis有全局配置的autoMapping:="true”局部可以省略不写
auto-mapping-behavior:full注意
autoMapping
- 确保数据库表字段与对象属性的名称格式一致,以避免映射错误。
- 避免重名:主映射与子映射不能有重名字段,如果有需要自己手动配映射。
编写自定义结果集(ResultMap) 进行封装
<!--自定义结果集-->
<resultMap id="EmpResultMap" type="com.atguigu.mybatis.entity.Employee">
<!--
column:为数据库的字段名
property:为javaBean的属性名
-->
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
<result column="emp_salary" property="empSalary"/>
</resultMap>
<!--使用自定义结果集:resulMap-->
<select id="getCustomerByIdWithOrders" resultMap="EmpResultMap">
<!--SQL语句-->
</select>
id 标签:必须指定主键列映射规则
result 标签:指定普通列映射规则
collection 标签
在 MyBatis 中,
<collection>
标签用于映射数据库中一对多的关系。它可以将一个父对象中的集合属性与子对象的数据进行映射,从而在查询时能够获取到相关的多条数据。
标签中参数
ofType:指定集合中每个元素的类型
select:指定分步查询调用的方法
column:指定分步查询传递的参数列
public class User {
private int id;
private String name;
private List<Order> orders; // 一对多关系
// getters and setters
}
public class Order {
private int id;
private String address;
private int userId;
// getters and setters
}
MyBatis映射文件
<resultMap id="ResultMap" type="com.chs.mybatis.bean.User">
<id column="u_id" property="id"></id>
<result column="name" property="name"></result>
<!--
collection:说明 一对N 的封装规则
ofType: 集合中元素的类型
-->
<collection property="orders" ofType="com.atguigu.chs.bean.Order">
<id column="id" property="id"></id>
<result column="address" property="address"></result>
<result column="u_id" property="userId"></result>
</collection>
</resultMap>
<!--使用自定义结果集-->
<select id="selectUserWithOrderById" resultMap="ResultMap">
<!--SQL语句-->
</select>
resultMap
定义了映射关系,<collection>
标签用于定义 User
对象中的 orders
属性。
property
属性指定要填充的对象属性,ofType
指定集合中对象的类型。
association 标签
在 MyBatis 中,
<association>
标签用于映射数据库中的一对一关系。它可以将父对象的属性映射到与之相关的单个子对象。
标签中参数
javaType:指定关联的Bean的类型
select:指定分步查询调用的方法
column:指定分步查询传递的参数列
public class User {
private int id;
private String name;
private Profile profile; // 一对一关系
// getters and setters
}
public class Profile {
private int id;
private String bio;
private int userId;
// getters and setters
}
MyBatis 映射文件:
<resultMap id="OrderRM" type="com.chs.mybatis.bean.User">
<id column="id" property="id"></id>
<result column="name" property="name"></result>
<!-- 一对一关联封装 -->
<association property="profile" javaType="com.chs.mybatis.bean.Profile">
<id column="p_id" property="id"></id>
<result column="bio" property="bio"></result>
<result column="user_id" property="userId"></result>
</association>
</resultMap>
<!--使用自定义结果集-->
<select id="selectUserWithProfileById" resultMap="OrderRM">
<!--SQL语句-->
</select>
resultMap
定义了映射关系,<association>
标签用于定义User
对象中的profile
属性。
property
属性指定要填充的对象属性,javaType
指定相关对象的类型。
分步查询
在 association 和 collection 的封装过程中,可以使用 select + column 指定分步查询逻辑
select:指定分步查询调用的方法
column:指定分步查询传递的参数
传递单个:直接写列名,表示将这列的值作为参数传递给下一个查询
传递多个:column="{prop1=col1,prop2=col2}",下一个查询使用prop1、prop2取值
@Mapper
public interface CustomerAndOrdersStep {
List<Order> getOrderListByCustomerId(long cId);
Customer getCustomerById(long id);
Customer getCustomerAndOrderStepById(long id);
}
MyBatis 映射文件:
<!--根据客户id查询所有订单-->
<select id="getOrderListByCustomerId" resultType="com.chs.bean.Order">
select * from t_order where customer_id = #{cId}
</select>
<!--根据客户id查询客户信息-->
<select id="getCustomerById" resultType="com.chs.bean.Customer">
select * from t_customer where id = #{id}
</select>
<!-- 自定义结果集,异步查询 -->
<resultMap id="CustomerAndOrderRM" type="com.chs.bean.Customer">
<id column="customer_id" property="id"></id>
<result column="customer_name" property="customerName"></result>
<result column="phone" property="phone"></result>
<collection property="orderList" ofType="com.chs.bean.Order"
select="getOrderListByCustomerId" <!-- 指定继续查询的方法名 -->
column="id" ></collection> <!-- 传递继续查询的参数值 -->
</resultMap>
<!--使用自定义结果集-->
<select id="getCustomerAndOrderStepById" resultMap="CustomerAndOrderRM">
select * from t_customer where id = #{id}
</select>
超级分步
分步的分步
<!-- 按照id查询客户 -->
<select id="getCustomerById" resultMap="CustomerOrdersStepRM">
select *
from t_customer
where id = #{id}
</select>
<!-- 按照客户id查询他的所有订单 resultType="com.atguigu.mybatis.bean.Order" -->
<select id="getOrdersByCustomerId" resultType="com.atguigu.mybatis.bean.Order">
select *
from t_order
where customer_id = #{cId}
</select>
<!-- 分步查询的自定义结果集: -->
<resultMap id="CustomerOrdersStepRM" type="com.atguigu.mybatis.bean.Customer">
<id column="id" property="id"></id>
<result column="customer_name" property="customerName"></result>
<result column="phone" property="phone"></result>
<collection property="orders"
select="com.atguigu.mybatis.mapper.OrderCustomerStepMapper.getOrdersByCustomerId"
column="id">
</collection>
<!-- 告诉MyBatis,封装 orders 属性的时候,是一个集合,
但是这个集合需要调用另一个 方法 进行查询;select:来指定我们要调用的另一个方法
column:来指定我们要调用方法时,把哪一列的值作为传递的参数,交给这个方法
1)、column="id": 单传参:id传递给方法
2)、column="{cid=id,name=customer_name}":多传参(属性名=列名);
cid=id:cid是属性名,它是id列的值
name=customer_name:name是属性名,它是customer_name列的值
-->
</resultMap>
<select id="getCustomerByIdAndOrdersStep" resultMap="CustomerOrdersStepRM">
select *
from t_customer
where id = #{id}
</select>
<!-- 分步查询:自定义结果集;封装订单的分步查询 -->
<resultMap id="OrderCustomerStepRM" type="com.atguigu.mybatis.bean.Order">
<id column="id" property="id"></id>
<result column="address" property="address"></result>
<result column="amount" property="amount"></result>
<result column="customer_id" property="customerId"></result>
<!-- customer属性关联一个对象,启动下一次查询,查询这个客户 -->
<association property="customer"
select="com.atguigu.mybatis.mapper.OrderCustomerStepMapper.getCustomerById"
column="customer_id">
</association>
</resultMap>
<select id="getOrderByIdAndCustomerStep" resultMap="OrderCustomerStepRM">
select *
from t_order
where id = #{id}
</select>
<!-- 查询订单 + 下单的客户 + 客户的其他所有订单 -->
<select id="getOrderByIdAndCustomerAndOtherOrdersStep"
resultMap="OrderCustomerStepRM">
select * from t_order where id = #{id}
</select>
延迟加载
分步查询有时候并不需要立即运行,我们希望在用到的时候再去查询,可以开启延迟加载的功能
全局配置:
- mybatis.configuration.lazy-loading-enabled=true
- mybatis.configuration.aggressive-lazy-loading=false
MyBatis 动态SQL
MyBatis 的动态 SQL 功能允许开发者根据不同的条件动态生成 SQL 语句。这种灵活性使得在处理复杂查询时,可以根据用户输入或其他条件来构建 SQL 语句,从而提高了代码的可维护性和可读性。
if和where标签
<if>
:根据条件判断是否包含某个 SQL 片段
where
:标签会自动去掉“标签体内前面多余的and/or等
<select id="selectEmployeeByCondition" resultType="employee">
select emp_id,emp_name,emp_salary from t_emp
<!-- where标签会自动去掉“标签体内前面多余的and/or” -->
<where>
<!-- 使用if标签,让我们可以有选择的加入SQL语句的片段。这个SQL语句片段是否要加入整个SQL语句,就看if标签判断的结果是否为true -->
<!-- 在if标签的test属性中,可以访问实体类的属性,不可以访问数据库表的字段 -->
<if test="empName != null">
<!-- 在if标签内部,需要访问接口的参数时还是正常写#{} -->
or emp_name=#{empName}
</if>
<if test="empSalary > 2000">
or emp_salary>#{empSalary}
</if>
<!--
第一种情况:所有条件都满足 WHERE emp_name=? or emp_salary>?
第二种情况:部分条件满足 WHERE emp_salary>?
第三种情况:所有条件都不满足 没有where子句
-->
</where>
</select>
set标签
使用set标签动态管理set子句,并且动态去掉两端多余的逗号
<update id="updateEmployeeDynamic">
update t_emp
<!-- set emp_name=#{empName},emp_salary=#{empSalary} -->
<!-- 使用set标签动态管理set子句,并且动态去掉两端多余的逗号 -->
<set>
<if test="empName != null">
emp_name=#{empName},
</if>
<if test="empSalary < 3000">
emp_salary=#{empSalary},
</if>
</set>
where emp_id=#{empId}
<!--
第一种情况:所有条件都满足 SET emp_name=?, emp_salary=?
第二种情况:部分条件满足 SET emp_salary=?
第三种情况:所有条件都不满足 update t_emp where emp_id=?
没有set子句的update语句会导致SQL语法错误
-->
</update>
trim标签
使用trim标签控制条件部分两端是否包含某些字符
- prefix属性:指定要动态添加的前缀
- suffix属性:指定要动态添加的后缀
- prefixOverrides属性:指定要动态去掉的前缀,使用“|”分隔有可能的多个值
- suffixOverrides属性:指定要动态去掉的后缀,使用“|”分隔有可能的多个值
<select id="selectEmployeeByConditionByTrim" resultType="com.chs.mybatis.entity.Employee">
select emp_id,emp_name,emp_age,emp_salary,emp_gender
from t_emp
<!-- prefix属性指定要动态添加的前缀 -->
<!-- suffix属性指定要动态添加的后缀 -->
<!-- prefixOverrides属性指定要动态去掉的前缀,使用“|”分隔有可能的多个值 -->
<!-- suffixOverrides属性指定要动态去掉的后缀,使用“|”分隔有可能的多个值 -->
<!-- 当前例子用where标签实现更简洁,但是trim标签更灵活,可以用在任何有需要的地方 -->
<trim prefix="where" suffixOverrides="and|or">
<if test="empName != null">
emp_name=#{empName} and
</if>
<if test="empSalary > 3000">
emp_salary>#{empSalary} and
</if>
<if test="empAge <= 20">
emp_age=#{empAge} or
</if>
<if test="empGender=='male'">
emp_gender=#{empGender}
</if>
</trim>
</select>
choose/when/otherwise标签
在多个分支条件中,仅执行一个。
- 从上到下依次执行条件判断
- 遇到的第一个满足条件的分支会被采纳
- 被采纳分支后面的分支都将不被考虑
- 如果所有的when分支都不满足,那么就执行otherwise分支
<select id="selectEmployeeByConditionByChoose" resultType="com.chs.mybatis.entity.Employee">
select emp_id,emp_name,emp_salary from t_emp
where
<choose>
<when test="empName != null">emp_name=#{empName}</when>
<when test="empSalary < 3000">emp_salary < 3000</when>
<otherwise>1=1</otherwise>
</choose>
<!--
第一种情况:第一个when满足条件 where emp_name=?
第二种情况:第二个when满足条件 where emp_salary < 3000
第三种情况:两个when都不满足 where 1=1 执行了otherwise
-->
</select>
foreach标签
用于遍历集合
用批量插入举例
<!--
collection属性:要遍历的集合
item属性:遍历集合的过程中能得到每一个具体对象,在item属性中设置一个名字,将来通过这个名字引用遍历出来的对象
separator属性:指定当foreach标签的标签体重复拼接字符串时,各个标签体字符串之间的分隔符
open属性:指定整个循环把字符串拼好后,字符串整体的前面要添加的字符串
close属性:指定整个循环把字符串拼好后,字符串整体的后面要添加的字符串
index属性:这里起一个名字,便于后面引用
遍历List集合,这里能够得到List集合的索引值
遍历Map集合,这里能够得到Map集合的key
-->
<foreach collection="empList" item="emp" separator="," open="values" index="myIndex">
<!-- 在foreach标签内部如果需要引用遍历得到的具体的一个对象,需要使用item属性声明的名称 -->
(#{emp.empName},#{myIndex},#{emp.empSalary},#{emp.empGender})
</foreach>
批量更新时需要注意
上面批量插入的例子本质上是一条SQL语句,而实现批量更新则需要多条SQL语句拼起来,用分号分开。也就是一次性发送多条SQL语句让数据库执行。此时需要在数据库连接信息的URL地址中设置:allowMultiQueries=true
spring.datasource.url=jdbc:mysql:///mybatis-example?allowMultiQueries=true
对应的foreach标签如下:
<update id="updateEmployeeBatch">
<foreach collection="empList" item="emp" separator=";">
update t_emp set emp_name=#{emp.empName} where emp_id=#{emp.empId}
</foreach>
</update>
关于foreach标签的collection属性
如果没有给接口中List类型的参数使用@Param注解指定一个具体的名字,那么在collection属性中默认可以使用collection或list来引用这个list集合。这一点可以通过异常信息看出来:
Parameter 'empList' not found. Available parameters are [arg0, collection, list]
在实际开发中,为了避免隐晦的表达造成一定的误会,建议使用@Param注解明确声明变量的名称,然后在foreach标签的collection属性中按照@Param注解指定的名称来引用传入的参数。
sql片段
抽取重复的SQL片段
<!-- 使用sql标签抽取重复出现的SQL片段 -->
<sql id="mySelectSql">
select emp_id,emp_name,emp_age,emp_salary,emp_gender from t_emp
</sql>
引用已抽取的SQL片段
<!-- 使用include标签引用声明的SQL片段 -->
<include refid="mySelectSql"/>
特殊字符
以后在xml中,以下字符需要用转义字符,不能直接写
原始字符 | 转义字符 |
---|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
MyBais-缓存机制
MyBatis 拥有二级缓存机制:
一级缓存默认开启; 事务级别:当前事务共享。
默认事务期间,会开启事务级别缓存;
- 同一个事务期间,前面查询的数据,后面如果再要执行相同查询,会从一级缓存中获取数据,不会给数据库发送SQL
@Transactional //默认 可重复读
public void find(){
//方法体
}
//有时候缓存会失效(缓存不命中)。
//失效几种情况
// 1、查询的东西不一样。
// 2、两次查询之间,进行了一次增删改(由于增删改会引起数据库变化,Mybatis认为,数据有可能变了,它就要再发一次查询)
二级缓存需要手动配置开启:所有事务共享
<!-- cache功能开启:所有的查询都会共享到二级缓存 -->
<mapper namespace="com.example.Mapper">
<cache/>
<!-- 其他 SQL 语句配置 -->
</mapper>
缓存中有就不用查数据库:
L1~LN:N级缓存
数字越小离我越近,查的越快。存储越小,造价越高。
数字越大离我越远,查的越慢。存储越大,造价越低。
插件机制
MyBatis 底层使用 拦截器机制提供插件功能,方便用户在SQL执行前后进行拦截增强。
拦截器:Interceptor
拦截器可以拦截四大对象的执行
ParameterHandler:处理SQL的参数对象
ResultSetHandler:处理SQL的返回结果集
StatementHandler:数据库的处理对象,用于执行SQL语句
Executor:MyBatis的执行器,用于执行增删改查操作
PageHelper 分页插件
PageHelper
是一个 MyBatis 的分页插件,广泛用于简化分页查询的操作。在使用 MyBatis 进行数据访问时,PageHelper
能够让你快速实现数据的分页功能,提高应用程序的性能与用户体验。
添加依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>最新版</version> <!-- 请替换为最新版本 -->
</dependency>
创建分页插件对象
@MapperScan("com.chs.mybatis.mapper") //批量只扫描mapper
@Configuration
public class MyBatisConfig {
@Bean
PageInterceptor pageInterceptor(){
//1、创建 分页插件 对象
PageInterceptor interceptor = new PageInterceptor();
//2、设置 参数
//.......
Properties properties = new Properties();
//设置页码超出范围=》自动规范
properties.setProperty("reasonable", "true");
interceptor.setProperties(properties);
return interceptor;
}
}
使用 PageHelper 进行分页查询
紧跟着 startPage (num)之后 的方法就会执行自动的 SQL 分页查询
原理:拦截器;
原业务底层sql:select * from user;
拦截器做两件事:
1)、统计这个表的总数量
2)、给原业务底层SQL 动态拼接上 limit (startPage,pageSize);
ThreadLocal: 同一个线程共享数据
- 第一个查询从 ThreadLocal 中获取到共享数据,执行分页
- 第一个执行完会把 ThreadLocal 分页数据删除
- 以后的查询,从 ThreadLocal 中拿不到分页数据,就不会分页
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import java.util.List;
public List<User> getUsers(int pageNum, int pageSize) {
// 开始分页
PageHelper.startPage(pageNum, pageSize); //紧跟着 startPage 之后 的方法就会执行的 SQL 分页查询
// 执行查询
List<User> users = userMapper.selectAll(); // 假设在 UserMapper 中定义了这个方法
// 封装分页信息
PageInfo<User> pageInfo = new PageInfo<>(users);
return pageInfo.getList(); // 获取当前页的记录
}
mybatisx - 逆向生成
IDEA 安装 mybatisx 插件,即可根据数据库表一键生成常用CRUD
首先使用 IntelliJ IDEA连接数据库
使用逆向工程
MyBatis总结
核心点 | 掌握目标 |
---|---|
mybatis基础 | 使用流程, 参数输入,#{} ${},参数输出 |
mybatis多表 | 实体类设计,resultMap多表结果映射 |
mybatis动态语句 | Mybatis动态语句概念, where , if , foreach标签 |
mybatis扩展 | Mapper批量处理,分页插件,逆向工程 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· Vue3状态管理终极指南:Pinia保姆级教程