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是什么样子的。

url.png

资源(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

安装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
new.png?raw=true

如果发现没有JDK,那么new一个就好。

下一步,设置项目标签,group通常是公司名称倒写,比如com.googlecom.alibaba等. ArtifactId就是我们的项目名称,比如这次demo为springboot-demo
group.png?raw=true

然后一路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

commandline-debug.png?raw=true

然后,在IDEA中,点击Edit configurations
edit-configurations.png?raw=true

选择remote
remote.png?raw=true
debug.png?raw=true

然后,点击debug
debug-start.png?raw=true

如果想支持热加载,则需要添加

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 == @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;
}

save-room.png?raw=true

接收数组参数

@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
    ){

valid.png?raw=true

静态文件

在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?
摘自博客园

2009040916453171.jpg

cors执行过程摘自自由的维基百科
Flowchart_showing_Simple_and_Preflight_XHR.svg

首先,本地模拟跨域请求。

我们当前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

MAC安装参考Mac install 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 root@localhost: /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;            
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)                                                           
                                                                                   
  1. ALTER TABLE table_name ADD column_name datatype 为修改表,并增加一个field。
  2. ALTER TABLE table_name DROP COLUMN column_name 为修改表,并删除一个field。
  3. ALTER TABLE table_name ALTER COLUMN column_name datatype为修改表,并更改一个field。
  4. tinyint 表示从 0 到 255 的整型数据。存储大小为 1 字节。
  5. desc tableName为查看表结构。
  6. 看可以看到表结构已经改变,并且给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);
}
  1. 需要有一个list来存储批量数据
  2. 调用batchUpdate方法即可,注意占位符的顺序
  3. 注意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);
    }
}
  1. 同样需要使用占位符
  2. 把需要批量的数据组成一个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里的分层如下:
686418-20171218221518865-254833322.png

这是一个整体的层次划分,落实到我们的代码上,则通常分为3层: controller, service, dao。

686418-20171218223806178-819790245.jpg

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;
  1. id是必须的,简单设置为自增,主键。
  2. username是用户的账号,用户登录账号要唯一,所以设置为UNIQUE,同时必然可以为null。
  3. name长度设置为12个字符以内。
  4. 创建时间和更新时间必须。

然后,创建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);
    }
}
  1. @RestController仍旧当作一个rest接口,即只是API,返回json数据。
  2. @RequestMapping("/api/v1/users") 定义我们的资源前缀和版本号
  3. @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'
  1. 这个jpa会包含所有需要引用的依赖,注意,之前已经引入了MySQL,因此还是需要引入MySQL驱动的,不然无法自动检测究竟使用的是哪个数据库
  2. 这个h2是用来搞测试的。之前的测试全都是针对真实数据库的。在后面我们会引入自动化测试,自动化测试会跑无数遍,肯定不能用真实的数据库来测试代码逻辑的准确。因此,引入h2. h2是一个内存数据库,Java编写的。可以兼容MySQL。后面,我们跑测试用例的时候,就会使用h2作为数据库,而不是真实的MySQL。

修改和标注我们的实体类

这里的JPA是基于注解来实现的。因此,我们需要标注实体类。

DI

面向接口编程

编写测试

集成CI

登陆拦截

OAuth2.0

事物

JPA

缓存

远程调用

参考

  • https://codeplanet.io/principles-good-restful-api-design/
  • https://github.com/aisuhua/restful-api-design-references
  • http://www.jianshu.com/p/5659e6e16115
  • https://www.zhihu.com/question/28557115

转载于:https://www.cnblogs.com/woshimrf/p/java-web-springboot.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值