Mybatis04__输入参数/输出参数

一、输入参数源码分析

  1、参数解析器解析参数(得到一个Map集合 names,默认是{arg0,arg1,arg2....},如果某一个位置使用了@Param注解,那么该位置的argx会被注解的值替代)

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
public ParamNameResolver(Configuration config, Method method) {
    // 批量获取参数类型,我这里参数类型分别是Integer、String、String
    final Class<?>[] paramTypes = method.getParameterTypes();
    // 批量获取标注了@Param注解的参数,用一个二维数组封装,由于我只在id上标注了一个@Param("userId")
    // 二维数组的长度与传入的参数相同,有几个参数二维数组的长度便是多少,标注了@Param注解的值用一维数组封装
    // 所以最终的二维数组如下:[Annotation[1],Annotation[0],Annotation[0],]
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();
    // 获取二维数组的长度,由于传入了三个参数,所以长度为3
    int paramCount = paramAnnotations.length;
    // get names from @Param annotations
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
      if (isSpecialParameter(paramTypes[paramIndex])) {
        // skip special parameters
        continue;
      }
      String name = null;
      for (Annotation annotation : paramAnnotations[paramIndex]) {
        if (annotation instanceof Param) {
          hasParamAnnotation = true;
          // 获取一维数组中的值,name的值为 userId
          name = ((Param) annotation).value();
          break;
        }
      }
      // 如果不是使用@Param注解标注的参数,并且传入了实际参数,那么根据paramIndex的值去获取对应的值{arg0,arg1,arg2}
      // 例如我们这里第二个参数使用的是 userName,那么它对应的就是 name就是arg1,第三个参数password对应的就是 arg2
      if (name == null) {
        // @Param was not specified.
        if (config.isUseActualParamName()) {
          name = getActualParamName(method, paramIndex);
        }
        // 这里应该是说没有实际参数传入的情况
        if (name == null) {
          // use the parameter index as the name ("0", "1", ...)
          // gcode issue #71
          name = String.valueOf(map.size());
        }
      }
      // 这里的map就是{0:userId,1:arg1,2:arg2}
      map.put(paramIndex, name);
    }
    // 通过Collections工具类将map转换成一个SortedMap names={0:userId,1:arg1,2:arg2}
    names = Collections.unmodifiableSortedMap(map);
  }

   2、通过动态代理获取xxxMapper接口的代理类,然后通过invoke(...)执行目标方法,Object[] args 数组中封装的是实际传入的参数[实际参数1,实际参数2,实际参数3....]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 动态代理执行invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (method.isDefault()) {
        if (privateLookupInMethod == null) {
          return invokeDefaultMethodJava8(proxy, method, args);
        } else {
          return invokeDefaultMethodJava9(proxy, method, args);
        }
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 关键方法,此处的args就是实际传入的参数 Object[2,"李四","24"]
    return mapperMethod.execute(sqlSession, args);
}

   3、根据操作的类型来选择不同的操作方法

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
47
48
49
50
51
52
53
// 通过command.getType()方法来判断当前类型是INSERT/UPDATE/DELETE/SELECT
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        // 返回值是否为空
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        // 返回值是否为数组
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        // 是否使用@MapKey注解
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

  4、将实际参数转换成Sql命令参数

1
2
3
public Object convertArgsToSqlCommandParam(Object[] args) {
  return paramNameResolver.getNamedParams(args);
}

  5、获取最终的参数集合

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
public Object getNamedParams(Object[] args) {
    // 这里面的names就是我们通过参数解析器获得的SortedMap names={0:userId,1:arg1,2:arg2}
    // 此处的args就是实际传入的参数 args = Object[2,"李四","24"]
    final int paramCount = names.size();
    // 如果没有传入实际参数,或者参数的名称个数为0,直接返回null
    if (args == null || paramCount == 0) {
      return null;
      // 如果没有使用@Parma注解并且只有一个参数,那么直接返回第一个参数
    } else if (!hasParamAnnotation && paramCount == 1) {
      return args[names.firstKey()];
    } else {
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      // names={0:userId,1:arg1,2:arg2}  args = Object[2,"李四","24"]
      // 遍历names,并且将相应的数据存入Map集合param中,存入的规则如下
      // param = {userId:2,arg1:"李四",arg2:"24"}
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        param.put(entry.getValue(), args[entry.getKey()]);
 
        // GENERIC_NAME_PREFIX的值为常量 param
        final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
        // 如果names中不包含param1、param2...等等数据,那么集合param中继续存入一份数据
        // {param1:2,param2:"李四",param3:"24"}
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      // 最终的Map集合 param={userId:2,arg1:"李四",arg2:"24",param1:2,param2:"李四",param3:"24"}
      // 相当于是对传入的参数使用了两套保存机制
      return param;
    }
  }
}

 

