- 文档
- 教程
- 后端
【免费下载链接】CodeGuide
:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
本篇技术指南以《SpringBoot 中间件设计和开发》专栏第 7 章为核心,讲解如何从零实现一个类似 MyBatis 的 ORM 对象关系映射框架。文章会带你走通「配置文件加载 → XML 解析 → SqlSessionFactory 构建 → SqlSession 执行 SQL → 结果集反射映射」的完整链路,并结合仓库中《基于 jdbc 实现一个 Demo 版的 Mybatis》等源码级资料,说明每一层设计背后的原理与可扩展点,帮助你理解 ORM 中间件屏蔽底层异构、提供统一服务的本质,并为后续与 Spring、SpringBoot 整合打下基础。
一、前言:中间件如何"屏蔽底层异构"
在互联网应用开发的演进过程中,中间件这一层的建设基本来自业务系统中非业务逻辑的通用性核心功能抽离,逐步形成各类中间件服务。中间件介于操作系统和应用软件之间,为应用软件提供服务功能,有消息中间件、通信中间件、应用服务器等。因为介于两种软件之间,所以称为中间件。
我们的生活中也有大量"一站式服务"为用户屏蔽底层异构:装修公司解决设计、人工、材料、品牌、搭配的问题;婚庆公司解决酒店、婚车、行程、司仪、布景的问题;旅游公司解决路线、行程、吃住、门票、照片的问题。这些服务的共同点,就是让用户有一个简单、单一、容易的方式来使用服务和功能。
在代码开发中,这种思想同样可以映射到中间件功能里——解决底层差异,提供统一服务。ORM 框架正是这样一类中间件:JDBC 直接操作数据库的底层 API 繁琐且异构,而 ORM 把「数据库连接、SQL 执行、结果集转换」等细节封装起来,让上层业务代码以对象的方式操作数据。
二、需求背景:从 JDBC 到 ORM 的演进
初学 Java 时,我们大多直接使用 JDBC 方式进行数据库连接操作。随着学习的深入,陆续接触到 iBatis、MyBatis、Hibernate,这些就是 ORM 组件的具体产品实现。
ORM(Object Relational Mapping,对象关系映射)是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换,让我们可以更方便地使用数据库。
那么,类似 MyBatis 这样的 ORM 框架是如何实现的呢?本章节就以实现一个 ORM 框架为目标,看看该怎么设计和实现。
需要说明的是,本章节的 ORM 框架实现,只是模拟开发一个类似 MyBatis SpringBoot Starter 的第一步。在《SpringBoot 中间件设计和开发》专栏中,这一章只是起点,后续会逐步实现以下内容:
- 第 8 章:ORM 框架与 Spring 集合 —— 把自己实现的 ORM 框架与 Spring 结合,交给 Spring 管理,实现类似 MyBatis-Spring 的效果;
- 第 9 章:结合 SpringBoot 开发 ORM Starter —— 把这个自己实现的 MyBatis 与 SpringBoot 结合,开发出类似 mybatis-spring-boot-starter 的 ORM 组件。
也就是说,本文介绍的 ORM 框架实现,是整个"手写 MyBatis 全家桶"的根基,第 8、9 章均建立在这一章实现的核心类之上。
三、整体设计:Demo 版 ORM 的核心类
在动手写代码之前,先梳理清楚一个 ORM 框架的核心骨架。仓库中的 《源码分析 | 基于 jdbc 实现一个 Demo 版的 Mybatis》 一文给出了完整的工程结构,这个 Demo 版本并非把所有 MyBatis 全部实现一遍,而是拨丝抽茧把最核心的内容展示出来。从使用上你会感受一模一样,但实现类已经被全部替换,核心类包括:
Configuration 配置类,承载数据源、连接、mapper 语句映射 DefaultSqlSession 会话实现类,执行 SQL、返回结果 DefaultSqlSessionFactory SqlSessionFactory 的实现类 Resources 资源加载类,通过 ClassLoader 读取配置文件 SqlSession 会话接口,定义查询与关闭方法 SqlSessionFactory 会话工厂接口 SqlSessionFactoryBuilder 构建入口,解析 XML 生成配置与工厂 XNode XML 节点封装,保存语句 id、参数、SQL 等信息这些类之间的关系很清晰:
- Resources负责把配置文件路径转换成 Reader 流;
- SqlSessionFactoryBuilder读取并解析 XML,把数据源、连接、mapper 语句解析结果塞进Configuration;
- 拿着 Configuration 构建出DefaultSqlSessionFactory;
- 调用工厂的
openSession()创建DefaultSqlSession; - 会话根据 statement 定位XNode(封装了 SQL、参数顺序、结果类型),用 JDBC 的
PreparedStatement执行,再通过反射把 ResultSet 转换为结果对象。
对照 MyBatis 的真实结构(SqlSessionFactoryBuilder → SqlSessionFactory → SqlSession → Executor → StatementHandler 等),这个 Demo 版把 MyBatis 最核心的调用骨架保留了下来,只是做了大幅简化。
四、环境与依赖准备
实现该 ORM 框架所需的运行环境与依赖如下(取自 Demo 版 Mybatis 案例工程):
- JDK 1.8
- IDEA(案例基于 2019.3.1 开发)
- dom4j 1.6.1(用于解析 XML 配置文件)
在 Maven 工程的pom.xml中引入 dom4j 依赖:
<dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency>五、代码实现:核心链路逐层拆解
1. 加载 XML 配置文件:Resources 资源类
整个框架的入口是Resources.getResourceAsReader(resource),通过配置文件地址获取读取流,为后面的解析做基础。
public class Resources { public static Reader getResourceAsReader(String resource) throws IOException { return new InputStreamReader(getResourceAsStream(resource)); } private static InputStream getResourceAsStream(String resource) throws IOException { ClassLoader[] classLoaders = getClassLoaders(); for (ClassLoader classLoader : classLoaders) { InputStream inputStream = classLoader.getResourceAsStream(resource); if (null != inputStream) { return inputStream; } } throw new IOException("Could not find resource " + resource); } private static ClassLoader[] getClassLoaders() { return new ClassLoader[]{ ClassLoader.getSystemClassLoader(), Thread.currentThread().getContextClassLoader()}; } }这段代码依次做了两件事:
- 获取 ClassLoader 集合,最大限度搜索配置文件。这里同时使用系统类加载器与线程上下文类加载器,是为了兼容不同运行环境下的类加载策略(例如在 Web 容器或 Spring 容器中,资源可能由线程上下文类加载器加载);
- 通过
classLoader.getResourceAsStream读取配置资源,找到后立即返回,否则抛出IOException("Could not find resource " + resource)。
2. 配置文件格式:数据源与 Mapper 映射
我们采用 MyBatis 的配置文件结构进行解析,在不破坏原有结构的情况下最大可能贴近源码。MyBatis 单独使用时会用到两个配置文件:数据源配置、Mapper 映射配置。
mybatis-config-datasource.xml —— 数据源配置
<?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://127.0.0.1:3306/itstack?useUnicode=true"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/User_Mapper.xml"/> <mapper resource="mapper/School_Mapper.xml"/> </mappers> </configuration>User_Mapper.xml —— Mapper 映射配置
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.itstack.demo.dao.IUserDao"> <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User"> SELECT id, name, age, createTime, updateTime FROM user where id = #{id} </select> <select id="queryUserList" parameterType="org.itstack.demo.po.User" resultType="org.itstack.demo.po.User"> SELECT id, name, age, createTime, updateTime FROM user where age = #{age} </select> </mapper>这里需要理解几个关键点:
namespace用于标识 Mapper 的命名空间,在 Demo 版中与 DAO 接口全限定名对应;id是 SQL 语句的唯一标识,与namespace拼接后作为全局 key(如org.itstack.demo.dao.IUserDao.queryUserInfoById);parameterType声明入参类型,resultType声明结果映射的 POJO 类型;- SQL 中使用
#{id}占位符,运行时需要替换为 JDBC 的?占位符,并记录参数名与位置的对应关系。
3. 解析 XML 配置文件:SqlSessionFactoryBuilder
配置文件加载后开始解析操作,同样仿照 MyBatis 但进行简化。构建入口如下:
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);SqlSessionFactoryBuilder.build() —— 入口构建类
public DefaultSqlSessionFactory build(Reader reader) { SAXReader saxReader = new SAXReader(); try { Document document = saxReader.read(new InputSource(reader)); Configuration configuration = parseConfiguration(document.getRootElement()); return new DefaultSqlSessionFactory(configuration); } catch (DocumentException e) { e.printStackTrace(); } return null; }流程分两步:
- 通过读取流创建 XML 解析的
Document类(这里用的是 dom4j 的 SAXReader); parseConfiguration解析 XML 文件,把连接池、数据源、mapper 关系设置到配置类中。
parseConfiguration() —— 解析过程
private Configuration parseConfiguration(Element root) { Configuration configuration = new Configuration(); configuration.setDataSource(dataSource(root.selectNodes("//dataSource"))); configuration.setConnection(connection(configuration.dataSource)); configuration.setMapperElement(mapperElement(root.selectNodes("mappers"))); return configuration; }从 XML 内容可以看到,需要解析出数据库连接池信息dataSource,以及数据库语句映射关系mappers。
dataSource() —— 解析出数据源
private Map<String, String> dataSource(List<Element> list) { Map<String, String> dataSource = new HashMap<>(4); Element element = list.get(0); List content = element.content(); for (Object o : content) { Element e = (Element) o; String name = e.attributeValue("name"); String value = e.attributeValue("value"); dataSource.put(name, value); } return dataSource; }这个过程比较简单,只需要将数据源信息(driver、url、username、password)以键值对形式取出即可。
connection() —— 获取数据库连接
private Connection connection(Map<String, String> dataSource) { try { Class.forName(dataSource.get("driver")); return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password")); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } return null; }这就是 JDBC 最原始的代码:先Class.forName加载驱动,再通过DriverManager.getConnection建立数据库连接。注意:这个 Demo 版为简化流程直接持有单一 Connection,真实生产环境应由连接池管理。
mapperElement() —— 解析 SQL 语句
private Map<String, XNode> mapperElement(List<Element> list) { Map<String, XNode> map = new HashMap<>(); Element element = list.get(0); List content = element.content(); for (Object o : content) { Element e = (Element) o; String resource = e.attributeValue("resource"); try { Reader reader = Resources.getResourceAsReader(resource); SAXReader saxReader = new SAXReader(); Document document = saxReader.read(new InputSource(reader)); Element root = document.getRootElement(); //命名空间 String namespace = root.attributeValue("namespace"); // SELECT List<Element> selectNodes = root.selectNodes("select"); for (Element node : selectNodes) { String id = node.attributeValue("id"); String parameterType = node.attributeValue("parameterType"); String resultType = node.attributeValue("resultType"); String sql = node.getText(); // ? 匹配 Map<Integer, String> parameter = new HashMap<>(); Pattern pattern = Pattern.compile("(#\\{(.*?)})"); Matcher matcher = pattern.matcher(sql); for (int i = 1; matcher.find(); i++) { String g1 = matcher.group(1); String g2 = matcher.group(2); parameter.put(i, g2); sql = sql.replace(g1, "?"); } XNode xNode = new XNode(); xNode.setNamespace(namespace); xNode.setId(id); xNode.setParameterType(parameterType); xNode.setResultType(resultType); xNode.setSql(sql); xNode.setParameter(parameter); map.put(namespace + "." + id, xNode); } } catch (Exception ex) { ex.printStackTrace(); } } return map; }这个过程的几个要点:
- 首先解析
<mappers>下所有<mapper resource="...">指向的 Mapper XML 文件,目前为了测试只解析select相关标签; - 所有的 SQL 语句为了确认唯一,使用
namespace + select 中的 id拼接作为 key,与 SQL 一起存放到 map 中; - MyBatis 的 SQL 语句配置中都有占位符用于传参,如
where id = #{id},所以需要将占位符#{id}替换为 JDBC 的问号?,同时把占位符的顺序(位置)与名称存放到 map 结构中,方便后续设置查询时的入参。
XNode就是这个映射节点的封装类,保存 namespace、id、parameterType、resultType、sql(已替换占位符)与参数位置映射:
public class XNode { private String namespace; private String id; private String parameterType; private String resultType; private String sql; private Map<Integer, String> parameter; // getter / setter 略 }4. 构建 DefaultSqlSessionFactory
最后,将初始化后的配置类 Configuration 作为参数创建 DefaultSqlSessionFactory。
public class DefaultSqlSessionFactory implements SqlSessionFactory { private final Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } @Override public SqlSession openSession() { return new DefaultSqlSession(configuration.connection, configuration.mapperElement); } }这个类非常简单:构造函数只接收配置类入参;实现SqlSessionFactory.openSession(),用于创建 DefaultSqlSession,从而可以执行 SQL 操作。
5. 开启 SqlSession
SqlSession session = sqlMapper.openSession();这一步就是创建 DefaultSqlSession。SqlSession接口定义如下:
public interface SqlSession { <T> T selectOne(String statement); <T> T selectOne(String statement, Object parameter); <T> List<T> selectList(String statement); <T> List<T> selectList(String statement, Object parameter); void close(); }接口能力与 MyBatis 的 SqlSession 对齐:selectOne查询单条,selectList查询集合,close关闭会话。
6. 执行 SQL:selectOne 与参数绑定
User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);DefaultSqlSession 中的 selectOne 实现:
public <T> T selectOne(String statement, Object parameter) { XNode xNode = mapperElement.get(statement); Map<Integer, String> parameterMap = xNode.getParameter(); try { PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql()); buildParameter(preparedStatement, parameter, parameterMap); ResultSet resultSet = preparedStatement.executeQuery(); List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType())); return objects.get(0); } catch (Exception e) { e.printStackTrace(); } return null; }执行流程梳理:
- 通过
statement从 map 中获取最初解析 XML 时存储的 select 标签信息(XNode); - 获取 SQL 语句后交给 JDBC 的
PreparedStatement执行——这也是 ORM 框架天然防 SQL 注入的原因之一,参数通过setXxx绑定而非字符串拼接; - 执行查询
preparedStatement.executeQuery()得到 ResultSet; - 通过
resultSet2Obj反射映射为resultType指定的 POJO; selectOne返回objects.get(0),selectList则返回全部结果。
参数绑定buildParameter的核心逻辑(抽取展示):
private void buildParameter(PreparedStatement preparedStatement, Object parameter, Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException { int size = parameterMap.size(); // 单个参数 if (parameter instanceof Long) { for (int i = 1; i <= size; i++) { preparedStatement.setLong(i, Long.parseLong(parameter.toString())); } return; } if (parameter instanceof Integer) { for (int i = 1; i <= size; i++) { preparedStatement.setInt(i, Integer.parseInt(parameter.toString())); } return; } if (parameter instanceof String) { for (int i = 1; i <= size; i++) { preparedStatement.setString(i, parameter.toString()); } return; } // 对象参数:反射读取字段 Map<String, Object> fieldMap = new HashMap<>(); Field[] declaredFields = parameter.getClass().getDeclaredFields(); for (Field field : declaredFields) { String name = field.getName(); field.setAccessible(true); Object obj = field.get(parameter); field.setAccessible(false); fieldMap.put(name, obj); } for (int i = 1; i <= size; i++) { String parameterDefine = parameterMap.get(i); Object obj = fieldMap.get(parameterDefine); if (obj instanceof Short) { preparedStatement.setShort(i, Short.parseShort(obj.toString())); continue; } if (obj instanceof Integer) { preparedStatement.setInt(i, Integer.parseInt(obj.toString())); continue; } if (obj instanceof Long) { preparedStatement.setLong(i, Long.parseLong(obj.toString())); continue; } if (obj instanceof String) { preparedStatement.setString(i, obj.toString()); continue; } if (obj instanceof Date) { preparedStatement.setDate(i, (java.sql.Date) obj); } } }两个分支逻辑:
- 单个参数(Long、Integer、String...)比较简单,直接把值 set 到对应占位符位置;
- 对象参数则通过反射获取类对象的所有 Field 属性,与解析 XML 时记录的参数 Map(位置 → 参数名)进行匹配后 set 值。
7. 结果集映射:反射封装 resultSet2Obj
参数设置完成后执行查询,接下来将查询结果转换为我们的类,核心是反射操作:
private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) { List<T> list = new ArrayList<>(); try { ResultSetMetaData metaData = resultSet.getMetaData(); int columnCount = metaData.getColumnCount(); // 每次遍历行值 while (resultSet.next()) { T obj = (T) clazz.newInstance(); for (int i = 1; i <= columnCount; i++) { Object value = resultSet.getObject(i); String columnName = metaData.getColumnName(i); String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1); Method method; if (value instanceof Timestamp) { method = clazz.getMethod(setMethod, Date.class); } else { method = clazz.getMethod(setMethod, value.getClass()); } method.invoke(obj, value); } list.add(obj); } } catch (Exception e) { e.printStackTrace(); } return list; }这里通过反射生成类对象,类类型定义在 SQL 标签的resultType上,要点包括:
- 通过
ResultSetMetaData获取列数量与列名; - 根据列名拼装
setXxx方法名(列名首字母大写),如id→setId,再通过clazz.getMethod找到对应 setter 反射调用; - 时间类型需要特殊判断:JDBC 返回的
Timestamp与 Java 的Date不是同一个类型,因此需要单独走Date.class分支处理。
这个映射逻辑演示了 ORM 中最常见的「列名 → 属性 → setter」约定式映射,真实 MyBatis 在此基础上还支持 resultMap、驼峰命名自动转换、TypeHandler 自定义类型处理器等,但底层思想一致。
8. 测试验证:单条查询与集合查询
先测试单条查询ApiLikeTest.test_queryUserInfoById():
@Test public void test_queryUserInfoById() { String resource = "spring/mybatis-config-datasource.xml"; Reader reader; try { reader = Resources.getResourceAsReader(resource); SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader); SqlSession session = sqlMapper.openSession(); try { User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L); System.out.println(JSON.toJSONString(user)); } finally { session.close(); reader.close(); } } catch (Exception e) { e.printStackTrace(); } }一切顺利结果如下:
{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000} Process finished with exit code 0可能乍一看这个测试类和 MyBatis 原生 API 的测试代码一模一样,其实它们引入的包不同:
MybatisApiTest.java 引入的包(MyBatis 原生)
import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder;ApiLikeTest.java 引入的包(本次自研 Demo)
import org.itstack.demo.like.Resources; import org.itstack.demo.like.SqlSession; import org.itstack.demo.like.SqlSessionFactory; import org.itstack.demo.like.SqlSessionFactoryBuilder;接口使用方式几乎完全一致,这正是中间件设计的目标——让使用方无感知底层实现差异。
再测试集合查询,入参为对象类型ApiLikeTest.test_queryUserList():
@Test public void test_queryUserList() { String resource = "spring/mybatis-config-datasource.xml"; Reader reader; try { reader = Resources.getResourceAsReader(resource); SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader); SqlSession session = sqlMapper.openSession(); try { User req = new User(); req.setAge(18); List<User> userList = session.selectList("org.itstack.demo.dao.IUserDao.queryUserList", req); System.out.println(JSON.toJSONString(userList)); } finally { session.close(); reader.close(); } } catch (Exception e) { e.printStackTrace(); } }测试结果:
[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}] Process finished with exit code 0从这两个测试可以看到:SQL 查询有入参、有不需要入参、有查询一个、有查询集合,只需在 SqlSession 上合理包装即可复用同一套解析、绑定、映射链路。
六、设计要点与可扩展方向
对照 MyBatis 源码,可以归纳出这个 Demo 版 ORM 的设计要点,以及后续演进方向:
- 配置解析层(Configuration):负责把 XML 中的 dataSource、mapper 解析成内存结构。真实 MyBatis 还会解析
<resultMap>、<if>/<where>/<foreach>动态 SQL、缓存配置等,本文 Demo 只解析了 select 与占位符; - 会话与执行层(SqlSession / Executor):Demo 版把 SQL 执行逻辑直接放在 DefaultSqlSession 中,MyBatis 则进一步拆出 Executor、StatementHandler、ParameterHandler、ResultSetHandler,通过责任链各司其职,这也是仓库中《手撸 Mybatis》专栏后续章节逐步完善的内容(从第 2 章"创建简单的映射器代理工厂"一直到第 22 章"MyBatis 设计模式");
- 参数与结果处理:Demo 版通过 instanceof 分支处理常见类型,MyBatis 用 TypeHandler 注册机制支持任意类型的读写转换,扩展性更强;
- 占位符与 SQL 注入:
#{id}统一替换为?并通过 PreparedStatement 绑定参数,从根本上避免了 SQL 拼接注入问题。关于预编译的更多细节可参考仓库中的《面经手册 · 第 32 篇:MyBatis 从 SQL 注入到预编译深度解析》。
另外,ORM 框架对接口的代理支持(只定义 Mapper 接口、不写实现类即可执行增删改查)也是其重要特性,这一机制的原理可参考仓库中的 《源码分析:Mybatis 接口没有实现类为什么可以执行增删改查》,以及《手撸 Mybatis 专栏》中关于映射器代理工厂的实现。
七、总结
本篇文章以 JDBC 为起点,逐步实现了一个简化版 ORM 框架的核心链路:
- 通过
Resources加载配置文件; - 通过
SqlSessionFactoryBuilder解析 XML,构建Configuration(数据源 + 连接 + mapper 语句映射); - 通过
DefaultSqlSessionFactory创建SqlSession; - 通过
DefaultSqlSession.selectOne/selectList完成参数绑定、SQL 执行、结果集反射映射。
学习完 MyBatis 核心源码,再亲手实现一下核心过程,就会非常清晰地理解整个流程,不会觉得自己的知识栈有漏洞。只有深入的学习,才能将这样的技术赋能到其他开发场景,例如给 ES 写一个类似"EsBatis"的查询组件。
同时,本章的 ORM 框架只是第一步。在《SpringBoot 中间件设计和开发》专栏的后续章节中,将把它与 Spring 结合(第 8 章),再结合 SpringBoot 开发出类似 mybatis-spring-boot-starter 的 ORM Starter(第 9 章),真正完成从"会用手写 ORM"到"能开发中间件"的能力跃迁。
- 文档
- 教程
- 后端
【免费下载链接】CodeGuide
:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
相关推荐
CodeGuide 中间件实战:自研 ORM 框架如何与 Spring 集合(MyBatis-Spring 核心原理剖析)
CodeGuide 中间件实战:自研 ORM 框架如何与 Spring 集合(MyBatis Spring 核心原理剖析) 本篇指南基于 CodeGuide 仓
文档教程后端setup-gcloud进阶用法:多服务账户切换与组件定制安装指南
setup gcloud进阶用法:多服务账户切换与组件定制安装指南 在Google Cloud CI/CD自动化流程中, setup gcloud GitHub
7天从零实现Go语言ORM框架GeeORM
7天从零实现Go语言ORM框架GeeORM 什么是ORM框架 ORM(Object Relational Mapping,对象关系映射)是一种编程技术,它通过在
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考