MyBatis 数据访问
到这里,HTTP 层、响应模型、Bean 装配、启动配置和 Profile 已经就位。接下来要把数据访问接进应用。
Feat Cloud 的 MyBatis 集成不试图替代 MyBatis。它做的是另一件事:在编译期把 Mapper 接进 Feat Cloud 的 Bean 生命周期,让 Controller 可以像注入普通 Bean 一样使用它。
一条完整的数据访问链路
Section titled “一条完整的数据访问链路”这一章会搭出一个最小的 CRUD 链路:
- 访问
/users能查到所有用户 - 访问
/users/{username}能查到单个用户 - 访问
/users/role/{role}能按角色查用户 POST /users能创建用户PUT /users能更新用户DELETE /users/{username}能删除用户
最终会涉及这些文件:
src/main/resources/├── feat.yaml├── mybatis/mybatis-config.xml└── mybatis/ddl/schema.sql
src/main/java/tech/smartboot/feat/demo/mybatis/├── Bootstrap.java├── entity/User.java├── mapper/UserMapper.java├── service/UserService.java└── controller/UserController.java你可以按这个顺序理解后面的内容:先准备资源文件,再写 Mapper 和 Service,最后把能力暴露成 HTTP 接口。
准备资源与启动入口
Section titled “准备资源与启动入口”这一部分先把项目跑起来所需的资源准备好:Maven 依赖、feat.yaml、MyBatis 配置、初始化 SQL,以及一个干净的启动类。
在已有 Feat Cloud 应用基础上,引入 MyBatis 和数据库驱动。下面的示例使用 H2 内存数据库,适合先把链路跑通:
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version></dependency>
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>${h2.version}</version></dependency>示例中 MyBatis 和 H2 的版本号使用占位符,实际使用时请替换为当前稳定版本。feat-cloud 与 feat-cloud-starter 仍按快速开始中的方式保留:前者提供运行期,后者负责生成 MyBatis 接入代码。
让 Feat Cloud 找到 MyBatis
Section titled “让 Feat Cloud 找到 MyBatis”在 src/main/resources/feat.yaml 里声明 MyBatis 主配置和初始化 SQL:
feat: mybatis: path: mybatis/mybatis-config.xml initial-sql: mybatis/ddl/schema.sqlpath 指向 MyBatis 主配置;initial-sql 指向启动时要执行的初始化脚本。它们都属于编译期配置输入,和前一章讲的 feat.yml / feat-{env}.yml 一样,修改后需要重新构建。
准备 MyBatis 自己的资源
Section titled “准备 MyBatis 自己的资源”这一段只处理 src/main/resources 下的文件。它们负责告诉 Feat Cloud 和 MyBatis:配置在哪里,数据库怎么初始化,Mapper 包从哪里加载。
示例里的 mybatis-config.xml 只做三件事:
- 打开 SQL 日志
- 配置 H2 数据源
- 注册 mapper 包
<configuration> <settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> <environments default="h2_mem"> <environment id="h2_mem"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="org.h2.Driver"/> <property name="url" value="jdbc:h2:mem:feat-demo;NON_KEYWORDS=value;mode=mysql;"/> </dataSource> </environment> </environments> <mappers> <package name="tech.smartboot.feat.demo.mybatis.mapper"/> </mappers></configuration>schema.sql 负责建表并插入几条初始数据。这样后面的 Controller 和 Service 能立刻验证,不需要再手动准备数据库。
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info ( username VARCHAR(64) PRIMARY KEY, password VARCHAR(128), `desc` VARCHAR(255), role VARCHAR(32));
INSERT INTO user_info(username, password, `desc`, role)VALUES ('feat', '123456', 'Feat admin user', 'admin'), ('guest', '123456', 'Guest user', 'user');到这里,配置和数据库初始化资源已经就位。接下来开始写 Java 代码。
保持启动入口干净
Section titled “保持启动入口干净”示例里的启动类非常简单:
import tech.smartboot.feat.cloud.FeatCloud;import tech.smartboot.feat.cloud.annotation.Bean;
@Beanpublic class Bootstrap { public static void main(String[] args) { FeatCloud.cloudServer().listen(); }}这个类本身几乎不做业务事,但它给了 Feat Cloud 一个明确的启动入口。
MyBatis 的配置接入,主要是靠前面的 feat.yaml 和资源文件完成的。
编写数据访问代码
Section titled “编写数据访问代码”这一段只负责数据访问层:User 表示一行业务数据,UserMapper 仍然按 MyBatis 的方式声明 SQL。
User 实体
Section titled “User 实体”package tech.smartboot.feat.demo.mybatis.entity;
public class User { private String username; private String password; private String desc; private String role;
public String getUsername() { return username; } public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; } public void setPassword(String password) { this.password = password; }
public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; }
public String getRole() { return role; } public void setRole(String role) { this.role = role; }}MyBatis 用字段映射数据库列,Feat Cloud 用 getter 做 JSON 序列化,所以实体里需要同时有字段和 getter。
UserMapper
Section titled “UserMapper”package tech.smartboot.feat.demo.mybatis.mapper;
import org.apache.ibatis.annotations.Delete;import org.apache.ibatis.annotations.Insert;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;import org.apache.ibatis.annotations.Select;import org.apache.ibatis.annotations.Update;import tech.smartboot.feat.demo.mybatis.entity.User;
import java.util.List;
@Mapperpublic interface UserMapper {
@Select("SELECT * FROM user_info WHERE username = #{username}") User selectByUsername(@Param("username") String username);
@Select("SELECT * FROM user_info") List<User> selectAll();
@Select("SELECT * FROM user_info WHERE role = #{role}") List<User> selectByRole(@Param("role") String role);
@Insert("INSERT INTO user_info(username, password, `desc`, role) VALUES(#{username}, #{password}, #{desc}, #{role})") int insert(User user);
@Update("UPDATE user_info SET password=#{password}, `desc`=#{desc}, role=#{role} WHERE username=#{username}") int update(User user);
@Delete("DELETE FROM user_info WHERE username = #{username}") int deleteByUsername(@Param("username") String username);}这里的关键不是 MyBatis 注解语法,而是职责边界:
- Mapper 仍然是纯 MyBatis 思维
- Feat Cloud 负责把它接进应用上下文
UserService
Section titled “UserService”示例项目没有直接在 Controller 里操作 Mapper,而是先过一层 Service:
package tech.smartboot.feat.demo.mybatis.service;
import tech.smartboot.feat.cloud.annotation.Autowired;import tech.smartboot.feat.cloud.annotation.Bean;import tech.smartboot.feat.demo.mybatis.entity.User;import tech.smartboot.feat.demo.mybatis.mapper.UserMapper;
import java.util.List;
@Beanpublic class UserService {
@Autowired private UserMapper userMapper;
public User findByUsername(String username) { return userMapper.selectByUsername(username); }
public List<User> findAll() { return userMapper.selectAll(); }
public List<User> findByRole(String role) { return userMapper.selectByRole(role); }
public boolean insert(User user) { try { return userMapper.insert(user) > 0; } catch (Exception e) { e.printStackTrace(); return false; } }
public boolean update(User user) { try { return userMapper.update(user) > 0; } catch (Exception e) { e.printStackTrace(); return false; } }
public boolean deleteByUsername(String username) { try { return userMapper.deleteByUsername(username) > 0; } catch (Exception e) { e.printStackTrace(); return false; } }}这一层让职责保持清楚:
- Controller 负责 HTTP
- Service 负责业务动作
- Mapper 负责数据库访问
这样后面继续扩展权限、事务、校验时,结构不会乱。
暴露并验证 HTTP 接口
Section titled “暴露并验证 HTTP 接口”资源文件、Mapper 和 Service 都准备好之后,最后一步是把这条链路接到 HTTP 上。
UserController
Section titled “UserController”最后在 Controller 里把这条链路接到 HTTP 上:
package tech.smartboot.feat.demo.mybatis.controller;
import tech.smartboot.feat.cloud.RestResult;import tech.smartboot.feat.cloud.annotation.Autowired;import tech.smartboot.feat.cloud.annotation.Controller;import tech.smartboot.feat.cloud.annotation.PathParam;import tech.smartboot.feat.cloud.annotation.RequestMapping;import tech.smartboot.feat.cloud.annotation.RequestMethod;import tech.smartboot.feat.demo.mybatis.entity.User;import tech.smartboot.feat.demo.mybatis.service.UserService;
import java.util.List;
@Controllerpublic class UserController {
@Autowired private UserService userService;
@RequestMapping("/users") public RestResult<List<User>> getAllUsers() { return RestResult.ok(userService.findAll()); }
@RequestMapping("/users/{username}") public RestResult<User> getUserByUsername(@PathParam("username") String username) { User user = userService.findByUsername(username); return user != null ? RestResult.ok(user) : RestResult.fail("User not found"); }
@RequestMapping("/users/role/{role}") public RestResult<List<User>> getUsersByRole(@PathParam("role") String role) { return RestResult.ok(userService.findByRole(role)); }
@RequestMapping(value = "/users", method = RequestMethod.POST) public RestResult<String> createUser(User user) { return userService.insert(user) ? RestResult.ok("User created successfully") : RestResult.fail("Failed to create user"); }
@RequestMapping(value = "/users", method = RequestMethod.PUT) public RestResult<String> updateUser(User user) { return userService.update(user) ? RestResult.ok("User updated successfully") : RestResult.fail("Failed to update user"); }
@RequestMapping(value = "/users/{username}", method = RequestMethod.DELETE) public RestResult<String> deleteUser(@PathParam("username") String username) { return userService.deleteByUsername(username) ? RestResult.ok("User deleted successfully") : RestResult.fail("Failed to delete user"); }}注意 createUser(User user) 和 updateUser(User user) 的参数没有加任何注解。这个写法在 Controller 与请求处理章节已经出现过:Feat Cloud 检测到参数类型是自定义 POJO 时,会自动从 JSON 请求体里反序列化出这个对象。
到这一步,链路已经完整了:
HTTP -> Controller -> Service -> Mapper -> DB
验证整条链路
Section titled “验证整条链路”启动应用后,可以先验证读接口:
curl http://localhost:8080/userscurl http://localhost:8080/users/featcurl http://localhost:8080/users/role/admin再验证写接口:
curl -X POST http://localhost:8080/users \ -H 'Content-Type: application/json' \ -d '{"username":"new_user","password":"123456","desc":"created from docs","role":"user"}'
curl -X PUT http://localhost:8080/users \ -H 'Content-Type: application/json' \ -d '{"username":"new_user","password":"654321","desc":"updated from docs","role":"admin"}'
curl -X DELETE http://localhost:8080/users/new_user如果这些请求都能返回合理结果,说明:
- Feat Cloud 启动成功
- MyBatis 配置被正确读取
- 初始化 SQL 生效
- Mapper / Service / Controller 整条链路都打通了
Feat Cloud 负责接线,MyBatis 仍负责访问数据
Section titled “Feat Cloud 负责接线,MyBatis 仍负责访问数据”在 Feat Cloud 里,MyBatis 集成不是重写一套数据访问层。Mapper 仍按 MyBatis 的方式定义,SQL 仍由 MyBatis 执行,Service 仍组织业务动作。
Feat Cloud 负责的是另一半:读取 feat.yaml,在编译期生成 Mapper 接入和 Bean 装配代码,让 Controller 可以通过 @Autowired 使用数据访问层。理解这个边界之后,后续切换数据库、拆分环境配置或进入部署阶段都会更清晰。