二、实际案例结合上述源码  

  1、UserMapper接口

1
2
3
4
public interface UserMapper {
    // 根据id、userName、password三个条件查询用户信息
    public abstract User queryUserByThreeConditions(@Param("userId") Integer id, String userName, String password);
}

  2、UserMapper.xml

  根据下面的源码分析,可以得出如下结论,当传入多个参数的时候,如果没有使用@Param注解对参数进行标注,那么param={arg0:2,arg1:"李四",arg2:"24",param1:2,param2:"李四",param3:"24"},如果某个参数使用了@Param注解的话,那么对应的位置的 argx 就会被@Param("value")中的value值所替代,

  所以我们这个例子中param={userId:2,arg1:"李四",arg2:"24",param1:2,param2:"李四",param3:"24"},只要能确保传入的最终参数是{2,"李四","24"},你可以使用各种各样的方式去param这个集合中取取值,例如下面的几种方式都是可以的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--根据id、userName、password三个条件查询用户信息-->
<select id="queryUserByThreeConditions" resultType="com.mybatis.entity.User">
    SELECT ID,USER_NAME,PASSWORD,USER_INFORMATION FROM user
    WHERE ID=#{param1} AND USER_NAME=#{param2} AND PASSWORD=#{param3}
</select>
<select id="queryUserByThreeConditions" resultType="com.mybatis.entity.User">
    SELECT ID,USER_NAME,PASSWORD,USER_INFORMATION FROM user
    WHERE ID=#{userId} AND USER_NAME=#{arg1} AND PASSWORD=#{arg2}
</select>
<select id="queryUserByThreeConditions" resultType="com.mybatis.entity.User">
    SELECT ID,USER_NAME,PASSWORD,USER_INFORMATION FROM user
    WHERE ID=#{userId} AND USER_NAME=#{param2} AND PASSWORD=#{param3}
</select>
<select id="queryUserByThreeConditions" resultType="com.mybatis.entity.User">
    SELECT ID,USER_NAME,PASSWORD,USER_INFORMATION FROM user
    WHERE ID=#{param1} AND USER_NAME=#{arg1} AND PASSWORD=#{arg2}
</select>

  3、测试类

1
2
3
4
5
6
7
8
9
public class TestMybatis {
    @Test
    public void test01() throws IOException {
        SqlSession session = TestMybatis.openSession();
        UserMapper mapper = session.getMapper(UserMapper.class);
        User user = mapper.queryUserByThreeConditions(2, "李四", "24");
        System.out.println(user);
    }
}

 

三、输入参数

  1、单个参数

    01、传入基本类型数据  

1
2
3
4
5
6
7
8
<!--根据主键ID查询用户信息-->
// UserMapper接口
public abstract User queryUserById(Integer id);
// UserMapper.xml
<select id="queryUserById" resultType="com.mybatis.entity.User">
    // 取值的时候可以使用#{任意字符}获得传入的参数
    SELECT ID,USER_NAME,PASSWORD,USER_INFORMATION FROM user WHERE ID=#{xxx}
</select>

    02、传入POJO类型

1
2
3
4
5
6
7
8
9
<!--根据id和userName查询用户信息-->
// UserMapper接口
public abstract User queryUserByIdAnduserName(User user);
// UserMapper.xml
<select id="queryUserByIdAnduserName" resultType="com.mybatis.entity.User">
    // 对于传入的是POJO类型的参数,取值的时候需要使用#{POJO的属性名}来取值
    SELECT ID,USER_NAME,PASSWORD,USER_INFORMATION FROM user
    WHERE ID=#{id} AND USER_NAME=#{userName}
