1. 程式人生 > >Java Web基礎入門

Java Web基礎入門

i18n

i18n

前言

語言都是相通的,只要搞清楚概念後就可以編寫程式碼了。而概念是需要學習成本的。

[Update] 由於樣式相容性問題,部落格內容部分不可見。可去個人部落格下找到。後面再fix樣式問題(我真不會搞前端啊)。部落格地址: https://blog.rmiao.top ,搜尋文章標題即可找到同一篇文章。

本文對應地址: https://blog.rmiao.top/Hello-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中的groupsusers都是資源的名稱,通過引數來確定資源的位置。

行為/操作(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

然後,建立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.googlecom.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按鈕。

另一個方式就是命令列:

細心可以發現專案根目錄下有gradlewgradlew.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;
    }
}
  1. LOG: 採用Sl4J介面
  2. 引數: @PathVariable 可以接收url路徑中的引數
  3. 引數: @RequestParam 可以接收?後的query引數
  4. 響應: @RestController == @[email protected], 其實,@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,
src\main\resources\static\content.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預設模板檔案讀取位置為:src\main\resources\templates. 新建 src\main\resources\templates\home.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?
摘自部落格園

首先,本地模擬跨域請求。

我們當前demo的域名為localhost:8081,現在新增一個本地域名, 在HOSTS檔案中新增:

127.0.0.1   corshost

然後,訪問http://corshost:8081,即本demo。

新增src\main\resources\static\cors.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.originalspringboot-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

Windows安裝

官網下載安裝包(mysql-5.7.20-winx64.zip). 當然,需要先註冊oracle賬號。

解壓當目錄,然後將bin目錄加入環境變數,同Java設定環境變數。這裡再次演示下。複製bin目錄地址,我的為D:\data\mysql\mysql-5.7.20-winx64\bin, 在此電腦右鍵,--> 屬性 --> 高階系統設定 --> 高階 --> 環境變數 --> 在系統環境變數中找到path --> 新建 --> 填入 --> 確認。

然後,重新開啟cmd。輸入mysqld --initialize --console

C:\Users\Ryan
λ 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 [email protected]: /r.Vtktfl9FN

複製我們的臨時密碼/r.Vtktfl9FN.

命令列啟動MySQL:

mysqld --console

新開一個cmd,命令列輸入賬號密碼mysql -u root -p

C:\Users\Ryan
λ 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)
  1. create table 建立表
  2. if not exists 如果不存在則建立
  3. room 表名
  4. id 表字段,欄位名為id, NOT NULL表示會給這個欄位建立非空索引,當存入空時會報錯。如果不寫明NOT NULL,則預設該欄位可以為空。
  5. AUTO_INCREMENT表示這個欄位會自動增加,即當儲存一條記錄的時候,如果不傳入id這個欄位,則該欄位會從系統序列中取出一個。該序列是一個遞增序列。即實現了每次id都增加1
  6. 反引號包裹欄位名是為了防止與關鍵字衝突
  7. INT 是指數字型別,括號裡的11是指MySQL裡的顯示寬度,和最大值取值範圍無關,是指需要多少位來表示這個數字,不夠長度的補齊。int最大值為2147483647
  8. VARCHAR是變長字串,即當儲存1個字元,則佔用空間就是1個位元組,當儲存2個字元,則佔用空間為2個字元。與之對應的是char定長。括號裡的是指字元的個數,即最大允許200個字元。
  9. DATA是日期型別,通常每條記錄都需要記錄建立時間和更新時間
  10. 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命令間接完成的提交為隱式提交。這些命令是:
ALTERAUDITCOMMENTCONNECTCREATEDISCONNECTDROP
EXITGRANTNOAUDITQUITREVOKERENAME

(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);
    }
}
  1. @Configuration 標註這個類是一個配置類,spring會自動掃描這個註解,將裡面的配置執行。
  2. @Bean 標註宣告一個Bean,由spring管理,在需要的地方注入。
  3. @Qualifier("dataSource") @Bean的引數列表中物件會從spring容器中查詢bean,找到後注入引數。而Qualifier則宣告要注入的bean的name或者id是什麼,這在spring容器包含2個以上同類型的bean的時候有用。
  4. DataSource 這個物件是springboot自動建立的,通過掃描配置類裡的配置,當檢測到有配置datasource的時候會建立這個bean。於是,在這裡就可以注入了,即我們配置的那幾個屬性。
  5. 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}]
  1. @RunWith(SpringRunner.class)執行spring容器的測試
  2. @SpringBootTest springboot測試
  3. @Import({Application.class, DBConfiguration.class}) 匯入我們需要的配置
  4. @Autowired自動注入屬性,剛才在Configuration中聲明瞭一個Bean,在這裡通過這個註解獲取那個bean
  5. @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方法論基於三個核心原則:

  1. 簡單:以最基本的形式建模資料。
  2. 傳達性:資料庫結構被任何人都能理解的語言文件化。
  3. 精確性:基於資料模型建立正確標準化的結構。
    典型地,建模者通過收集來自那些熟悉應用程式但不熟練的資料建模者的人的資訊開發資訊模型。建模者必須能夠用非技術企業專家可以理解的術語在概念層次上與資料結構進行通訊。建模者也必須能以簡單的單元分析資訊,對樣本資料進行處理。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);
}
  1. @Transactional是spring提供的事物註解,標註這個在測試類中的含義是:每次執行完該測試類後,回滾(rollback).
  2. jdbcTemplate.update(sql, 引數) 提供了佔位符的資料操縱語句的執行。為什麼要使用佔位符(PreparedStatement)而不是直接拼接字串?防止sql注入。
  3. RoomTable是一個新建Entity,關於什麼是Entity後面分層架構中將講到。
  4. 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);
}
  1. KeyHolder用來接收自動生成的主鍵.
  2. PreparedStatement用來建立一個佔位符的sql語句.
  3. 需要注意日期型別的對映規則,需要將java.util.Date轉換為java.sql.*
  4. 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());
}
  1. 注意要使用select 欄位列表來獲取想要的欄位,不要用*
  2. varchar的對映為String
  3. int的對映為Integer
  4. datetime的對映為time
  5. 此處的對映為一個lambda表示式,從結果集中選擇想要的欄位來建立我們的對映關係
  6. 最後一個引數是佔位符的值,防止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());
}
  1. 同樣要做結果集對映
  2. 同樣需要傳入佔位符value
  3. 返回值是一個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);
}
  1. 使用update方法,第二個引數為佔位符value
  2. 返回一個count表明生效的數量,這裡刪除了一條,應該返回1
  3. 為了驗證我們是否刪除成功了。首先,我們每次會初始化資料庫,資料庫中只有初始化的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}
  1. 注意update的sql語法,我之前就是把逗號寫成了and總是報錯。
  2. 注意佔位符的匹配,按順序填充value。
  3. 更新成功應該返回1

之前提到,刪除操作通常並非真實的刪除一條記錄。而是設定一個flag,通過判斷flag來確定是否有效。

修改room的表,增加一個欄位active.

mysql> alter table room add column `active` tinyint default 0 not null;            
Q