跳转到内容

MyBatis 数据访问

到这里,HTTP 层、响应模型、Bean 装配、启动配置和 Profile 已经就位。接下来要把数据访问接进应用。

Feat Cloud 的 MyBatis 集成不试图替代 MyBatis。它做的是另一件事:在编译期把 Mapper 接进 Feat Cloud 的 Bean 生命周期,让 Controller 可以像注入普通 Bean 一样使用它。

这一章会搭出一个最小的 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 接口。

这一部分先把项目跑起来所需的资源准备好:Maven 依赖、feat.yaml、MyBatis 配置、初始化 SQL,以及一个干净的启动类。

在已有 Feat Cloud 应用基础上,引入 MyBatis 和数据库驱动。下面的示例使用 H2 内存数据库,适合先把链路跑通:

pom.xml
<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-cloudfeat-cloud-starter 仍按快速开始中的方式保留:前者提供运行期,后者负责生成 MyBatis 接入代码。

src/main/resources/feat.yaml 里声明 MyBatis 主配置和初始化 SQL:

feat.yaml
feat:
mybatis:
path: mybatis/mybatis-config.xml
initial-sql: mybatis/ddl/schema.sql

path 指向 MyBatis 主配置;initial-sql 指向启动时要执行的初始化脚本。它们都属于编译期配置输入,和前一章讲的 feat.yml / feat-{env}.yml 一样,修改后需要重新构建。

这一段只处理 src/main/resources 下的文件。它们负责告诉 Feat Cloud 和 MyBatis:配置在哪里,数据库怎么初始化,Mapper 包从哪里加载。

示例里的 mybatis-config.xml 只做三件事:

  1. 打开 SQL 日志
  2. 配置 H2 数据源
  3. 注册 mapper 包
mybatis-config.xml
<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 能立刻验证,不需要再手动准备数据库。

schema.sql
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 代码。

示例里的启动类非常简单:

Bootstrap.java
import tech.smartboot.feat.cloud.FeatCloud;
import tech.smartboot.feat.cloud.annotation.Bean;
@Bean
public class Bootstrap {
public static void main(String[] args) {
FeatCloud.cloudServer().listen();
}
}

这个类本身几乎不做业务事,但它给了 Feat Cloud 一个明确的启动入口。 MyBatis 的配置接入,主要是靠前面的 feat.yaml 和资源文件完成的。

这一段只负责数据访问层:User 表示一行业务数据,UserMapper 仍然按 MyBatis 的方式声明 SQL。

User.java
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.java
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;
@Mapper
public 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 负责把它接进应用上下文

示例项目没有直接在 Controller 里操作 Mapper,而是先过一层 Service:

UserService.java
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;
@Bean
public 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 负责数据库访问

这样后面继续扩展权限、事务、校验时,结构不会乱。

资源文件、Mapper 和 Service 都准备好之后,最后一步是把这条链路接到 HTTP 上。

最后在 Controller 里把这条链路接到 HTTP 上:

UserController.java
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;
@Controller
public 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

启动应用后,可以先验证读接口:

查询接口验证
curl http://localhost:8080/users
curl http://localhost:8080/users/feat
curl 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 使用数据访问层。理解这个边界之后,后续切换数据库、拆分环境配置或进入部署阶段都会更清晰。