</select>

  

  2、多个参数(这种情况下可以配合@Param("value")注解,可以做到见名知意)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--根据id、userName、password三个条件查询用户信息-->
<select id="queryUserByThreeConditions" resultType="com.mybatis.entity.User">
    SELECT ID,USER_NAME,PASSWORD,USER_INFORMATION FROM user
    WHERE ID=#{param1} AND USER_NAME=#{param2} AND PASSWORD=#{param3}
</select>
<select id="queryUserByThreeConditions" resultType="com.mybatis.entity.User">
    SELECT ID,USER_NAME,PASSWORD,USER_INFORMATION FROM user
    WHERE ID=#{userId} AND USER_NAME=#{arg1} AND PASSWORD=#{arg2}
</select>
<select id="queryUserByThreeConditions" resultType="com.mybatis.entity.User">
    SELECT ID,USER_NAME,PASSWORD,USER_INFORMATION FROM user
    WHERE ID=#{userId} AND USER_NAME=#{param2} AND PASSWORD=#{param3}
</select>
<select id="queryUserByThreeConditions" resultType="com.mybatis.entity.User">
    SELECT ID,USER_NAME,PASSWORD,USER_INFORMATION FROM user
    WHERE ID=#{param1} AND USER_NAME=#{arg1} AND PASSWORD=#{arg2}
</select>

 

  3、传入Map集合

1
2
3
4
5
6
7
8
9
10
传入Map集合(mybatis本来就是要把输入的参数转化为map存起来,既然你已经传入了一个map集合,直接通过键取值就可以了)
map:{"id":1,"password":23}
 
public abstract User queryUserByIdAndPassword(Map<String,Object> map);
 
// 这里的id、password对应的是传入进来的Map集合的key
<select id="queryUserByIdAndPassword" resultType="com.mybatis.entity.User">
    SELECT ID,USER_NAME,PASSWORD,USER_INFORMATION FROM user
    WHERE ID=#{id} AND PASSWORD=#{password}
</select>

 

  4、传入Integer类型数组  

1
2
3
4
5
6
7
8
9
10
11
12
13
//根据多个用户id查询出多条用户记录
public abstract List<User> queryUsersByIds(Integer[] ids);
 
<!--根据多个用户id查询出多条用户记录-->
<select id="queryUsersByIds" resultType="com.mybatis.domain.User">
    SELECT ID,USER_NAME,PASSWORD,USER_INFORMATION FROM tbl_user
        <where>
            ID in
            <foreach collection="array" item="id"  open="(" close=")" separator=",">
                 #{id}
            </foreach>
        </where>
</select>

 

  5、传入List集合 

1
2
3
4
5
6
7
8
9
10
11
12
13
//根据多个用户id查询出多条用户记录
public abstract List<User> queryUsersByIds(List<Integer> ids);
 
<!--根据多个用户id查询出多条用户记录-->
<select id="queryUsersByIds" resultType="com.mybatis.domain.User">
    SELECT ID,USER_NAME,PASSWORD,USER_INFORMATION FROM tbl_user
        <where>
            ID in
            <foreach collection="list" item="id"  open="(" close=")" separator=",">
                 #{id}
            </foreach>
        </where>
</select>

  

四、输出参数

  1、resultType,返回值类型为实体类对象:自动封装,将数据库中的数据自动封装到javaBean中

1
2
// 根据用户EmployeeId查询用户信息
   public abstract Employee queryEmployeeByEmployeeId(Employee employee);
1
2
3
4
5
6
7
8
9
10
11
12
<select id="queryEmployeeByEmployeeId" resultType="com.mybatis.entity.Employee">
        SELECT
            e.EMPLOYEE_ID,
            e.EMPLOYEE_NAME,
            e.EMPLOYEE_PASSWORD,
            e.EMPLOYEE_AGE,
            e.DEPARTMENT_ID
        FROM
          employee e
        WHERE
          e.EMPLOYEE_ID=#{employeeId}
    </select> 
1
2
// 测试结果
Employee{employeeId=3, employeeName='王五', employeePassword='789', employeeAge='25', departmentId=3}

  

  2、resultType,返回值类型为map

