Java Web基礎入門
前言
語言都是相通的,只要搞清楚概念後就可以編寫程式碼了。而概念是需要學習成本的。
Java基礎
不用看《程式設計思想》,基礎語法看 http://www.runoob.com/java/java-basic-syntax.html 就可以了,入門後想幹啥幹啥,如果感興趣,如果有時間。
Web
這裡講的web是指提供API(Application Programming Interface)的能力。那麼什麼是API?
API是指server端和client端進行資源互動的通道。Client可以通過API來獲取和修改server端的資源(Resource). 實際上,API差不多就是URL的代稱,現階段,推薦採用RESTfull API.
RESTfull API
API表現方式就是URL(Uniform Resoure Locator)。RESTfull API是一個概念,規定了應該以什麼樣的結構去構建API,即應該如何拼接URL。先來看看URL是什麼樣子的。
資源(Resources)
path中的groups
和users
都是資源的名稱,通過引數來確定資源的位置。
行為/操作(Method)
我們通過約定的Http Method
來表示對Resource的操作。
常用的HTTP動詞有下面五個(括號裡是對應的SQL命令)。
GET(SELECT):從伺服器取出資源(一項或多項)。 POST(CREATE):在伺服器新建一個資源。 PUT(UPDATE):在伺服器更新資源(客戶端提供改變後的完整資源)。 PATCH(UPDATE):在伺服器更新資源(客戶端提供改變的屬性)。 DELETE(DELETE):從伺服器刪除資源。
還有兩個不常用的HTTP動詞。
HEAD:獲取資源的元資料。
OPTIONS:獲取資訊,關於資源的哪些屬性是客戶端可以改變的。
示例:
GET /zoos:列出所有動物園 POST /zoos:新建一個動物園 GET /zoos/ID:獲取某個指定動物園的資訊 PUT /zoos/ID:更新某個指定動物園的資訊(提供該動物園的全部資訊) PATCH /zoos/ID:更新某個指定動物園的資訊(提供該動物園的部分資訊) DELETE /zoos/ID:刪除某個動物園 GET /zoos/ID/animals:列出某個指定動物園的所有動物 DELETE /zoos/ID/animals/ID:刪除某個指定動物園的指定動物
當path的組成仍舊無法準確定位資源的時候,可以通過queryParam來進一步縮小範圍。
?limit=10:指定返回記錄的數量
?offset=10:指定返回記錄的開始位置。
?page=2&per_page=100:指定第幾頁,以及每頁的記錄數。
?sortby=name&order=asc:指定返回結果按照哪個屬性排序,以及排序順序。
?animal_type_id=1:指定篩選條件
更多關於構建RESTfull API的資訊,參閱https://codeplanet.io/principles-good-restful-api-design/
ContentType
現在的介面都是基於JSON傳輸的,什麼是JSON(JavaScript Object Notation)?
一個基於JSON的API的response應該包含以下header
Content-Type:application/json; charset=utf-8
NodeJS Web
安裝NodeJS
然後,建立app.js, npm install express --save
, node app.js
, 訪問localhost:3000/
,localhost:3000/json
// 這句的意思就是引入 `express` 模組,並將它賦予 `express` 這個變數等待使用。
var express = require('express');
// 呼叫 express 例項,它是一個函式,不帶引數呼叫時,會返回一個 express 例項,將這個變數賦予 app 變數。
var app = express();
// app 本身有很多方法,其中包括最常用的 get、post、put/patch、delete,在這裡我們呼叫其中的 get 方法,為我們的 `/` 路徑指定一個 handler 函式。
// 這個 handler 函式會接收 req 和 res 兩個物件,他們分別是請求的 request 和 response。
// request 中包含了瀏覽器傳來的各種資訊,比如 query 啊,body 啊,headers 啊之類的,都可以通過 req 物件訪問到。
// res 物件,我們一般不從裡面取資訊,而是通過它來定製我們向瀏覽器輸出的資訊,比如 header 資訊,比如想要向瀏覽器輸出的內容。這裡我們呼叫了它的 #send 方法,向瀏覽器輸出一個字串。
app.get('/', function (req, res) {
res.send('Hello World');
});
app.get('/json', function (req, res) {
var rs = {};
rs.id=1;
rs.name = "Ryan";
res.send(rs);
});
// 定義好我們 app 的行為之後,讓它監聽本地的 3000 埠。這裡的第二個函式是個回撥函式,會在 listen 動作成功後執行,我們這裡執行了一個命令列輸出操作,告訴我們監聽動作已完成。
app.listen(3000, function () {
console.log('app is listening at port 3000');
});
Java Web
Java Web的開源框架中,目前最常用的是SpringBoot. SpringBoot可以提供API,可以渲染頁面,是作為API Server的最佳選擇。
寫了無數遍hello world, 這次還是要從hello world開始。
demo source
https://github.com/Ryan-Miao/springboot-demo-gradle
Java Web的包管理工具有maven,gradle。這裡將使用gradle作為依賴管理工具。
Gradle是什麼
gradle是繼maven之後,Java專案構建工具的集大成者。它管理依賴,為什麼要管理依賴?我們的專案中將會使用很多其他的lib,這些lib有我們自己的,也有開源的,甚至大部分都是開源的。當引入這些lib的時候,引入哪個版本?去哪裡下載?多個版本產生了衝突怎麼辦?以及最後我們專案開發完成後,怎麼打包?甚至,想使用CI/CD自動化構建工具,如何整合?這就是gradle可以做的事情。
gradle要怎麼學?
一般來說不用學,不用理會內建的邏輯,只需要用就好。就好比IDE,你不會深究IDE是c編寫的還是Java編寫的,但會使用IDE來編寫程式碼。同樣,gradle的用法很簡單,可以滿足我們開發中覺得部分需求。當然,當需要自定義功能的時候,可以使用groovy
來編寫gradle指令碼。
IntelIj IDEA
IDEA是目前構建Java Web專案最火IDE。用法和Eclipse還是有不少的區別,剛轉過來的時候可能有點不習慣。但根據2-8原則,我們只需要掌握其中一部分用法就可以開發了,剩下的高階用法可以在開發中慢慢摸索。即,其實用法也很簡單。
新建一個gradle專案
點選File
->New
->project
->gradle
->勾選Java
如果發現沒有JDK,那麼new一個就好。
下一步,設定專案標籤,group通常是公司名稱倒寫,比如com.google
,com.alibaba
等. ArtifactId就是我們的專案名稱,比如這次demo為springboot-demo
然後一路next,完成後確定。IDEA會下載gradle,下載簡單的依賴,完畢後,專案根目錄下多出幾個檔案,目前不用care。
.
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
├── main
│ ├── java
│ └── resources
└── test
├── java
└── resources
接下來修改build.gradle
,這個檔案是依賴管理的核心檔案
buildscript {
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/groups/public/"
}
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.8.RELEASE")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
jar {
baseName = 'springboot-demo'
version = '0.1.0'
}
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/groups/public/"
}
mavenCentral()
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
testCompile('org.springframework.boot:spring-boot-starter-test')
}
- maven是一個倉庫,一些開源的第三方庫lib都從這裡下載,這裡引用了aliyun映象,因為maven在國內訪問比較慢,如果在國外可以移除這個節點
-
buildscript
裡就這麼寫,不用關心為什麼,只需要知道這裡這樣寫就可以引入springboot的版本 -
dependencies
是唯一會改變和增加內容的地方,當需要第三方庫的時候新增,新增規則就是groupId:artifactId:version
, 正好和我們建立專案的時候宣告的標籤一樣
修改build.gradle
之後就要重新build,在IDEA中,點選右側的工具欄,gradle,點選重新整理按鈕。就會自動下依賴,如果沒有下載,點選gradle下Task裡的build
按鈕。
另一個方式就是命令列:
細心可以發現專案根目錄下有gradlew
和gradlew.bat
這個檔案,這是分別為linux和windows準備的啟動工具,在Linux系統中
./gradlew build
or
sh gradlew build
在windows中
gradlew build
編譯完成後,在左側的專案目錄下的External Libraties
下可以看到我們引入的第三方庫。為什麼這麼多?因為依賴是樹狀的,或者說網狀的。lib也有他自己的依賴,gradle會負責把我們引入的lib的依賴也給下載下來。在沒有maven和gradle這種構建工具之前,專案開發都是自己下載jar,自己丟進去classpath裡,很容遺漏,也很容易造成衝突。gralde會負責下載依賴,還會解決衝突,比如不同版本等問題。
開始編寫服務端配置
Springboot的一個優點是約定大於配置,意思是我們都約定好怎麼配置,我幫你配置好了,你直接用就好。因此,springmvc時代的大部分配置都可以自動化完成。我們的啟動類也只有一行.
可以看到,src/main/java
這個目錄變成藍色,在IDEA裡是指sourceSet,也就是原始檔,我們的Java程式碼就是放在這檔案下的,這也是約定好的。
在該目錄下新建com.test.demo.Application.java
package com.test.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Created by Ryan on 2017/11/13/0013.
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
到這裡,我們的服務端就配置完畢了。執行main方法即可啟動。
編寫第一個API
雖然服務端配置好了,但並沒有API. 新建com.test.demo.controller.HelloController.java
package com.test.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* Created by Ryan on 2017/11/14/0014.
*/
@Controller
public class HelloController {
@ResponseBody
@GetMapping("/hello")
public String hello(){
return "{"hello":"world"}";
}
}
然後,再次執行main方法,啟動完畢後,訪問 http://localhost:8080/hello, 第一個API開發完畢。
-
@Controller
這個註解標註這個類是一個controller,用來接收請求和響應response -
@GetMapping("/hello")
標註這個方法是一個路由請求實現,括號裡就是我們的路由 -
@ResponseBody
這個註解標註這個API的返回值是json,其實就是再response的header裡塞入了contentType, 當然,在這裡還涉及到class轉json的問題。那麼,回到開始的問題,json是什麼東西?
JSON在Java裡沒有這個資料結構,其實就是一個String,遵從JSON規則的String,我們的方法在返回這段String的時候,加上header裡的contentType,瀏覽器就會當做JSON讀取。在Javascript去讀Ajax的結果就變成了一個JSON物件了。其他的,比如Android,讀取出來的還是一個字串,需要手動反序列化成我們想要的類。
說到序列化,我們不可能每個返回結構都這樣拼接字串吧。所以,ResponseBody
標註的請求還會使用一個jackson的介面卡,這些都是springboot內建的。暫時也不需要研究實現原理。jackson是什麼鬼?
jackson是Java中使用最廣泛的一個json解析lib,他可以將一個Java 類轉變成一個json字串,也同樣可以把一個json字串反序列化成一個java物件。Springboot是如何做到的?這就需要去研究原始碼了。
啟動和除錯
最簡單的是啟動就是執行main方法,還可以命令列啟動
gradlew bootRun
debug,最簡單的就是以debug啟動main方法。當然也可以遠端。
gradlew bootRun --debug-jvm
然後,在IDEA中,點選Edit configurations
選擇remote
然後,點選debug
如果想支援熱載入,則需要新增
compile("org.springframework.boot:spring-boot-devtools")
在IDEA裡修改Java class後需要,重新build當前class才能生效。快捷鍵 ctrl+shif+F9
配置檔案
spring boot預設配置了很多東西,但有時候我們想要修改預設值,比如不想用8080作為埠,因為埠被佔用了。
在resources
下,新建application.properties
, 然後在裡面輸入
server.port=8081
然後,重啟專案,發現埠已經生效。
再配置一些common的自定義,比如日誌。專案肯定要記錄日誌的,System.out.println
遠遠達不到日誌的要求。springboot預設採用Logback
作為日誌處理工具。
spring.output.ansi.enabled=ALWAYS
logging.file=logs/demo.log
logging.level.root=INFO
接著,開發和生產環境的配置必然不同的,比如資料庫的地址不同,那麼可以分配置檔案來區分環境。
在resources下新建application-dev.properties
, application-prod.properties
. spring預設通過後綴不同來識別不同的環境,不加字尾的是base配置。那麼如何生效呢?
只要在base的配置檔案中
spring.profiles.active=dev
比如,我們在dev環境中設定loglevel為debug
logging.level.root=debug
這樣,springboot會優先讀取base檔案,然後讀取dev,當dev有相同的配置項時,dev會覆蓋base。
這樣,本地開發和生產環境隔離,部署也方便。事實上,springboot接收引數的優先順序為resources下的配置檔案
<命令列引數
. 通常,我們部署專案的指令碼會使用命令列引數來覆蓋配置檔案,這樣就可以動態指定配置檔案了。
接收引數,響應JSON
新建一個controller, com.test.demo.controller.ParamController
package com.test.demo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Created by Ryan on 2017/11/16/0016.
*/
@RestController
@RequestMapping("/param")
public class ParamController {
private static final Logger LOGGER = LoggerFactory.getLogger(ParamController.class);
@GetMapping("/hotels/{htid}/rooms")
public List<Long> getRooms(
@PathVariable String htid,
@RequestParam String langId,
@RequestParam(value = "limit", required = false, defaultValue = "10") int limit,
@RequestParam(value = "offset", required = false, defaultValue = "1") int offset
){
final Map<String, Object> params = new HashMap<>();
params.put("hotelId", htid);
params.put("langId", langId);
params.put("limit", limit);
params.put("offset", offset);
LOGGER.info("The params is {}", params);
List<Long> roomIds = new ArrayList<>();
roomIds.add(1L);
roomIds.add(2L);
roomIds.add(3L);
return roomIds;
}
}
- LOG: 採用Sl4J介面
- 引數:
@PathVariable
可以接收url路徑中的引數 - 引數:
@RequestParam
可以接收?
後的query引數 - 響應:
@RestController
==@Controller+@ResponseBody
, 其實,@ResponseBody
註解表明這個方法會返回json,會將Java類轉換成JSON字串,預設轉換器為Jackason
引數為JSON
新建class com.test.demo.entity.Room
public class Room {
private Integer roomId;
private String roomName;
private String comment;
public Integer getRoomId() {
return roomId;
}
public void setRoomId(Integer roomId) {
this.roomId = roomId;
}
public String getRoomName() {
return roomName;
}
public void setRoomName(String roomName) {
this.roomName = roomName;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
}
假設,我們需要儲存一個Room資訊,先來get一個
@GetMapping("/hotels/{htid}/rooms/{roomId}")
public Room getRoomById(
@PathVariable String htid,
@PathVariable Integer roomId
){
if (htid.equals("6606")){
final Room room = new Room();
room.setComment("None");
room.setRoomId(roomId);
room.setRoomName("豪華雙人間");
return room;
}
return null;
}
然後儲存一個
@PostMapping("/hotels/{htid}/rooms")
public Integer addRoom(@RequestBody Room room){
final Random random = new Random();
final int id = random.nextInt(10);
room.setRoomId(id);
LOGGER.info("Add a room: {}", room);
return id;
}
接收陣列引數
@GetMapping("/hotels/{htid}/rooms/ids")
public String getRoomsWithIds(@RequestParam List<Integer> ids){
String s = ids.toString();
LOGGER.info(s);
return s;
}
瀏覽器訪問 http://localhost:8081/param//hotels/6606/rooms/ids?ids=1,2,3
引數校驗
我們除了一個個的if去判斷引數,還可以使用註解
public class Room {
private Integer roomId;
@NotEmpty
@Size(min = 3, max = 20, message = "The size of room name should between 3 and 20")
private String roomName;
只要在引數前新增javax.validation.Valid
@PostMapping("/hotels/{htid}/rooms")
public Integer addRoom(
@Valid @RequestBody Room room,
@RequestHeader(name = "transactionId") String transactionId
){
靜態檔案
在springboot中,static content預設尋找規則是
By default Spring Boot will serve static content from a directory called
/static
(or/public
or/resources
or/META-INF/resources
) in the classpath or from the root of the ServletContext.
在resources
下新建資料夾 static
,
srcmainresourcesstaticcontent.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello static content</title>
<script src="/js/test.js"></script>
</head>
<body>
<h1>Static Content</h1>
<p>Static content is the files that render directly, the file is the whole content. The different between template is that
the template page will be resolved by server and then render out.
</p>
</body>
</html>
瀏覽器訪問: http://localhost:8081/content.html
同理,放在static下的檔案都可以通過如此對映訪問。
模板檔案
模板檔案是指通過服務端生成的檔案。比如Jsp,會經過servlet編譯後,最終生成一個html頁面。Springboot預設支援以下幾種模板:
FreeMarker Groovy Thymeleaf Mustache
JSP在jar檔案中的表現有問題,除非部署為war。
官方推薦的模板為Thymeleaf
, 在depenency中新增依賴:
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
rebuild.
SpringBoot預設模板檔案讀取位置為:srcmainresourcestemplates
. 新建 srcmainresourcestemplateshome.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
<meta charset="UTF-8"/>
<title>Home</title>
</head>
<body>
<h1>Template content</h1>
<p th:text="${msg} + ' The current user is:' + ${user.name}">Welcome!</p>
</body>
</html>
模板檔案只能通過服務端路由渲染,也就是說不能像剛開始靜態檔案那樣直接路由過去。
建立一個controller, com.test.demo.controller.HomeController
package com.test.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.HashMap;
import java.util.Map;
/**
* Created by Ryan on 2017/11/18/0018.
*/
@Controller
public class HomeController {
@RequestMapping("/home")
public String index(Model model, String name){
final Map<String, Object> user = new HashMap<>();
user.put("name", name);
model.addAttribute("user", user);
model.addAttribute("msg", "Hello World!");
return "home";
}
}
這個和之前的API的介面有一點不同,首先是沒有@ResponseBody
註解,然後是方法的返回值是一個String,這個String不是value,而是指模板檔案的位置,相對於templates
的位置。
瀏覽器訪問:http://localhost:8081/home?name=Ryan123
方法引數的Model
是模板檔案的變數來源,模板檔案從這個物件裡讀取變數,將這個類放到引數裡,Spring會自動注入這個類,繫結到模板檔案。這裡,放入兩個變數。
在模板端,就可以讀取這個變量了。
為什麼要這麼做?既然有了靜態檔案,為什麼還要模板檔案?
首先,這是早期web開發的做法,之前是沒有web 前端這個兵種的,頁面從靜態頁面變成動態頁面,代表就是jsp,php等。模板檔案的有個好處是,服務端可以控制頁面,比如從session中拿到使用者資訊,放入頁面。這個在靜態頁面是做不到的。
然而,現在前後端的分離實踐,使得模板檔案的作用越來越小。目前主要用於基礎資料傳遞,其他資料則通過客戶端的非同步請求獲得。
當然,隨著頁面構建複雜,非同步請求太多,首屏渲染時間越來越長,嚴重影響了使用者體驗,比如淘寶雙11的宣傳頁。這時候,服務端渲染的優勢又體現出來了,靜態頁面直接出資料,不需要多次的ajax請求。
跨域
Cross-origin resource sharing (CORS) is a W3C specification implemented by most browsers that allows you to specify in a flexible way what kind of cross domain requests are authorized, instead of using some less secure and less powerful approaches like IFRAME or JSONP.
CORS是瀏覽器的一種安全保護,隔離不同域名之間的可見度。比如,不允許把本域名下cookie傳送給另一個域名,否則cookie被釣魚後,黑客就可以模擬本人登陸了。更多細節參考MDN
為什麼瀏覽器要拒絕cors? 摘自部落格園
cors執行過程摘自自由的維基百科
首先,本地模擬跨域請求。
我們當前demo的域名為localhost:8081
,現在新增一個本地域名, 在HOSTS檔案中新增:
127.0.0.1 corshost
然後,訪問http://corshost:8081,即本demo。
新增srcmainresourcesstaticcors.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test Cors</title>
</head>
<body>
<script src="http://cdn.staticfile.org/jquery/3.2.1/jquery.min.js"></script>
<script>
$.ajax({ url: "http://localhost:8081/hello", success: function(data){
console.log(data);
}});
</script>
</body>
</html>
訪問之前建立的hello介面,可以看到訪問失敗,
Failed to load http://localhost:8081/hello: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://corshost:8081' is therefore not allowed access.
這是瀏覽器正常的行為。
但,由於前後端分離,甚至分開部署,域名肯定不會是同一個了,那麼就需要支援跨域。Springboot支援跨域,解決方案如下:
在需要跨域的method上,新增一個@CrossOrigin
註解即可。
@CrossOrigin(origins = {"http://corshost:8081"})
@ResponseBody
@GetMapping("/hello")
public String hello(){
return "{"hello":"world"}";
}
如果是全域性配置允許跨域,新建com.test.demo.config.CorsConfiguration
package com.test.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* Created by Ryan on 2017/11/18/0018.
*/
@Configuration
public class CorsConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(false).maxAge(3600);
}
};
}
}
部署
剛開始看Springboot的時候看到推薦使用fat jar
部署,於是記錄下來。後面看到公司的生產環境中既有使用war也有使用jar的,為了方便,非不得已,還是使用jar來部署。
首先,打包:
gradlew clean build
然後,可以看到,在build/libs下有兩個jar,springboot-demo-0.1.0.jar.original
和springboot-demo-0.1.0.jar
。後面這個就是springboot外掛打包好的fat jar
,前一個是gradle打包的源jar。接著就可以直接執行這個jar,prod也是如此。
java -jar build/libs/springboot-demo-0.1.0.jar --spring.profiles.active=prod
後面通過引數來指定配置檔案的環境,這種命令列引數的優先順序要高於配置在base裡的,所以會覆蓋變數,因此,最終採用的就是prod這個環境配置。
引入MySQL/MariaDB
MySQL被Oracle收走之後,他的father另外建立了新的社群分支MariaDB, 據說用法和MySQL一致。然後,各大Linux開源系統都預置了MariaDB。 當然,由於新出沒多久,市場還不夠開闊。根據[DB-Engines Ranking]釋出的2017年11月份排行, MySQL幾乎完全接近Oracle,排名第二。而MariaDB的上升之路還比較遙遠。So,還是入手MySQL靠譜。因為開源技術的掌握能力和跳槽能力成正相關。
安裝MySQL
MAC安裝參考Mac install MySQL。
Windows安裝
去官網下載安裝包(mysql-5.7.20-winx64.zip). 當然,需要先註冊oracle賬號。
解壓當目錄,然後將bin目錄加入環境變數,同Java設定環境變數。這裡再次演示下。複製bin目錄地址,我的為D:datamysqlmysql-5.7.20-winx64bin
, 在此電腦
右鍵,--> 屬性 --> 高階系統設定 --> 高階 --> 環境變數 --> 在系統環境變數中找到path --> 新建 --> 填入 --> 確認。
然後,重新開啟cmd。輸入mysqld --initialize --console
C:UsersRyan
λ mysqld --initialize --console
mysqld: Could not create or access the registry key needed for the MySQL application
to log to the Windows EventLog. Run the application with sufficient
privileges once to create the key, add the key manually, or turn off
logging for that application.
2017-11-26T05:22:48.434089Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
2017-11-26T05:22:48.437096Z 0 [ERROR] Cannot open Windows EventLog; check privileges, or start server with --log_syslog=0
2017-11-26T05:22:49.148986Z 0 [Warning] InnoDB: New log files created, LSN=45790
2017-11-26T05:22:49.276866Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.
2017-11-26T05:22:49.370828Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: d7e6ac05-d269-11e7-a91e-9883891ed8e3.
2017-11-26T05:22:49.383970Z 0 [Warning] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened.
2017-11-26T05:22:49.398975Z 1 [Note] A temporary password is generated for root@localhost: /r.Vtktfl9FN
複製我們的臨時密碼/r.Vtktfl9FN
.
命令列啟動MySQL:
mysqld --console
新開一個cmd,命令列輸入賬號密碼mysql -u root -p
C:UsersRyan
λ mysql -u root -p
Enter password: ************
Welcome to the MySQL monitor. Commands end with ; or g.
Your MySQL connection id is 3
Server version: 5.7.20
Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.
mysql>
然後就連線到MySQL了。第一個命令列就是啟動mysql,第二個命令列就是client,連線MySQL。現在修改我們的root
密碼
mysql> set password=password('123456');
Query OK, 0 rows affected, 1 warning (0.00 sec)
然後,關閉client,輸入exit
退出。 重新以新密碼123456登陸(不要自己難為自己,設定密碼為123456是最佳選擇).
確認成功就安裝完畢。賬號為root
, 密碼為123456
。
基本操作
關於MySQL的基本語法,學習http://www.runoob.com/mysql/mysql-tutorial.html 即可。
這裡簡單記錄幾個簡單的概念。
database
MySQL以不同的database為單位儲存資料。所以,開發資料庫的時候,先要建立一個database。
檢視已有的database
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.00 sec)
建立我們的database
mysql> create database if not exists springboot_demo charset utf8 collate utf8_general_ci;
Query OK, 1 row affected (0.01 sec)
進入database:
mysql> use springboot_demo
Database changed
建立表
檢視當前database的所有表
mysql> use springboot_demo
Database changed
mysql> show tables;
Empty set (0.00 sec)
建立一個表room
mysql> create table if not exists room (
-> id INT(11) NOT NULL AUTO_INCREMENT,
-> `name` VARCHAR(80) NOT NULL,
-> `comment` VARCHAR(200),
-> create_date DATETIME,
-> update_date DATETIME,
-> PRIMARY KEY(id)
-> )ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.08 sec)
-
create table
建立表 -
if not exists
如果不存在則建立 -
room
表名 -
id
表字段,欄位名為id
,NOT NULL
表示會給這個欄位建立非空索引,當存入空時會報錯。如果不寫明NOT NULL
,則預設該欄位可以為空。 -
AUTO_INCREMENT
表示這個欄位會自動增加,即當儲存一條記錄的時候,如果不傳入id
這個欄位,則該欄位會從系統序列中取出一個。該序列是一個遞增序列。即實現了每次id都增加1 -
反引號
包裹欄位名是為了防止與關鍵字衝突 -
INT
是指數字型別,括號裡的11
是指MySQL裡的顯示寬度,和最大值取值範圍無關,是指需要多少位來表示這個數字,不夠長度的補齊。int最大值為2147483647
-
VARCHAR
是變長字串,即當儲存1個字元,則佔用空間就是1個位元組,當儲存2個字元,則佔用空間為2個字元。與之對應的是char
定長。括號裡的是指字元的個數,即最大允許200個字元。 -
DATA
是日期型別,通常每條記錄都需要記錄建立時間和更新時間 -
PRIMARY KEY
表示這個欄位是主鍵
,即該記錄的唯一識別符號。
插入一條記錄
mysql> insert into room(`name`, `comment`, `create_date`, `update_date`) values ("大床房", "", "2017-11-26","2017-11-26
11:00:00");
Query OK, 1 row affected, 1 warning (0.01 sec)
mysql>insert into room(`name`, `comment`, `create_date`, `update_date`) values ("雙人床房", "有窗戶", "2017-11-26","201
7-11-26 11:00:00");
Query OK, 1 row affected, 1 warning (0.01 sec)
檢視所有記錄
mysql> select * from room;
+----+----------+---------+-------------+-------------+
| id | name | comment | create_date | update_date |
+----+----------+---------+-------------+-------------+
| 1 | 大床房 | | 2017-11-26 | 2017-11-26 |
| 2 | 雙人床房 | 有窗戶 | 2017-11-26 | 2017-11-26 |
+----+----------+---------+-------------+-------------+
2 rows in set (0.00 sec)
更新一條記錄
mysql> update room set comment="無窗" where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from room;
+----+----------+---------+-------------+-------------+
| id | name | comment | create_date | update_date |
+----+----------+---------+-------------+-------------+
| 1 | 大床房 | 無窗 | 2017-11-26 | 2017-11-26 |
| 2 | 雙人床房 | 有窗戶 | 2017-11-26 | 2017-11-26 |
+----+----------+---------+-------------+-------------+
2 rows in set (0.00 sec)
刪除一條記錄
mysql> delete from room where id = 2;
Query OK, 1 row affected (0.01 sec)
mysql> select * from room;
+----+--------+---------+-------------+-------------+
| id | name | comment | create_date | update_date |
+----+--------+---------+-------------+-------------+
| 1 | 大床房 | 無窗 | 2017-11-26 | 2017-11-26 |
+----+--------+---------+-------------+-------------+
1 row in set (0.00 sec)
什麼是資料操縱語句
以下來自部落格園。
SQL語言共分為四大類:資料查詢語言DQL,資料操縱語言DML,資料定義語言DDL,資料控制語言DCL。
1. 資料查詢語言DQL
資料查詢語言DQL基本結構是由SELECT子句,FROM子句,WHERE 子句組成的查詢塊:
SELECT <欄位名錶>
FROM <表或檢視名>
WHERE <查詢條件>
2 .資料操縱語言DML
資料操縱語言DML主要有三種形式:
1) 插入:INSERT
2) 更新:UPDATE
3) 刪除:DELETE
3. 資料定義語言DDL
資料定義語言DDL用來建立資料庫中的各種物件-----表、檢視、 索引、同義詞、聚簇等如:
CREATE TABLE/VIEW/INDEX/SYN/CLUSTER
表 /檢視/ 索引/ 同義詞/ 簇
DDL操作是隱性提交的!不能rollback.
4. 資料控制語言DCL
資料控制語言DCL用來授予或回收訪問資料庫的某種特權,並控制 資料庫操縱事務發生的時間及效果,對資料庫實行監視等。如:
1) GRANT:授權。
2) ROLLBACK [WORK] TO [SAVEPOINT]:回退到某一點。 回滾---ROLLBACK 回滾命令使資料庫狀態回到上次最後提交的狀態。其格式為: SQL>ROLLBACK;
3) COMMIT [WORK]:提交。
在資料庫的插入、刪除和修改操作時,只有當事務在提交到資料 庫時才算完成。在事務提交前,只有操作資料庫的這個人才能有權看 到所做的事情,別人只有在最後提交完成後才可以看到。 提交資料有三種類型:顯式提交、隱式提交及自動提交。下面分 別說明這三種類型。
(1) 顯式提交
用COMMIT命令直接完成的提交為顯式提交。其格式為:
SQL>COMMIT
;
(2) 隱式提交
用SQL命令間接完成的提交為隱式提交。這些命令是:
ALTER
,AUDIT
,COMMENT
,CONNECT
,CREATE
,DISCONNECT
,DROP
,
EXIT
,GRANT
,NOAUDIT
,QUIT
,REVOKE
,RENAME
。
(3) 自動提交
若把AUTOCOMMIT設定為ON,則在插入、修改、刪除語句執行後,
系統將自動進行提交,這就是自動提交。其格式為:
SQL>SET AUTOCOMMIT ON
;
到此,增刪改查語句複習完畢。開始引入專案。
專案連線MySQL
保持MySQL開啟狀態。
引入mysql驅動和spring-jdbc
compile("org.springframework.boot:spring-boot-starter-jdbc")
compile group: 'mysql', name: 'mysql-connector-java', version: '6.0.6'
修改配置檔案,新增:
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_demo?serverTimezone=UTC&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
新建com.test.demo.config.DBConfiguration
package com.test.demo.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
@Configuration
public class DBConfiguration {
@Bean
public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
-
@Configuration
標註這個類是一個配置類,spring會自動掃描這個註解,將裡面的配置執行。 -
@Bean
標註宣告一個Bean,由spring管理,在需要的地方注入。 -
@Qualifier("dataSource")
@Bean的引數列表中物件會從spring容器中查詢bean,找到後注入引數。而Qualifier
則宣告要注入的bean的name或者id是什麼,這在spring容器包含2個以上同類型的bean的時候有用。 -
DataSource
這個物件是springboot自動建立的,通過掃描配置類裡的配置,當檢測到有配置datasource的時候會建立這個bean。於是,在這裡就可以注入了,即我們配置的那幾個屬性。 -
JdbcTemplate
一個封裝了對DB操作的library, 通過它來對資料庫操作。
下面寫一個測試來測試是否聯通了。在src/test/java下,新建com.test.demo.config.DBConfigurationTest
package com.test.demo.config;
import com.test.demo.Application;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import java.util.Map;
@RunWith(SpringRunner.class)
@SpringBootTest
@Import({Application.class, DBConfiguration.class})
public class DBConfigurationTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void testSelect() {
List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from room");
System.out.println(maps);
}
}
控制檯打印出剛才的資料庫中的資料:
[{id=1, name=大床房, comment=無窗, create_date=2017-11-26, update_date=2017-11-26}]
-
@RunWith(SpringRunner.class)
執行spring容器的測試 -
@SpringBootTest
springboot測試 -
@Import({Application.class, DBConfiguration.class})
匯入我們需要的配置 -
@Autowired
自動注入屬性,剛才在Configuration中聲明瞭一個Bean,在這裡通過這個註解獲取那個bean -
@Test
這是一個JUnit測試
JDBCTemplate
Spring-JDBC
提供了簡化版的資料庫連線操作。對於簡單的連線資料庫來說,spring-jdbc已經足夠提供orm能力。當然,現在國內流行的orm還是Mybatis。不過,隨著微服務拆分的盛行,jpa的優勢更加明顯。不管用什麼框架,原理都是差不多的,就是封裝複雜的對映邏輯,簡化操作。
什麼是JDBC? JDBC即Java DataBase Connectivity,Java資料庫連線,JDK自帶了JDBC。
什麼是Mybatis? 以下來自百度百科
MyBatis 是一款優秀的持久層框架,它支援定製化 SQL、儲存過程以及高階對映。MyBatis 避免了幾乎所有的 JDBC 程式碼和手動設定引數以及獲取結果集。MyBatis 可以使用簡單的 XML 或註解來配置和對映原生資訊,將介面和 Java 的 POJOs(Plain Old Java Objects,普通的 Java物件)對映成資料庫中的記錄。
什麼是JPA? JPA是Java Persistence API的簡稱,中文名Java持久層API.
什麼是ORM?
物件關係對映(英語:(Object Relational Mapping,簡稱ORM,或O/RM,或O/R mapping),是一種程式技術,用於實現面向物件程式語言裡不同型別系統的資料之間的轉換[1] 。從效果上說,它其實是建立了一個可在程式語言裡使用的--“虛擬物件資料庫”。
面向物件是從軟體工程基本原則(如耦合、聚合、封裝)的基礎上發展起來的,而關係資料庫則是從數學理論發展而來的,兩套理論存在顯著的區別。為了解決這個不匹配的現象,物件關係對映技術應運而生。
物件關係對映(Object-Relational Mapping)提供了概念性的、易於理解的模型化資料的方法。ORM方法論基於三個核心原則:
- 簡單:以最基本的形式建模資料。
- 傳達性:資料庫結構被任何人都能理解的語言文件化。
- 精確性:基於資料模型建立正確標準化的結構。 典型地,建模者通過收集來自那些熟悉應用程式但不熟練的資料建模者的人的資訊開發資訊模型。建模者必須能夠用非技術企業專家可以理解的術語在概念層次上與資料結構進行通訊。建模者也必須能以簡單的單元分析資訊,對樣本資料進行處理。ORM專門被設計為改進這種聯絡。 簡單的說:ORM相當於中繼資料, 即通過操作物件來完成sql語句,自動提供了物件和sql的對映。
為什麼明明標題是JDBCTemplate, 卻說了一堆別的?實際生產中,對關係型資料庫的操作多是用Mybatis或Hibernate這樣的ORM框架。而ORM框架的根源還是jdbc,因此,學習jdbc是學習其他ORM框架的第一步。
為什麼不直接講jdk自帶的jdbc?當Java基礎掌握好之後,jdbc也就是多一個library,學習jdbc也就是學習這個lib的用法而已。那麼,既然有簡化的spring-jdbc,自然可以先跳過原生。
下面開始簡單使用spring-jdbc。
插入一條資料
在上一步的新建的com.test.demo.config.DBConfigurationTest
中繼續開發。新增一個新的測試:
@Transactional
@Test
public void testInsert() {
final RoomTable room = new RoomTable("Doule Bed", "no", new Date(), new Date());
final String sql = "INSERT INTO room(`name`, `comment`, `create_date`, `update_date`) VALUES (?,?,?,?)";
final int rs = jdbcTemplate.update(sql,
room.getName(), room.getComment(), room.getCreateDate(), room.getUpdateDate());
System.out.println(rs);
}
-
@Transactional
是spring提供的事物註解,標註這個在測試類中的含義是:每次執行完該測試類後,回滾(rollback). -
jdbcTemplate.update(sql, 引數)
提供了佔位符的資料操縱語句的執行。為什麼要使用佔位符(PreparedStatement)而不是直接拼接字串?防止sql注入。 -
RoomTable
是一個新建Entity,關於什麼是Entity後面分層架構中將講到。 -
rs
是執行sql結束後,資料返回的一個數字,含義成功了多少行。
新建com.test.demo.domain.entity.RoomTable
package com.test.demo.domain.entity;
import java.util.Date;
/**
* Created by Ryan Miao on 12/2/17.
*/
public class RoomTable {
private Integer id;
private String name;
private String comment;
private Date createDate;
private Date updateDate;
public RoomTable() {
}
public RoomTable(String name, String comment, Date createDate, Date updateDate) {
this.name = name;
this.comment = comment;
this.createDate = createDate;
this.updateDate = updateDate;
}
public RoomTable(Integer id, String name, String comment, Date createDate, Date updateDate) {
this.id = id;
this.name = name;
this.comment = comment;
this.createDate = createDate;
this.updateDate = updateDate;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public Date getCreateDate() {
return createDate;
}
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
public Date getUpdateDate() {
return updateDate;
}
public void setUpdateDate(Date updateDate) {
this.updateDate = updateDate;
}
@Override
public String toString() {
return "RoomTable{" +
"id=" + id +
", name='" + name + ''' +
", comment='" + comment + ''' +
", createDate=" + createDate +
", updateDate=" + updateDate +
'}';
}
}
RoomTable是一個Entity類,對應資料庫的表。欄位型別要一致。關於Java型別和SQL的資料庫表對映規則,請查閱官網。
插入一條資料並返回主鍵
我們新建的表RoomTable是有ID的,我們建立了一個Room後要知道生成的id,來返回給前端。不然前端不知道id就無法進行修改之類的操作了。
@Transactional
@Test
public void testInsertAndGetKey() {
final RoomTable room = new RoomTable("Doule Bed", "no", new Date(), new Date());
final KeyHolder keyHolder = new GeneratedKeyHolder();
final int update = jdbcTemplate.update((Connection con) -> {
final String sql = "INSERT INTO room(`name`, `comment`, `create_date`, `update_date`) VALUES (?,?,?,?)";
PreparedStatement preparedStatement = con.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, room.getName());
preparedStatement.setString(2, room.getComment());
preparedStatement.setObject(3, new Timestamp(room.getCreateDate().getTime()));
preparedStatement.setObject(4, new Timestamp(room.getUpdateDate().getTime()));
return preparedStatement;
}, keyHolder);
System.out.println("The number of success:"+update);
System.out.println("The primary key of insert row: "+keyHolder.getKey().intValue());
final List<Map<String, Object>> maps = jdbcTemplate.queryForList("SELECT * FROM room");
System.out.println(maps);
}
-
KeyHolder
用來接收自動生成的主鍵. -
PreparedStatement
用來建立一個佔位符的sql語句. - 需要注意日期型別的對映規則,需要將java.util.Date轉換為java.sql.*
-
queryForList
可以查詢當前資料中的內容
查詢--findById
首先,修改下Date型別為datetime, 因為需要直到修改的具體時間。因此,room的scheme修改如下:
create database if not exists springboot_demo charset utf8 collate utf8_general_ci;
use springboot_demo;
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for room
-- ----------------------------
DROP TABLE IF EXISTS `room`;
CREATE TABLE `room` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(80) NOT NULL,
`comment` varchar(200) DEFAULT NULL,
`create_date` datetime NOT NULL,
`update_date` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of room
-- ----------------------------
INSERT INTO `room` VALUES ('1', '大床房', '無窗', '2017-11-26 00:00:00', '2017-11-26 00:00:00');
INSERT INTO `room` VALUES ('2', 'Double Bed', 'no', '2017-12-06 00:00:00', '2017-12-06 00:00:00');
INSERT INTO `room` VALUES ('3', 'Big Bed', '', '2017-12-06 00:00:00', '2017-12-06 10:00:00');
預設新增3條記錄。
在resources下新建schema.sql,填入上述內容。當springboot啟動時,會自動載入這個sql。那麼就會重新初始化資料庫。
我們的測試類會真實啟動springboot的,因此每個測試都會重新初始化資料庫一遍。下面可以測試根據id查詢內容。
@Test
public void testSelectOne(){
final String sql = "select `id`,`name`,`comment`,`create_date`,`update_date` from room WHERE id=?";
final RoomTable roomTable = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new RoomTable(rs.getInt("id"),
rs.getString("name"),
rs.getString("comment"),
rs.getTimestamp("create_date"),
rs.getTimestamp("update_date")), 3);
System.out.println(roomTable);
Assert.assertTrue(3== roomTable.getId());
Assert.assertNotNull(roomTable.getCreateDate());
}
- 注意要使用select 欄位列表來獲取想要的欄位,不要用
*
-
varchar
的對映為String -
int
的對映為Integer -
datetime
的對映為time - 此處的對映為一個lambda表示式,從結果集中選擇想要的欄位來建立我們的對映關係
- 最後一個引數是佔位符的值,防止sql注入。
然後,可以觀察到控制檯重新啟動springboot,並且運行了schema.sql。接下來需要注意的地方到了:
RoomTable{id=3, name='Big Bed', comment='', createDate=08:00:00, updateDate=18:00:00}
打印出查詢的時間比我們插入的時間多了8h。很容易猜測到時區問題。因為我們是北京時間,UTC+8
。所以,在從資料庫中取出時間的時候,做了下時區轉換。我們的專案把資料的時區當作是UTC
了。事實上,在生產環境中確實應該把資料庫的時區設定為UTC
。因為我們是全球性的專案。當然,設定為UTC+8
也是可以的。但為了防止困擾,設定為UTC
是最佳選擇。
然而,真正的問題還不是這個。我們資料庫當前的timezone是多少?
mysql> show variables like '%time_zone%';
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| system_time_zone | |
| time_zone | SYSTEM |
+------------------+--------+
2 rows in set, 1 warning (0.00 sec)
系統時區,顯然應該是北京時間,即UTC+8
的。那麼,我們為什麼查詢的時候會把資料庫當作0時區呢?
因為Java裡的北京時間對應的時區為Asia/Shanghai
,修改配置檔案:
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_demo?serverTimezone=Asia/Shanghai&characterEncoding=utf-8
然後,重新執行測試。結果正常了。此時,我們的專案時區為系統時區,我們的資料時區為系統時區。我們連線的驅動轉換也標記了資料庫為北京時間。這樣就不會出現時區問題。如果是生產環境,就要把資料庫/伺服器/驅動引數設定為UTC
.
查詢返回list
除了最常用的findbyId, 最常用的查詢是返回一個list。因為我們的搜尋是返回條件匹配的值,而匹配條件的item通常很多個,即list。
@Test
public void testSelectList(){
final String sql = "select `id`,`name`,`comment`,`create_date`,`update_date` from room WHERE id>? LIMIT 0,2";
final List<RoomTable> roomTableList = jdbcTemplate.query(sql, (rs, rowNum) -> new RoomTable(rs.getInt("id"),
rs.getString("name"),
rs.getString("comment"),
rs.getTimestamp("create_date"),
rs.getTimestamp("update_date")), 1);
System.out.println(roomTableList);
assertEquals(2, roomTableList.size());
}
- 同樣要做結果集對映
- 同樣需要傳入佔位符value
- 返回值是一個list
刪除一條資料
刪除一條資料就是把這條記錄給刪除掉。 刪除一條資料這個功能通常都有,但是,現在並不是把資料真正的刪除。因為基於某種想恢復的可能或者某國法律要求,被刪除的資料只是被隱藏,仍舊遺留在資料庫中。在這裡,先實現徹底刪除一條記錄:
@Transactional
@Test
public void testDelete(){
final String sql = "DELETE FROM room WHERE `id`=?";
final int update = jdbcTemplate.update(sql, 1);
Assert.assertEquals(1, update);
List<Map<String, Object>> maps = jdbcTemplate.queryForList("select id from room where `id`=?", 1);
Assert.assertTrue(maps.isEmpty());
final int count = jdbcTemplate.queryForObject("select count(*) from room", Integer.class);
Assert.assertEquals(2, count);
}
- 使用update方法,第二個引數為佔位符value
- 返回一個count表明生效的數量,這裡刪除了一條,應該返回1
- 為了驗證我們是否刪除成功了。首先,我們每次會初始化資料庫,資料庫中只有初始化的3條記錄。現在刪除id為1的記錄。應該剩下2條記錄。還有就是查詢id為1的資料的結果集是null.
另外,由於jdbcTemplate查詢的結果集為nul時,會丟擲異常EmptyResultDataAccessException
, 根據stackoverflow, 推薦捕獲異常來確定結果集為null。於是,也可以這樣判斷資料是否被刪除。
try {
jdbcTemplate.queryForObject("select id from room where `id`=?", Integer.class, 1);
} catch (EmptyResultDataAccessException e) {
System.err.println("Get a null result, the data is not exist in the database."+e.getMessage());
}
更新一條資料
更新一條資料是基於查詢條件唯一確定一條記錄,然後更新該記錄的某個或者多個屬性。
@Transactional
@Test
public void testUpdate(){
final String sql = "update room set `update_date`=?, `comment`=? where id=?";
final int update = jdbcTemplate.update(sql, new Object[]{new Date(), "booked", 1});
assertEquals(1, update);
final String getSql = "select `id`,`name`,`comment`,`create_date`,`update_date` from room WHERE id=?";
final RoomTable roomTable = jdbcTemplate.queryForObject(getSql, (rs, rowNum) -> new RoomTable(rs.getInt("id"),
rs.getString("name"),
rs.getString("comment"),
rs.getTimestamp("create_date"),
rs.getTimestamp("update_date")), 1);
System.out.println(roomTable);
assertEquals("booked", roomTable.getComment());
}
可以看到控制檯列印的更新時間:
RoomTable{id=1, name='大床房', comment='booked', createDate=00:00:00, updateDate=22:23:18}
- 注意update的sql語法,我之前就是把逗號寫成了
and
總是報錯。 - 注意佔位符的匹配,按順序填充value。
- 更新成功應該返回1
之前提到,刪除操作通常並非真實的刪除一條記錄。而是設定一個flag,通過判斷flag來確定是否有效。
修改room的表,增加一個欄位active
.
mysql> alter table room add column `active` tinyint default 0 not null;
Query OK, 0 rows affected (0.16 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> desc room;
+-------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(80) | NO | | NULL | |
| comment | varchar(200) | YES | | NULL | |
| create_date | datetime | NO | | NULL | |
| update_date | datetime | NO | | NULL | |
| active | tinyint(4) | NO | | 0 | |
+-------------+--------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)
mysql> select * from room;
+----+------------+---------+---------------------+---------------------+--------+
| id | name | comment | create_date | update_date | active |
+----+------------+---------+---------------------+---------------------+--------+
| 1 | 大床房 | 無窗 | 2017-11-26 00:00:00 | 2017-11-26 00:00:00 | 0 |
| 2 | Double Bed | no | 2017-12-06 00:00:00 | 2017-12-06 00:00:00 | 0 |
| 3 | Big Bed | | 2017-12-06 00:00:00 | 2017-12-06 10:00:00 | 0 |
+----+------------+---------+---------------------+---------------------+--------+
3 rows in set (0.00 sec)
-
ALTER TABLE table_name ADD column_name datatype
為修改表,並增加一個field。 -
ALTER TABLE table_name DROP COLUMN column_name
為修改表,並刪除一個field。 -
ALTER TABLE table_name ALTER COLUMN column_name datatype
為修改表,並更改一個field。 -
tinyint
表示從 0 到 255 的整型資料。儲存大小為 1 位元組。 -
desc tableName
為查看錶結構。 - 看可以看到表結構已經改變,並且給active設定了預設值0,那麼當需要刪除時,設定為1.
下面,當接到一個刪除的需求時,我們設定active為1. 需要注意,由於每次測試都會重新覆蓋資料庫,需要將修改的sql放入schama.sql.
@Transactional
@Test
public void testUpdateForDelete(){
final String sql = "update room set `update_date`=?, `active`=1 where id=?";
final int update = jdbcTemplate.update(sql, new Object[]{new Date(), 1});
Assert.assertEquals(1, update);
final String getSql = "select `active` from room WHERE id=?";
Integer active = jdbcTemplate.queryForObject(getSql, Integer.class, 1);
System.out.println(active);
Assert.assertTrue(active == 1 );
}
批量新增/更新資料
有時候需要批量新增一些資料,比如匯入資料。這時候每條都執行一次sql就會顯得很慢。這裡提供了batch方法,可以一次同時插入多條資料。
@Test
public void testBatchInsert(){
final ArrayList<RoomTable> rooms = Lists.newArrayList(
new RoomTable("name1", "", new Date(), new Date()),
new RoomTable("name2", "", new Date(), new Date()),
new RoomTable("name3", "", new Date(), new Date())
);
final String sql = "INSERT INTO room(`name`, `comment`, `create_date`, `update_date`) VALUES (?,?,?,?)";
int[] ints = jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
final RoomTable roomTable = rooms.get(i);
ps.setString(1, roomTable.getName());
ps.setString(2, roomTable.getComment());
ps.setTimestamp(3, new java.sql.Timestamp(roomTable.getCreateDate().getTime()));
ps.setTimestamp(4, new java.sql.Timestamp(roomTable.getUpdateDate().getTime()));
}
@Override
public int getBatchSize() {
return rooms.size();
}
});
for (int anInt : ints) {
assertEquals(1, anInt);
}
final int count = jdbcTemplate.queryForObject("select count(*) from room", Integer.class);
assertEquals(6, count);
}
- 需要有一個list來儲存批量資料
- 呼叫batchUpdate方法即可,注意佔位符的順序
- 注意batch 的size
同時,提供了陣列版本:
@Test
public void testBatchInert2(){
final String sql = "INSERT INTO room(`name`, `comment`, `create_date`, `update_date`) VALUES (?,?,?,?)";
int[] ints = jdbcTemplate.batchUpdate(sql,
Lists.newArrayList(
new Object[]{"name1", "這是一條資料", new Date(), new Date()},
new Object[]{"name2", "這是另一條資料的value", new Date(), new Date()}));
for (int anInt : ints) {
assertEquals(1, anInt);
}
}
- 同樣需要使用佔位符
- 把需要批量的資料組成一個list,每個元素又是陣列,陣列的內容為一條資料的佔位符value
批量刪除資料
同理。
@Test
public void testBatchDelete() {
int[] ints = jdbcTemplate.batchUpdate("DELETE FROM room WHERE id=?", Lists.newArrayList(new Object[]{1}, new Object[]{2}, new Object[]{3}));
for (int anInt : ints) {
assertEquals(1, anInt);
}
final int count = jdbcTemplate.queryForObject("select count(*) from room", Integer.class);
assertEquals(0, count);
}
分層架構
程式碼的數量隨著業務會越積越多,為了能夠更容易開發,更容易維護,有許多規範需要遵守。最基本就是分層架構。
Domain Driven Design的主旨是模組化,模組內聚,模組間低耦合。只有分的開,互不相干,才能更好的維護,編寫程式碼才能更輕鬆。DDD裡的分層如下:
這是一個整體的層次劃分,落實到我們的程式碼上,則通常分為3層: controller, service, dao。
controller呼叫service,service呼叫dao。
controller負責路由分發。
service負責業務邏輯處理。
dao曾則是持久化層,服務物件和資料的持久化儲存。通常是存入資料庫。
實體entity
在DDD裡,重要的就是領域模型,上述的分層架構只是為了能讓模型間的互動更加清晰,那麼模型該如何定義? 俠義的理解,我們可以把一個Java bean當作一個model,當作一個領域模型。再具體的講,和資料庫表做對映的類,可以當作是領域物件。領域物件即entity,所以,在我們的架構裡會有個 entity的packag,用來存放領域物件。領域物件也給顯著的特徵是 有唯一性id , 通過唯一性id可以區分不同entity。
值物件valueobject
與entity相關的是值物件,即valueobject。值物件,即儲存值的物件。DTO可以說是一種值物件,值物件是在資料傳輸過程中使用的物件。因為資料傳輸過程中 可能會執行物件的方法,呼叫物件的屬性,甚至只需要領域物件的部分資料。所以不能直接講領域物件entity傳輸出來,而要使用值物件。值物件的一個顯著特徵 是 不可變,構造的值物件最好要設定為不可變更的。值物件對id沒有要求。
面向介面程式設計
Java中的介面可以通過子類向上轉型來代理實現類。interface只需要頂以好行為,然後就可以被呼叫。呼叫者只需要直到介面入參和返回值以及目的就可以了,完全 不用甚至不應該理會介面內部的實現,如此可以將業務邏輯隔離開來,降低耦合性。所以, 分層呼叫必須使用面向介面變成。
對應到我們的具體程式碼上,則應該controller
呼叫IService
, IService
呼叫IDao
。而serviceimpl
之類的實現類不應和呼叫者產生聯絡。
一個簡單的demo
清楚上述的幾個概念後,就可以理解接下來的做法了。我們從下往上,依次建立我們需要的類。
首先,是DAO層。我們需要一個使用者表來儲存使用者資訊,新建一個user表。
use springboot_demo;
CREATE TABLE `user`(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(8) NOT NULL UNIQUE,
`name` VARCHAR(12),
`create_date` datetime NOT NULL,
`update_date` datetime NOT NULL
)ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
- id是必須的,簡單設定為自增,主鍵。
- username是使用者的賬號,使用者登入賬號要唯一,所以設定為
UNIQUE
,同時必然可以為null。 - name長度設定為12個字元以內。
- 建立時間和更新時間必須。
然後,建立dao層。dao層需要和資料庫互動,則必須要一個entity來儲存資料,於是需要先新建一個entity。新建com.test.demo.domain.entity.UserTable
package com.test.demo.domain.entity;
import java.util.Date;
/**
* Created by Ryan Miao(http://www.cnblogs.com/woshimrf/)
*/
public class UserTable {
private Integer id;
private String username;
private String name;
private Date createDate;
private Date updateDate;
public UserTable() {
}
public UserTable(String username, String name, Date createDate, Date updateDate) {
this.username = username;
this.name = name;
this.createDate = createDate;
this.updateDate = updateDate;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getCreateDate() {
return createDate;
}
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
public Date getUpdateDate() {
return updateDate;
}
public void setUpdateDate(Date updateDate) {
this.updateDate = updateDate;
}
}
然後,建立我們的Dao, com.test.demo.domain.dao.IUserDao
package com.test.demo.domain.dao;
import com.test.demo.domain.entity.UserTable;
/**
* Created by Ryan Miao(http://www.cnblogs.com/woshimrf/)
*/
public interface IUserDao {
/**
* 建立一個使用者。
* @param userTable user資訊,id將被忽略
* @return id。
*/
Integer insert(UserTable userTable);
/**
* 獲取使用者by id。
*/
UserTable getById(Integer id);
}
實現類先暫停,繼續上一層,service層。新建``, 目標依舊是建立和獲取使用者。
package com.test.demo.domain.service;
import com.test.demo.domain.entity.UserTable;
/**
* Created by Ryan Miao(http://www.cnblogs.com/woshimrf/)
*/
public interface IUserService {
/**
* 建立一個使用者。
* @param userTable user資訊,id將被忽略
* @return id。
*/
Integer insert(UserTable userTable);
/**
* 獲取使用者by id。
*/
UserTable getById(Integer id);
}
到這裡,你會發現,這兩個介面明明一模一樣,除了名字。是的,在一定程度來說,這兩個抽象的介面的行為很相似,但從分層的理念上看,含義是不同的。我也是過了很久才體會到這種分層的好處的。分層可以把業務邏輯和資料處理隔離開來,這個demo裡業務簡單,所以看著相似,但事實上,service層要處理更多的業務邏輯,即實現層是不同的。service層也不僅僅是一個轉發。
有了service層,那麼可以在controller裡呼叫了。新建com.test.demo.controller.UserController
package com.test.demo.controller;
import com.test.demo.domain.entity.UserTable;
import com.test.demo.domain.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@Autowired
private IUserService userService;
@GetMapping("/{id}")
public UserTable getUserById(@PathVariable Integer id){
return userService.getById(id);
}
@PostMapping
public Integer insertUser(@RequestBody UserTable userTable){
return userService.insert(userTable);
}
}
-
@RestController
仍舊當作一個rest介面,即只是API,返回json資料。 -
@RequestMapping("/api/v1/users")
定義我們的資源字首和版本號 -
@Autowired private IUserService userService;
獲取我們的User service,@Autowired
是spring容器裡自動注入的註解,作用是幫忙生成一定物件,並賦值給它。這裡即獲得一個IUserService
物件。但是,在IDEA裡,你會看到編譯器報警,紅色的波浪線,Could not autowired
。 是指我們要注入一個IUserService
例項,但我們並沒有提供給它,它也就沒辦法找到並幫忙注入了。也就是說,我們的程式碼還不能用。還需要一個例項。一種做法是,像我們之前宣告JdbcTemplate
一樣,宣告一個出來。但前提是我們有這個class可以new,目前是我們只有介面,所以還需要建立它的實現類。
<未完待續!> 新建,``
JPA
JPA是Java Persistence API的簡稱。
相比jdbcTemplate, 需要寫sql,需要做對映。JPA提供了一個規範,即通常這樣寫,我給封裝好,你就這樣呼叫即可。
以下參考官方文件以及https://www.cnblogs.com/ityouknow/p/5891443.html。
引入JPA
新增依賴
compile("org.springframework.boot:spring-boot-starter-data-jpa")
testCompile group: 'com.h2database', name: 'h2', version: '1.4.196'
- 這個jpa會包含所有需要引用的依賴,注意,之前已經引入了MySQL,因此還是需要引入MySQL驅動的,不然無法自動檢測究竟使用的是哪個資料庫
- 這個h2是用來搞測試的。之前的測試全都是針對真實資料庫的。在後面我們會引入自動化測試,自動化測試會跑無數遍,肯定不能用真實的資料庫來測試程式碼邏輯的準確。因此,引入h2. h2是一個記憶體資料庫,Java編寫的。可以相容MySQL。後面,我們跑測試用例的時候,就會使用h2作為資料庫,而不是真實的MySQL。
修改和標註我們的實體類
這裡的JPA是基於註解來實現的。因此,我們需要標註實體類。