從單體架構(gòu)到分布式數(shù)據(jù)持久化,ORM 框架之 Mybatis
本文轉(zhuǎn)載自微信公眾號(hào)「會(huì)點(diǎn)代碼的大叔」,作者會(huì)點(diǎn)代碼的大叔 。轉(zhuǎn)載本文請(qǐng)聯(lián)系會(huì)點(diǎn)代碼的大叔公眾號(hào)。
1前置概念
01 持久化
持久化就是把數(shù)據(jù)保存到可以永久保存的存儲(chǔ)設(shè)備中,比如磁盤。
02 JDBC
大多數(shù)程序員在學(xué)習(xí) Java 的過程中,當(dāng)學(xué)習(xí)到 Java 訪問數(shù)據(jù)庫的時(shí)候,一定會(huì)先學(xué)習(xí) JDBC,它是一種用于執(zhí)行 SQL 語句的 Java API,為數(shù)據(jù)庫提供統(tǒng)一訪問,并把數(shù)據(jù)“持久化”到數(shù)據(jù)庫中。
我再通俗地解釋一下(對(duì) JDBC 有一定了解的同學(xué)可以直接跳過):
Sun 公司在 97 年發(fā)布 JDK1.1 ,JDBC 就是這個(gè)版本中一個(gè)重要的技術(shù)點(diǎn),要用 Java 語言連接數(shù)據(jù)庫,正常的思維都是 Sun 公司自己來實(shí)現(xiàn)如何連接數(shù)據(jù)庫、如果執(zhí)行 SQL 語句,但是市場(chǎng)上的數(shù)據(jù)庫太多了,數(shù)據(jù)庫之間的差異也很大,而且 Sun 公司也不可能了解每個(gè)數(shù)據(jù)庫的內(nèi)部細(xì)節(jié)吶...
于是為了讓 Java 代碼能更好地與數(shù)據(jù)庫連接,Sun 公司于是制定了一系列的接口,說是接口,其實(shí)也就是一套【標(biāo)準(zhǔn)】、一套【規(guī)范】,具體代碼如何實(shí)現(xiàn)由各個(gè)數(shù)據(jù)庫廠商來敲代碼;所以我們常說的“驅(qū)動(dòng)類”,就是各個(gè)廠商的實(shí)現(xiàn)類。
所以我們?cè)谟?JDBC 連接數(shù)據(jù)庫的時(shí)候,第一步需要注冊(cè)驅(qū)動(dòng),就是要告訴 JVM 使用的是操作哪個(gè)數(shù)據(jù)庫的實(shí)現(xiàn)類。
03 ORM
在沒有 ORM 框架之前,我們操作數(shù)據(jù)庫需要這樣:
我們可以看到使用 JDBC 操作數(shù)據(jù)庫,代碼比較繁瑣,參數(shù)拼寫在 SQL 中容易出錯(cuò),而且可讀性比較差,增加了代碼維護(hù)的難度。
有了 ORM 框架之后,我們操作數(shù)據(jù)庫是這樣的:
ORM 框架在 Java 對(duì)象和數(shù)據(jù)庫表之間做了一個(gè)映射,封裝了數(shù)據(jù)庫的訪問細(xì)節(jié),我們?cè)傩枰僮鲾?shù)據(jù)庫語句的時(shí)候,直接操作 Java 對(duì)象就可以了。
2Spring Boot 集成 MyBatis
Java 常用的 ORM 框架有 Hibernate、MyBatis、JPA 等等,我在后文中在比較這集中框架的優(yōu)缺點(diǎn),本章節(jié)主要介紹 Spring Boot 項(xiàng)目集成 MyBatis 訪問數(shù)據(jù)庫。
Step 1. 添加依賴
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
- <dependency>
- <groupId>org.mybatis.spring.boot</groupId>
- <artifactId>mybatis-spring-boot-starter</artifactId>
- <version>2.0.1</version>
- </dependency>
Step 2. 配置數(shù)據(jù)庫鏈接
在 application.yml 文件中配置數(shù)據(jù)庫相關(guān)信息。
- #數(shù)據(jù)源配置
- spring:
- datasource:
- #數(shù)據(jù)庫驅(qū)動(dòng)
- driver-class-name: com.mysql.cj.jdbc.Driver
- #數(shù)據(jù)庫 url
- url: jdbc:mysql://127.0.0.1:3306/arch?characterEncoding=UTF-8&serverTimezone=UTC
- #用戶名
- username: root
- #密碼
- password: root
Step 3. 配置數(shù)據(jù)庫鏈接
在我們本機(jī)的數(shù)據(jù)庫上,創(chuàng)建一個(gè)用戶表,并插入一條數(shù)據(jù):
- CREATE TABLE IF NOT EXISTS `user`(
- `id` INT UNSIGNED AUTO_INCREMENT,
- `userid` VARCHAR(100) NOT NULL,
- `username` VARCHAR(100) NOT NULL,
- `gender` CHAR(1) NOT NULL,
- `age` INT NOT NULL,
- PRIMARY KEY ( `id` )
- )ENGINE=InnoDB DEFAULT CHARSET=utf8;
- insert into user(userid ,username, gender, age) values('dashu','大叔','M',18);
Step 4. 創(chuàng)建 Model 層
通常用于接收數(shù)據(jù)庫中數(shù)據(jù)的對(duì)象,我會(huì)單獨(dú)創(chuàng)建一個(gè) model package,類中的屬性我習(xí)慣和字段保持相同。
- package com.archevolution.chapter4.model;
- public class User {
- private int id;//主鍵ID,自增長(zhǎng)
- private String userId;//用戶ID,或看做登錄名
- private String userName;//用戶姓名
- private String gender;//性別
- private int age;//年齡
- //省略 get、set、toString 方法
- }
Step 5. 創(chuàng)建 Dao 層
通常直接和數(shù)據(jù)庫打交道的代碼,我們把它們放在 DAO 層(Data Access Object),數(shù)據(jù)訪問邏輯全都在這里;
我們新建一個(gè) dao package,并在下面新建一個(gè)**接口**,注意是接口:
- @Mapper
- public interface UserDao {
- @Select("SELECT id, userId, userName, gender, age FROM USER WHERE id = #{id}")
- public User queryUserById(@Param("id") int id);
- }
這里我多說幾句!
從技術(shù)角度來說,model 中的屬性可以和表中的字段不一樣,比如我們?cè)跀?shù)據(jù)庫中增加一個(gè)手機(jī)號(hào)的字段叫做 [mobilephone] :
- --增加手機(jī)號(hào)字段
- ALTER TABLE user ADD mobilephone varchar(15) ;
- --更新 userid = 1 數(shù)據(jù)的手機(jī)號(hào)
- update user set mobilephone = '13800000000' where userid = '1';
我們?cè)?User.java 中添加一個(gè)字段,叫做 [telephone]:
我們自己知道 [telephone] 是和數(shù)據(jù)庫中的 [mobilephone] 對(duì)應(yīng),但是如何讓 Mybatis 知道這兩個(gè)字段要對(duì)應(yīng)上呢?有幾個(gè)辦法:
01. 在 SQL 語句中控制,對(duì)名字不相同的字段起別名:
- @Select("SELECT id, userId, userName, gender, age, mobilephone as telephone FROM USER WHERE id = #{id}")
- public User queryUserTelById(@Param("id") int id);
02. 使用 @Results 標(biāo)簽,將屬性和字段不相同的設(shè)置映射(名稱相同的可以不寫):
- @Select("SELECT id, userId, userName, gender, age, mobilephone FROM USER WHERE id = #{id}")
- @Results({
- @Result(property = "telephone" , column = "mobilephone")
- })
- public User queryUserTelById2(@Param("id") int id);
不過我還是建議大家在寫 model 類的時(shí)候,屬性和表中的字段保持一模一樣,這樣不僅可以減少代碼的復(fù)雜程度,還能很大程度地增加代碼的可讀性,減少出錯(cuò)的可能;
有些同學(xué)可能會(huì)有疑問,很多項(xiàng)目的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)的不是那么規(guī)范,比如字段名稱可能是一個(gè)很奇怪的名字,比如 flag01、flag02,如果這樣的字段從數(shù)據(jù)庫中查詢出來,通過接口返回,那么會(huì)不會(huì)造成接口的可讀性太差?
通常我們不會(huì)把 model 中的內(nèi)容直接包裝返回,model 很多的是數(shù)據(jù)庫和 Java 對(duì)象的映射,而傳輸數(shù)據(jù)的話,通常需要 DTO;我們從數(shù)據(jù)庫中查詢出來數(shù)據(jù)放到 model 中,在接口中返回?cái)?shù)據(jù)之前,把 model 轉(zhuǎn)換成 DTO,而 DTO 中的屬性需要保證其規(guī)范性和見名知意。
Step 6. 創(chuàng)建 Service 層
我們的項(xiàng)目現(xiàn)在已經(jīng)有了 Dao 層,用于訪問數(shù)據(jù),有 Controller 層,用戶提供接口訪問,那么 Controller 是否能直接調(diào)用 Dao 中的方法呢?最好不要直接調(diào)用!
通常我們會(huì)創(chuàng)建一個(gè) Service 層,用于存放業(yè)務(wù)邏輯,這時(shí)候完整的調(diào)用流程是:
Controller - Service - Dao
創(chuàng)建 Service package 之后,在里面創(chuàng)建一個(gè) UserService :
- @Service
- public class UserService {
- @Autowired
- UserDao userDao;
- public User queryUserById(int id) {
- return userDao.queryUserById(id);
- }
- }
Step 7. 在 Controller 層增加接口
增加一個(gè)接口,通過 userId 查詢客戶信息,并返回客戶信息:
- @RequestMapping(value = "/queryUser/{id}")
- @ResponseBody
- public String queryUserById(@PathVariable("id") int id){
- User user = userService.queryUserById(id);
- return user == null ? "User is not find" : user.toString() ;
- }
Step 8. 測(cè)試驗(yàn)證
在瀏覽器或客戶端中訪問接口進(jìn)行調(diào)試測(cè)試,可以查詢到客戶信息:
- http://127.0.0.1:8088/queryUser/1
- User [id=1, userId=dashu, userName=大叔, gender=M, age=18, telephone=null]
03MyBatis 的其他操作
只給出關(guān)鍵代碼,完整代碼請(qǐng)參考本章節(jié)的項(xiàng)目代碼。
01. 新增
- @Insert("INSERT INTO USER(userId, userName, gender, age) values"
- + " (#{userId}, #{userName}, #{gender}, #{age})")
- public void insertUser(User user);
02. 修改
- @Update("UPDATE USER SET mobilephone = #{telephone} WHERE id = #{id}")
- public void updateUserTel(User user);
03. 刪除
- @Delete("DELETE FROM USER WHERE id = #{id}")
- public void deleteUserById(@Param("id") int id);
4代碼完善
上面我們就完成了 Spring Boot 和 MyBatis 最簡(jiǎn)單的集成,可以正常地讀取數(shù)據(jù)庫做 CRUD 了,但是因?yàn)槭亲詈?jiǎn)單的集成,所以有一些細(xì)節(jié)需要完善一下,比如:
參數(shù)都在顯示在了 url 中;
直接返回 Object.toString(), 不是很友好;
查詢不到數(shù)據(jù)或發(fā)生異常,沒有做特殊處理;
下面讓我們逐步完善
01. 使用 Json 作為參數(shù)發(fā)送 Post 請(qǐng)求
如果嚴(yán)格地遵守 Restful 風(fēng)格,那么需要遵守:
查詢:GET /url/xxx
新增:POST /url
修改:PUT /url/xxx
刪除:DELETE /url/xxx
在這里我們就單純地認(rèn)為把參數(shù)寫在 url 中,容易一眼就看到我們的參數(shù)內(nèi)容,并且如果參數(shù)比較多的時(shí)候會(huì)造成 url 過長(zhǎng),所以通常我們比較習(xí)慣使用 Json 作為參數(shù)發(fā)送 Post 請(qǐng)求。比如新增 User 的接口可以寫成這樣:
新增 DTO package 并新建 UserDTO:
- //使用了 Josn 作為參數(shù),需要設(shè)置 headers = {"content-type=application/json"}
- //@RequestBody UserDto userDto 可以讓 JSON 串自動(dòng)和 UserDto 綁定和轉(zhuǎn)換
- @RequestMapping(value = "/insertUser2",headers = {"content-type=application/json"})
- @ResponseBody
- public String insertUser2(@RequestBody UserDto userDto){
- //DTO 轉(zhuǎn)成 Model
- User user = new User();
- user.setUserId(userDto.getUserId());
- user.setUserName(userDto.getUserName());
- user.setGender(userDto.getGender());
- user.setAge(userDto.getAge());
- userService.insertUser(user);
- return "Success" ;
- }
新增 User 的接口:
- //使用了 Josn 作為參數(shù),需要設(shè)置 headers = {"content-type=application/json"}
- //@RequestBody UserDto userDto 可以讓 JSON 串自動(dòng)和 UserDto 綁定和轉(zhuǎn)換
- @RequestMapping(value = "/insertUser2",headers = {"content-type=application/json"})
- @ResponseBody
- public String insertUser2(@RequestBody UserDto userDto){
- //DTO 轉(zhuǎn)成 Model
- User user = new User();
- user.setUserId(userDto.getUserId());
- user.setUserName(userDto.getUserName());
- user.setGender(userDto.getGender());
- user.setAge(userDto.getAge());
- userService.insertUser(user);
- return "Success" ;
- }
讓我們調(diào)用接口測(cè)試一下:
- {
- "userId": "lisi",
- "userName": "李四",
- "gender": "F",
- "age": "40",
- "telephone": "18600000000"
- }
02. 規(guī)范回參
直接返回 Object.toString(), 不是很友好;
讓我們?cè)O(shè)計(jì)一個(gè)簡(jiǎn)單的回參對(duì)象,包括 code-狀態(tài)碼,message-異常信息描述,data-數(shù)據(jù):
- public class JsonResponse {
- private String code;
- private String message;
- private Object data;
- //省略 set、get 方法
- }
其中 code 我們就參考 Http 狀態(tài)碼,使用常用的幾個(gè):
- public class ResponseCode {
- public static final String SUCCESS = "200";//查詢成功
- public static final String SUCCESS_NULL = "204";//查詢成功,但是沒有數(shù)據(jù)
- public static final String PARAMETERERROR = "400";//參數(shù)錯(cuò)誤
- public static final String FAIL = "500";//服務(wù)器異常
- }
這時(shí)我們?cè)賮碇貙懸幌虏樵兘涌冢?/p>
- @RequestMapping(value = "/queryUser2")
- @ResponseBody
- public JsonResponse queryUser2ById(@RequestBody UserDto userDto){
- JsonResponse res = new JsonResponse();
- //省略參數(shù)校驗(yàn)
- User user = userService.queryUserById(userDto.getUserId());
- if(user != null){
- //能查詢到結(jié)果,封裝到回參中
- res.setCode(ResponseCode.SUCCESS);
- res.setData(user);;
- }else{
- //如果查詢不到結(jié)果,則返回 '204'
- res.setCode(ResponseCode.SUCCESS_NULL);
- res.setMessage("查詢不到數(shù)據(jù)");
- }
- return res;
- }
調(diào)用結(jié)果可以看到封裝后的回參,看起來是不是規(guī)范了很多:
- {
- "code": "200",
- "message": null,
- "data": {
- "id": 3,
- "userId": "lisi",
- "userName": "李四",
- "gender": "F",
- "age": 40,
- "telephone": null
- }
- }
03. 異常處理
如果代碼在運(yùn)行過程中發(fā)生異常,那么改如何處理呢?直接把異常信息返回給前端么?這樣做對(duì)調(diào)用方不是很友好,通常我們把錯(cuò)誤日志打印到本地,給調(diào)用方返回一個(gè)異常狀態(tài)碼即可。
Service、Dao 層的集成都往上拋:
- public User queryUserById(int userId) throws Exception{
- return userDao.queryUserById(userId);
- }
在 Controller 層抓住異常,并封裝回參:
- User user = new User();
- try {
- user = userService.queryUserById(userDto.getId());
- } catch (Exception e) {
- res.setCode(ResponseCode.FAIL);
- res.setMessage("服務(wù)異常");
- }
4MyBatis 常見問題
01. 為什么 MyBatis 被稱為半自動(dòng) ORM 框架?
有半自動(dòng)就會(huì)有全自動(dòng);
Hibernate 就屬于全自動(dòng) ORM 框架,使用 Hibernate 可以完全根據(jù)對(duì)象關(guān)系模型進(jìn)行操作,也就是指操作 Java 對(duì)象不需要寫 SQL,因此是全自動(dòng)的;而 MyBatis 在關(guān)聯(lián)對(duì)象的時(shí)候,需要手動(dòng)編寫 SQL 語句,因此被稱作“半自動(dòng)”。
02. 使用注解還是 XML?
相信大部分項(xiàng)目使用 MyBatis 的時(shí)候,都是使用 XML 配置 SQL 語句,而我們課程中的例子,都是使用注解的方式,那么這兩者有什么區(qū)別呢?我們?cè)趯?shí)際開發(fā)中,要如何選擇呢?
首先官方是比較推薦使用 XML 的,因?yàn)槭褂米⒔獾姆绞剑唇觿?dòng)態(tài) SQL 比較費(fèi)勁兒,如果你們的 SQL 比較復(fù)雜,需要多表關(guān)聯(lián),還是使用 XML 比較好;而且現(xiàn)在也有很多插件,可以自動(dòng)生成 MyBatis XML。
但是事物總是有兩方面的,復(fù)雜的 SQL 并不是值得驕傲的事情,如果你們的項(xiàng)目能做到?jīng)]有復(fù)雜 SQL 的話,使用注解會(huì)是更好的選擇(我們現(xiàn)在的項(xiàng)目 95% 以上的 SQL 都是單表查詢)。
03. #{} 和 ${} 的區(qū)別是什么?
${} 是字符串替換,#{} 是預(yù)編譯處理;使用 #{} 可以防止 SQL 注入,提高系統(tǒng)安全性。
04. 如何做批量插入?
注解的方式同樣可以使用動(dòng)態(tài) SQL :
- @Insert({
- "<script>"
- + "INSERT INTO USER(userId, userName, gender, age) values"
- + "<foreach collection='userList' item='item' index='index' separator=','>"
- + " (#{item.userId}, #{item.userName}, #{item.gender}, #{item.age})"
- + "</foreach>"
- + "</script>"
- })
- public void insertUserList(@Param(value="userList") List<User> userList);
Spring Boot 集成 MyBatis 做數(shù)據(jù)庫增刪查改的操作,是比較基礎(chǔ)的知識(shí),希望對(duì)初學(xué) Java 的人有所幫助。