1
2
// 根据用户EmployeeId查询用户信息
public abstract Map<String,Object> queryEmployeeByEmployeeId(Employee employee);
1
2
3
4
5
6
7
8
9
10
11
12
<select id="queryEmployeeByEmployeeId" resultType="java.util.Map">
    SELECT
        e.EMPLOYEE_ID,
        e.EMPLOYEE_NAME,
        e.EMPLOYEE_PASSWORD,
        e.EMPLOYEE_AGE,
        e.DEPARTMENT_ID
    FROM
      employee e
    WHERE
      e.EMPLOYEE_ID=#{employeeId}
</select>
1
2
// 测试结果
{EMPLOYEE_ID=3, EMPLOYEE_AGE=25, DEPARTMENT_ID=3, EMPLOYEE_NAME=王五, EMPLOYEE_PASSWORD=789}

 

  3、如果查询的结果是多条记录,而又指定返回值为map集合,这个时候就不能使用mybatis的自动封装,这个时候要为多条记录指定主键,使用@MapKey("数据库中的列名")  

1
2
3
// EMPLOYEE_ID对应的是数据库的字段
@MapKey("EMPLOYEE_ID")
public abstract Map<String,Object> queryEmployeeByEmployeeId(Employee employee);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 根据employeeAge模糊查询用户信息
<select id="queryEmployeeByEmployeeId" resultType="java.util.Map">
    SELECT
        e.EMPLOYEE_ID,
        e.EMPLOYEE_NAME,
        e.EMPLOYEE_PASSWORD,
        e.EMPLOYEE_AGE,
        e.DEPARTMENT_ID
    FROM
      employee e
    WHERE
      e.EMPLOYEE_AGE
    LIKE
      "%"#{employeeAge}"%"
</select>
1
2
3
4
5
6
7
8
// 测试结果
{
    1={EMPLOYEE_ID=1, EMPLOYEE_AGE=23, DEPARTMENT_ID=1, EMPLOYEE_NAME=张三, EMPLOYEE_PASSWORD=123},
    2={EMPLOYEE_ID=2, EMPLOYEE_AGE=24, DEPARTMENT_ID=2, EMPLOYEE_NAME=李四, EMPLOYEE_PASSWORD=456},
    3={EMPLOYEE_ID=3, EMPLOYEE_AGE=25, DEPARTMENT_ID=3, EMPLOYEE_NAME=王五, EMPLOYEE_PASSWORD=789},
    4={EMPLOYEE_ID=4, EMPLOYEE_AGE=26, DEPARTMENT_ID=4, EMPLOYEE_NAME=赵六, EMPLOYEE_PASSWORD=1123},
    5={EMPLOYEE_ID=5, EMPLOYEE_AGE=27, DEPARTMENT_ID=1, EMPLOYEE_NAME=田七, EMPLOYEE_PASSWORD=123579}
}

  

   4、返回值类型为resultMap:支持手动的封装(多对一)

1
2
3
4
5
6
7
8
// 实体类Employee,省略set/get/toString方法
public class Employee {
    private Integer employeeId;
    private String employeeName;
    private String employeePassword;
    private String employeeAge;
    private Department department;
}
1
2
3
4
5
6
// 实体类Department,省略set/get/toString方法
public class Department {
    private Integer departmentId;
    private String description;
    private String location;
} 
1
2
3
4
5
// EmployeeMapper接口
public interface EmployeeMapper {
    // 根据用户EmployeeId查询用户信息
    public abstract Employee queryEmployeeByEmployeeId(Integer id);
} 
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
// EmployeeMapper的全包类路径
<mapper namespace="com.mybatis.dao.EmployeeMapper">
    // 这里的emp可以是任意值,但是一定要和resultMap的值相同
    <resultMap id="emp" type="com.mybatis.entity.Employee">
        // 主键策略
        <id property="employeeId" column="EMPLOYEE_ID" javaType="int" jdbcType="INTEGER"></id>
        <result property="employeeName" column="EMPLOYEE_NAME" javaType="string" jdbcType="VARCHAR"></result>
        <result property="employeePassword" column="EMPLOYEE_PASSWORD" javaType="string" jdbcType="VARCHAR"></result>
        <result property="employeeAge" column="EMPLOYEE_AGE" javaType="string" jdbcType="VARCHAR"></result>
        // 由于Employee实体类中有Department属性,这里的department对应的是属性名,javaType对应Department实体类的全包类路径
        <association property="department" javaType="com.mybatis.entity.Department">
            // 主键策略
            <id property="departmentId" column="DEPARTMENT_ID" javaType="int" jdbcType="INTEGER"></id>
            <result property="description" column="DESCRIPTION" javaType="string" jdbcType="VARCHAR"></result>
            <result property="location" column="LOCATION" javaType="string" jdbcType="VARCHAR"></result>
        </association>
    </resultMap>
 
    <select id="queryEmployeeByEmployeeId" resultMap="emp">
        SELECT
            e.EMPLOYEE_ID,
            e.EMPLOYEE_NAME,
            e.EMPLOYEE_PASSWORD,
            e.EMPLOYEE_AGE,
            d.DEPARTMENT_ID,
            d.DESCRIPTION,
            d.LOCATION
        FROM employee e
        LEFT JOIN
          department d
        ON
          e.DEPARTMENT_ID=D.DEPARTMENT_ID
        WHERE e. EMPLOYEE_ID=#{employeeId}
    </select>
</mapper>
1
2
3
4
5
6
7
8
// 测试结果
Employee{
    employeeId=1,
    employeeName='张三',
    employeePassword='123',
    employeeAge='23',
    department=Department{departmentId=1, description='市场部', location='北京'}
}

  

   5、返回值类型为resultMap:支持手动的封装(一对多)

1
2
3
4
5
6
7
8
// 实体类Employee,省略set/get/toString方法
public class Employee {
    private Integer employeeId;
    private String employeeName;
    private String employeePassword;
    private String employeeAge;
    private Integer departmentId;
}
1
2
3
4
5
6
7
// 实体类Department,省略set/get/toString方法
public class Department {
    private Integer departmentId;
    private String description;
    private String location;
    private List<Employee> employees;
}
1
2
// 查询部门编号为1的所有所有员工信息
public abstract Department queryAllByDepartmentId(Integer id);
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
<mapper namespace="com.mybatis.dao.DepartmentMapper">
    // dep任意,但是一定要和resultMap的值相同
    <resultMap id="dep" type="com.mybatis.entity.Department">
        <id property="departmentId" column="DEPARTMENT_ID" javaType="int" jdbcType="INTEGER"></id>
        <result property="description" column="DESCRIPTION" javaType="string" jdbcType="VARCHAR"></result>
        <result property="location" column="LOCATION" javaType="string" jdbcType="VARCHAR"></result>
        // Department中有一个List<Employee>集合,使用ofType引入Employee对象
        <collection property="employees" ofType="com.mybatis.entity.Employee">
            <id property="employeeId" column="EMPLOYEE_ID" javaType="int" jdbcType="INTEGER"></id>
            <result property="employeeName" column="EMPLOYEE_NAME" javaType="string" jdbcType="VARCHAR"></result>
            <result property="employeePassword" column="EMPLOYEE_PASSWORD" javaType="string" jdbcType="VARCHAR"></result>
            <result property="employeeAge" column="EMPLOYEE_AGE" javaType="string" jdbcType="VARCHAR"></result>
            <result property="departmentId" column="DEPARTMENT_ID" javaType="int" jdbcType="INTEGER"></result>
        </collection>
    </resultMap>
 
    <select id="queryAllByDepartmentId" resultMap="dep">
        SELECT
            d.DEPARTMENT_ID,
            d.DESCRIPTION,
            d.LOCATION,
            e.EMPLOYEE_ID,
            e.EMPLOYEE_NAME,
            e.EMPLOYEE_PASSWORD,
            e.EMPLOYEE_AGE
        FROM department d
        LEFT JOIN
          employee e
        ON
          d.DEPARTMENT_ID=e.DEPARTMENT_ID
        WHERE d.DEPARTMENT_ID=#{departmentId}
    </select>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
// 测试结果
Department{
    departmentId=1,
    description='市场部',
    location='北京',
    employees=
    [
        Employee{employeeId=1, employeeName='张三', employeePassword='123', employeeAge='23', departmentId=1},
        Employee{employeeId=5, employeeName='田七', employeePassword='123579', employeeAge='27', departmentId=1}
    ]
}

  

  

posted @   变体精灵  阅读(308)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示