小黄

黄小黄的幸福生活!


  • 首页

  • 标签

  • 分类

  • 归档

  • Java

struts2总结(三)--Struts Tags

发表于 2019-04-21 | 分类于 Java , Struts2

5 Struts Tags

要在jsp中使用Struts2的标签,需要先引入<%@ taglib prefix="s" uri="/struts-tags" %>

5.1 通用标签

  • property

    1
    2
    3
    4
    <li>property: <s:property value="username"/> </li>
    <li>property 取值为字符串: <s:property value="'username'"/> </li>
    <li>property 设定默认值: <s:property value="admin" default="管理员"/> </li>
    <li>property 设定HTML: <s:property value="'<hr/>'" escape="false"/> </li>
  • set 将某个值放入指定范围内

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <li>set 设定adminName值(默认为request 和 ActionContext): <s:set var="adminName" value="username" /></li>
    <li>set 从request取值: <s:property value="#request.adminName" /></li>
    <li>set 从ActionContext取值: <s:property value="#adminName" /></li>
    <%--<li>set 设定范围: <s:set name="adminPassword" value="password" scope="page"/></li>
    <li>set 从相应范围取值: <%=pageContext.getAttribute("adminPassword") %></li>
    --%>
    <li>set 设定var,范围为ActionContext: <s:set var="adminPassword" value="password" scope="session"/></li>
    <li>set 使用#取值: <s:property value="#adminPassword"/> </li>
    <li>set 从相应范围取值: <s:property value="#session.adminPassword"/> </li>
  • bean 实例化一个JavaBean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <li>bean 定义bean,并使用param来设定新的属性值:
    <s:bean name="com.struts2.tags.Dog" >
    <s:param name="name" value="'pp'"></s:param>
    <s:property value="name"/>
    </s:bean>
    </li>
    <li>bean 查看debug情况:
    <s:bean name="com.struts2.tags.Dog" var="myDog">
    <s:param name="name" value="'oudy'"></s:param>
    </s:bean>
    拿出值:
    <s:property value="#myDog.name"/>
    </li>
  • include 将一JSP或者Servlet包含到当前页面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <li>include _include1.html 包含静态英文文件
    <s:include value="/_include1.html"></s:include>
    </li>
    <li>include _include2.html 包含静态中文文件
    <s:include value="/_include2.html"></s:include>
    </li>
    <li>include _include1.html 包含静态英文文件,说明%用法
    <s:set var="incPage" value="%{'/_include1.html'}" />
    <s:include value="%{#incPage}"></s:include>
    </li>
  • param 为其他标签提供参数

  • debug 辅助调试,查看值栈等信息

    5.2 控制标签

  • if elseif else 用来控制选择输出的标签

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <li>if elseif else:
    age = <s:property value="#parameters.age[0]" /> <br />
    <s:set var="age" value="#parameters.age[0]" />
    <s:if test="#age < 0">wrong age!</s:if>
    <s:elseif test="#parameters.age[0] < 20">too young!</s:elseif>
    <s:else>yeah!</s:else><br />
    <s:if test="#parameters.aaa == null">null</s:if>
    </li>
  • iterator 迭代器,用于将集合迭代输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    <li>遍历集合:<br />
    <s:iterator value="{1, 2, 3}" >
    <s:property/> |
    </s:iterator>
    </li>
    <li>自定义变量:<br />
    <s:iterator value="{'aaa', 'bbb', 'ccc'}" var="x">
    <s:property value="#x.toUpperCase()"/> |
    </s:iterator>
    </li>
    <li>使用status:<br />
    <s:iterator value="{'aaa', 'bbb', 'ccc'}" status="status">
    <s:property/> |
    遍历过的元素总数:<s:property value="#status.count"/> |
    遍历过的元素索引:<s:property value="#status.index"/> |
    当前是偶数?:<s:property value="#status.even"/> |
    当前是奇数?:<s:property value="#status.odd"/> |
    是第一个元素吗?:<s:property value="#status.first"/> |
    是最后一个元素吗?:<s:property value="#status.last"/>
    <br />
    </s:iterator>
    </li>
    <li>
    <s:iterator value="#{1:'a', 2:'b', 3:'c'}" >
    <s:property value="key"/> | <s:property value="value"/> <br />
    </s:iterator>
    </li>
    <li>
    <s:iterator value="#{1:'a', 2:'b', 3:'c'}" var="x">
    <s:property value="#x.key"/> | <s:property value="#x.value"/> <br />
    </s:iterator>
    </li>
    <li>
    <s:fielderror fieldName="fielderror.test" theme="simple"></s:fielderror>
    </li>
  • subset 用于截取集合的部分元素,形成新的子集合

    5.3 UI标签

    Struts2中一共定义了4个主题,simple, XHTML(默认), css_xhtml, ajax.

    5.4 AJAX标签

    后续可以查

    5.5 $ # % 的区别

  • $用于i18n和struts配置文件

  • #取得ActionContext的值
  • %将原本的文本属性解析为OGNL,对本来是OGNL的属性不起作用

参考文章

这部分用的不多,所以存了几个相关的比较全的博客地址,方便需要的时候去学习:
Struts2标签小结
UI标签
AJAX标签

struts2总结(一)--简介和Action

发表于 2019-04-21 | 分类于 Java , Struts2

1. 简介

Apache Struts2是一个基于MVC设计模式的Web应用框架,它本质上相当于一个servlet,在MVC设计模式中,Struts2作为控制器(Controller)来建立模型与视图的数据交互。Struts2是Struts1和WebWork的技术基础上进行了合并的全新的Struts2框架。其全新的Struts2的体系结构与Struts1的体系结构差别巨大。Struts2以WebWork为核心,采用拦截器的机制来处理用户的请求,这样的设也是的业务逻辑控制器能够ServletAPI完全脱离开。

1.1 Struts2的优点

  1. 在软件设计上,Struts2没有像struts1那样和Servlet API和struts API有着紧密的耦合,Struts2的应用可以不依赖于Servlet API和Struts2 API,属于无侵入式设计。
  2. Struts2提供了拦截器,利用拦截器可以进行AOP(面向切面编程),实现注入权限拦截等功能。
  3. Struts2提供了类型转换器,可以把特殊的请求参数转换为需要的类型。
  4. Struts2提供支持多种表现层技术,如JSP,freeMarker, Velocity等
  5. Struts2的输入校验可以对指定方法进行校验
  6. 提供了全局范围,包范围和Action范围的国际化资源文件管理实现

1.2 Web项目中的三层框架

1.2.1 表示层

Struts2框架就是工作在这里

1.2.2 业务逻辑层

Service层,处理业务逻辑,比如判断用户名是否存在,用户名和密码是否匹配,权限检查等。为一些执行某一个操作条件的判断。

1.2.3 数据访问层

dao层,专门处理和数据库进行交换的业务。jdbc/hibernate在这里使用。MVC架构是项目中三层架构的一部分,三层架构的表示层可以使用MVC架构的struts2框架来实现。

2. web项目中引入struts2框架

2.1 开发环境

  • IDE:Intellij IDEA 2017.2
  • Server:Tomcat 9.0
  • JDK:JDK 8

2.2 新建项目

File-New-Project
创建JavaEE项目,勾选WebApplication和Struts2选项。

2.3 目录结构

  • Project Name
    • .idea
    • lib
    • out
    • src
    • web
    • xx.iml
      需要注意将lib移动到web目录下面的Web-INF文件夹下,并在moudule中添加路径,不然部署过程中会报ClassNotFoundException。因为部署过程中只会读取web下的lib, 外部的lib是不会读取的。

2.4 struts2配置文件

src目录下面会生成struts.xml文件,默认内容如下:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
"http://struts.apache.org/dtds/struts-2.3.dtd">
<struts>
</struts>

2.5 web.xml文件中配置struts2框架的过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

这个过滤器的作用是拦截struts2框架中的action

3 Action

3.1 Action定义

  • struts2框架中有一种Java类叫做Action。struts2框架楼底层封装了Servlet的相关内容而实现出来的。替代之前Servlet的类在Struts2中成为Action。比Servlet功能更加强大,操作更加简单。原因在于拦截器的设计,拦截器可以拦截访问Action的请求,拦截请求之后,可以给Action添加很多丰富的功能,而Action专注于业务逻辑的处理就可以。

3.2 实现一个Action

3.2.1 类中只要有一个固定的方法:

1
2
3
4
5
public class IndexAction1 {
public String execute() {
return "success";
}
}

不需要实现或继承任何接口或者父类。execute方法一定要返回String类型的对象,每个字符串都可以对应一个跳转的页面。(字符串是可以自己随便定义的,字符串对应的哪一个跳转到页面也是自己定义的,在struts.xml文件中定义这些内容)

3.2.2 实现一个接口:com.opensyymphony.xwork2.Action

1
2
3
4
5
6
7
8
9
10
package com.struts2.front.action;
import com.opensymphony.xwork2.Action;
public class IndexAction2 implements Action{
@Override
public String execute() throws Exception {
return "success";
}
}

该接口有一个抽象方法execute()和五个静态属性:ERROR,SUCCESS, INPUT NONE, LOGIN

3.2.3 继承一个指定的父类ActionSupport

1
2
3
4
5
6
7
8
9
10
package com.struts2.front.action;
import com.opensymphony.xwork2.ActionSupport;
public class IndexAction3 extends ActionSupport{
@Override
public String execute() {
return "success";
}
}

这种方法是最常用的,继承ActionSupport类,重写execute()方法。实际上ActionSupport实现了Action接口。

3.3 在struts.xml中进行配置

配置之后,才可以通知struts2框架我们写的这个类是一个Action,将来struts2框架中要给这个类创建对象,调用方法以及这个Action加入更多丰富的功能,注册的实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
"http://struts.apache.org/dtds/struts-2.3.dtd">
<struts>
<constant name="struts.devMode" value="true" />
<package name="front" extends="struts-default" namespace="/">
<action name="index" class="com.struts2.front.action.IndexAction1">
<result name="success">/ActionIntroduction.jsp</result>
</action>
</package>
</struts>

  • <package>: 一个struts.xml文件中可以配置多个<package>标签,一个<package>标签里面可以配置多个<action>标签,一个<action>标签里面可以配多个<result>标签
  • name=”front” 表示给当前package起一个名字,唯一标示当前这个package,方便package之间的继承
  • extends=”struct-default” 表示当前这个package继承了另外一个名字叫做struct-default的package。这个package定义在core包中的struts-default.xml中定义。所有的package都会直接或者间接的继承struts-default这个包,类似java中的Object类。
  • namespace=”/“ 表示当前package的命名空间为/,以后这个package里面所有的action被访问时候,路径里面都要加上这个命名空间
  • <action>标签属性及其子标签:
    • name=”index” 表示当前配置这个action的名字为index,这个名字是自己定义的,可以和action类的名字相同,同时将来浏览器中的地址栏里面就要出现这个名字来访问当前这个action类
    • class=”com.struts2.front.action.IndexAction1” 表示当前配置的action对应的是IndexAction1这个类,
    • <result> 标签表示action访问完成之后,根据action返回的字符串的值,跳转不同的页面,默认结果为’success’, 可以省略,一般都是Action接口中的那5个返回值,也可以自己定义。

3.4 动态方法调用DMI

struts2默认调用的是action中的execute方法,实际上我们也可以让其调用其他的方法,首先定义一个类,让其继承ActionSupport类,配置的时候配置其方法,或者不写具体的方法,用DMI的方式进行调用:

1
2
3
4
5
6
7
8
9
package com.struts2.user.action;
import com.opensymphony.xwork2.ActionSupport;
public class UserAction extends ActionSupport{
public String add() {
return SUCCESS;
}
}

对其进行配置struts2.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<struts>
<!--<constant name="struts.enable.DynamicMethodInvocation" value="true"/>-->
<!--允许默认动态方法调用,Struts2默认不支持此功能-->
<constant name="struts.enable.DynamicMethodInvocation" value="true"/>
<constant name="struts.devMode" value="true" />
<package name="user" extends="struts-default" namespace="/user">
<action name="userAdd" class="com.struts2.user.action.UserAction" method="add">
<result>/user_add_success.jsp</result>
</action>
<action name="user" class="com.struts2.user.action.UserAction">
<result>/user_add_success.jsp</result>
</action>
</package>
</struts>

其中需要注意的是:

  • struts.enable.DynamicMethodInvocation 默认为false,将其设置为true
  • 配置action的时候,
  • 浏览器中的访问方法为:../user/user!add 动态方法调用, 或者../user/userAdd 都可以成功访问

3.5 使用通配符来配置action

从之前的学习中可以发现,多个action名字或者类的名字是有一定的规律的,比如我们要写两个action, StudentAction, TeacherAction, 每个action有两个方法,add和execute方法,配置的时候可以使用通配符来简化配置:

1
2
3
4
5
6
7
8
9
10
11
12
<struts>
<constant name="struts.devMode" value="true" />
<package name="actions" extends="struts-default" namespace="/actions">
<action name="Student*" class="com.struts2.action.StudentAction" method="{1}">
<result>/Student{1}_success.jsp</result>
</action>
<action name="*_*" class="com.struts2.action.{1}Action" method="{2}" >
<result>/{1}_{2}_success.jsp</result>
</action>
</package>
</struts>

其中{1}代表的是前面name属性中的第一个*号的值。这样可以简化配置,如果约定的好的话,整个配置文件可以只配置一个action。但是需要记住一点: 约定优于配置!!

3.6 action 的特点及其访问

3.6.1 特点:

  • Servlet 是线程不安全的,因为Servlet是单例
  • struts2 框架中的action是线程安全的,因为每次访问都会创建一个新的Action对象,所有的action里面可以随意定义成员变变量(只有成员变量才有线程安全的问题)

    3.6.2 访问:

    默认情况下,namespace/actionName.action或者namespace/actionName即可访问到action。访问成功会跳转到相应的jsp页面。由于这样会影响对相关Servlet的访问,所有最好默认action访问使用后缀名.action
  • 访问action的时候,action中方法的调用
    1. 默认情况下,访问action的 时候会调用action的execute()方法,这个方法执行完返回一个字符串,根据字符串进行跳转
    2. 可以在action标签里面添加一个method属性来指明将来访问这个action的时候会调用的方法
    3. 地址栏中动态指定调用的方法

3.7 Action接收参数

3.7.1 同名参数

例如通过页面将name=a&age=8这个参数传送给action,这时候需要:

  • action里面定义两个成员变量;name,age
  • action里面需要提供get/set方法
  • 当页面将参数传送过来的时候,struts2框架会自动帮我们把这两个参数的值放在action中的属性里面,并自动进行了类型转换。从而完成了参数的初始化,实际上这个是有defaultStack这个拦截器栈里面的拦截器来完成的

index.jsp:

1
2
3
4
5
6
7
8
9
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
使用action属性接受参数<a href="user/user!add?name=a&age=8">添加用户</a>
</body>
</html>

struts.xml:

1
2
3
4
5
6
7
8
9
<struts>
<constant name="struts.devMode" value="true"/>
<constant name="struts.enable.DynamicMethodInvocation" value="true" />
<package name="user" extends="struts-default" namespace="/user">
<action name="user" class="com.struts2.user.action.UserAction">
<result>/user_add_success.jsp</result>
</action>
</package>
</struts>

UserAction:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.struts2.user.action;
import com.opensymphony.xwork2.ActionSupport;
public class UserAction extends ActionSupport{
private String name;
private int age;
public String add() {
System.out.println("name = " + name);
System.out.println("age = " + age);
return SUCCESS;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}

3.7.2 DomainModel 域模型

在接收到的页面传值的时候,可以让struts2框架直接帮我们把这些接收到的值封装到一个JavaBean对象里面:

  1. Action中定义一个User类型的变量user,User类中有name,age两个成员变量及其get/set方法
  2. UserAction中提供这个User的get/set方法
  3. 页面向Action传递参数的名称要求user.name=1&user.age=23

接收到这个参数之后,struts2框架会帮我们去创建一个User队形,并且将所传的参数封装到对象的三个属性中去,最后将这个封装好的对象放到Action中的user属性中去。

3.7.3 模型驱动

实现ModelDriven接口,没怎么接触,不常用!创建一个User类有name,age两个成员变量,以及一个UserAction类,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserAction extends ActionSupport implements ModelDriven<User>{
private User user = new User();
public String add() {
System.out.println("name=" + user.getName());
System.out.println("age=" + user.getAge());
return SUCCESS;
}
@Override
public User getModel() {
return user;
}
}

3.8 简单数据验证

Action可以进行简单数据验证,这种验证一般不使用UI标签就可以完成,添加addFieldError到页面:

1
2
3
4
5
6
7
8
public String add() {
if(name == null || !name.equals("admin")) {
this.addFieldError("name", "name is error");
this.addFieldError("name", "name is too long");
return ERROR;
}
return SUCCESS;
}

3.9 访问Web元素

访问Web元素有四种方法,Map类型的和原始类型的各两种:

  1. Map类型 依赖于容器:Action初始化的时候生成三个Action容器,获取网页元素

    1
    2
    3
    4
    5
    public LoginAction1() {
    request = (Map)ActionContext.getContext().get("request");
    session = ActionContext.getContext().getSession();
    application = ActionContext.getContext().getApplication();
    }
  2. Map类型 IOC,Action实现了RequestAware,SessionAware, ApplicationAware三个接口,有三个Map类型的成员变量,并重写了成员变量的set方法,struts会自动调用set方法将元素注入到Action中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class LoginAction2 extends ActionSupport implements RequestAware, SessionAware, ApplicationAware{
    private Map<String, Object> request;
    private Map<String, Object> session;
    private Map<String, Object> application;
    public String execute() {
    request.put("r1", "r1");
    session.put("s1", "s1");
    application.put("a1", "a1");
    return SUCCESS;
    }
    @Override
    public void setApplication(Map<String, Object> application) {
    this.application = application;
    }
    @Override
    public void setRequest(Map<String, Object> request) {
    this.request = request;
    }
    @Override
    public void setSession(Map<String, Object> session) {
    this.session = session;
    }
    }
  3. 原始类型,IOC,实现了ServletRequestAware接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class LoginAction3 extends ActionSupport implements ServletRequestAware{
    private HttpServletRequest request;
    private HttpSession session;
    private ServletContext application;
    public String execute() {
    request.setAttribute("r1", "r1");
    session.setAttribute("s1", "s1");
    application.setAttribute("a1", "a1");
    return SUCCESS;
    }
    @Override
    public void setServletRequest(HttpServletRequest httpServletRequest) {
    this.request = request;
    this.session = request.getSession();
    this.application = session.getServletContext();
    }
    }
  4. 原始类型,依赖容器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class LoginAction4 extends ActionSupport{
    private HttpServletRequest request;
    private HttpSession session;
    private ServletContext application;
    public LoginAction4() {
    request = ServletActionContext.getRequest();
    session = request.getSession();
    application = session.getServletContext();
    }
    public String execute() {
    request.setAttribute("r1", "r1");
    session.setAttribute("s1", "s1");
    application.setAttribute("a1", "a1");
    return SUCCESS;
    }
    }
  • 原类型和map类型的关系
    1. 使用Map类型的对象,可以降低对ServletAPI的依赖
    2. 我们使用元类型大多时候也是存值和取值,原来性里面本身也封装了Map对象,使用Map类型对象也可以完成存值和取值
    3. map类型的request对象里面的k-v值是复制原类型request对象的键值对的,元类型的request对象中还包含其他的属性和方法。
    4. 原类型和Map类型的Request对象是相同的 ,可以互相读取值,session和application也一样。

3.10 包含文件配置

struts2支持配置文件之间的继承,通过include标签完成,相当于将include内容复制到include的位置:

  • struts.xml:

    1
    2
    3
    4
    <struts>
    <constant name="struts.devMode" value="true" />
    <include file="login.xml" />
    </struts>
  • login.xml:

    1
    2
    3
    4
    5
    6
    7
    <struts>
    <package name="login" extends="struts-default" namespace="/login">
    <action name="login*" class="com.bjsxt.struts2.user.action.LoginAction{1}">
    <result>/user_login_success.jsp</result>
    </action>
    </package>
    </struts>

3.11 默认Action处理

当struts中没有找到请求的Action的时候会报错,我们可以设置一个默认的Action,当找不到请求的action的时候访问这个特定的Action,使得用户体验更加友好。在package中配置一条语句就可以完成:

  • <default-action-ref name="index"></default-action-ref>

SQL开发技巧

发表于 2019-04-21 | 分类于 MySQL , database

SQL语句分类

  • DDL:数据定义语言
  • TPL:事务处理语言
  • DCL:数据控制语言-grant
  • DML:数据操作语言-crud

join从句

操作表数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mysql> select * from user1;
+----+-----------+----------+
| id | user_name | over |
+----+-----------+----------+
| 1 | 唐僧 | 唐僧成佛 |
| 2 | 猪八戒 | 净坛使者 |
| 3 | 孙悟空 | 斗战胜佛 |
| 4 | 沙僧 | 金身罗汉 |
+----+-----------+----------+
4 rows in set (0.00 sec)
mysql> select * from user2;
+----+-----------+--------+
| id | user_name | over |
+----+-----------+--------+
| 1 | 孙悟空 | 成佛 |
| 2 | 牛魔王 | 妖怪吧 |
| 3 | 蛟魔王 | 妖怪吧 |
| 4 | 鹏魔王 | 妖怪吧 |
| 5 | 狮驼王 | 妖怪吧 |
+----+-----------+--------+
5 rows in set (0.00 sec)

内连接 inner

基于连接谓词将两张表的列组合在一起,产生新的结果表,交集。

1
2
3
4
5
6
7
mysql> select a.user_name, a.over, b.over from user1 a join user2 b on a.user_name = b.user_name;
+-----------+----------+------+
| user_name | over | over |
+-----------+----------+------+
| 孙悟空 | 斗战胜佛 | 成佛 |
+-----------+----------+------+
1 row in set (0.00 sec)

全外连接 full outer

并集,MySQL中不支持直接的full join。用union all来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> select a.user_name, a.over, b.over from user1 a left join user2 b on a.user_name = b. user_name
-> union all
-> select b.user_name, b.over, a.over
-> from user1 a right join user2 b on b.user_name = a.user_name;
+-----------+----------+----------+
| user_name | over | over |
+-----------+----------+----------+
| 孙悟空 | 斗战胜佛 | 成佛 |
| 唐僧 | 唐僧成佛 | NULL |
| 猪八戒 | 净坛使者 | NULL |
| 沙僧 | 金身罗汉 | NULL |
| 孙悟空 | 成佛 | 斗战胜佛 |
| 牛魔王 | 妖怪吧 | NULL |
| 蛟魔王 | 妖怪吧 | NULL |
| 鹏魔王 | 妖怪吧 | NULL |
| 狮驼王 | 妖怪吧 | NULL |
+-----------+----------+----------+
9 rows in set (0.00 sec)

左外连接 left outer

以左表为准。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> select a.user_name, a.over, b.over from user1 a left join user2 b on a.user_name = b.user_name where b.user_name is null;
+-----------+----------+------+
| user_name | over | over |
+-----------+----------+------+
| 唐僧 | 唐僧成佛 | NULL |
| 猪八戒 | 净坛使者 | NULL |
| 沙僧 | 金身罗汉 | NULL |
+-----------+----------+------+
3 rows in set (0.00 sec)
mysql> select a.user_name, a.over, b.over from user1 a left join user2 b on a.user_name = b.user_name where b.user_name is not null;
+-----------+----------+------+
| user_name | over | over |
+-----------+----------+------+
| 孙悟空 | 斗战胜佛 | 成佛 |
+-----------+----------+------+
1 row in set (0.00 sec)

右外连接 right outer

以右表为准。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> select b.user_name, b.over, a.over from user1 a right join user2 b on b.user_name = a.user_name where a.user_name is null;
+-----------+--------+------+
| user_name | over | over |
+-----------+--------+------+
| 牛魔王 | 妖怪吧 | NULL |
| 蛟魔王 | 妖怪吧 | NULL |
| 鹏魔王 | 妖怪吧 | NULL |
| 狮驼王 | 妖怪吧 | NULL |
+-----------+--------+------+
4 rows in set (0.00 sec)
mysql> select b.user_name, b.over, a.over from user1 a right join user2 b on b.user_name = a.user_name where a.user_name is not null;
+-----------+------+----------+
| user_name | over | over |
+-----------+------+----------+
| 孙悟空 | 成佛 | 斗战胜佛 |
+-----------+------+----------+
1 row in set (0.00 sec)

交叉连接 cross

实际中需要避免。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
mysql> select a.user_name, a.over, b.over from user1 a cross join user2 b;
+-----------+----------+--------+
| user_name | over | over |
+-----------+----------+--------+
| 唐僧 | 唐僧成佛 | 成佛 |
| 猪八戒 | 净坛使者 | 成佛 |
| 孙悟空 | 斗战胜佛 | 成佛 |
| 沙僧 | 金身罗汉 | 成佛 |
| 唐僧 | 唐僧成佛 | 妖怪吧 |
| 猪八戒 | 净坛使者 | 妖怪吧 |
| 孙悟空 | 斗战胜佛 | 妖怪吧 |
| 沙僧 | 金身罗汉 | 妖怪吧 |
| 唐僧 | 唐僧成佛 | 妖怪吧 |
| 猪八戒 | 净坛使者 | 妖怪吧 |
| 孙悟空 | 斗战胜佛 | 妖怪吧 |
| 沙僧 | 金身罗汉 | 妖怪吧 |
| 唐僧 | 唐僧成佛 | 妖怪吧 |
| 猪八戒 | 净坛使者 | 妖怪吧 |
| 孙悟空 | 斗战胜佛 | 妖怪吧 |
| 沙僧 | 金身罗汉 | 妖怪吧 |
| 唐僧 | 唐僧成佛 | 妖怪吧 |
| 猪八戒 | 净坛使者 | 妖怪吧 |
| 孙悟空 | 斗战胜佛 | 妖怪吧 |
| 沙僧 | 金身罗汉 | 妖怪吧 |
+-----------+----------+--------+
20 rows in set (0.00 sec)

如何更新过滤条件中包括自身的表

直接更新是不可以的:

1
2
3
4
5
mysql> update user1 set over='齐天大圣' where user1.user_name in (
-> select b.user_name
-> from user1 a inner join user2 b on
-> a.user_name=b.user_name);
ERROR 1093 (HY000): You can't specify target table 'user1' for update in FROM clause

先join,更新join之后的表方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> update user1 a join(
-> select b.user_name
-> from user1 a inner join user2 b on
-> a.user_name=b.user_name) b on a.user_name = b.user_name set a.over='齐天大圣';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from user1;
+----+-----------+----------+
| id | user_name | over |
+----+-----------+----------+
| 1 | 唐僧 | 唐僧成佛 |
| 2 | 猪八戒 | 净坛使者 |
| 3 | 孙悟空 | 齐天大圣 |
| 4 | 沙僧 | 金身罗汉 |
+----+-----------+----------+
4 rows in set (0.00 sec)

join优化子查询

低效的子查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
mysql> select a.user_name, a.over, (select over from user2 b where a.user_name=b.user_name) as over2 from user1 a;
+-----------+----------+-------+
| user_name | over | over2 |
+-----------+----------+-------+
| 唐僧 | 唐僧成佛 | NULL |
| 猪八戒 | 净坛使者 | NULL |
| 孙悟空 | 齐天大圣 | 成佛 |
| 沙僧 | 金身罗汉 | NULL |
+-----------+----------+-------+
4 rows in set (0.00 sec)
mysql> explain select a.user_name, a.over, (select over from user2 b where a.user_name=b.user_name) as over2 from user1 a\G
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: a
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 4
filtered: 100.00
Extra: NULL
*************************** 2. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY
table: b
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 5
filtered: 20.00
Extra: Using where
2 rows in set, 2 warnings (0.00 sec)

left join优化后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
mysql> select a.user_name, a.over, b.over as over2 from user1 a left join user2 b on a.user_name=b.user_name;
+-----------+----------+-------+
| user_name | over | over2 |
+-----------+----------+-------+
| 孙悟空 | 齐天大圣 | 成佛 |
| 唐僧 | 唐僧成佛 | NULL |
| 猪八戒 | 净坛使者 | NULL |
| 沙僧 | 金身罗汉 | NULL |
+-----------+----------+-------+
4 rows in set (0.00 sec)
mysql> explain select a.user_name, a.over, b.over as over2 from user1 a left join user2 b on a.user_name=b.user_name\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: a
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 4
filtered: 100.00
Extra: NULL
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: b
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 5
filtered: 100.00
Extra: Using where; Using join buffer (Block Nested Loop)
2 rows in set, 1 warning (0.00 sec)

join优化聚合子查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
mysql> select a.user_name, b.timestr, b.kills from user1 a join user_kills b on a.id=b.user_id where b.kills=(select max(c.kills) from user_kills c where c.user_id = b.user_id);
+-----------+-----------+-------+
| user_name | timestr | kills |
+-----------+-----------+-------+
| 猪八戒 | 2013-2-5 | 12 |
| 沙僧 | 2013-1-10 | 3 |
+-----------+-----------+-------+
2 rows in set (0.00 sec)
mysql> explain select a.user_name, b.timestr, b.kills from user1 a join user_kills b on a.id=b.user_id where b.kills=(select max(c.kills) from user_kills c where c.user_id = b.user_id)\G
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: a
partitions: NULL
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 4
filtered: 100.00
Extra: NULL
*************************** 2. row ***************************
id: 1
select_type: PRIMARY
table: b
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 5
filtered: 20.00
Extra: Using where; Using join buffer (Block Nested Loop)
*************************** 3. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY
table: c
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 5
filtered: 20.00
Extra: Using where
3 rows in set, 2 warnings (0.00 sec)

优化后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
mysql> select a.user_name, b.timestr, b.kills from user1 a join user_kills b on a.id = b.user_id join user_kills c on c.user_id=b.user_id group by a.user_name, b.timestr, b.kills having b.kills = max(c.kills);
+-----------+-----------+-------+
| user_name | timestr | kills |
+-----------+-----------+-------+
| 沙僧 | 2013-1-10 | 3 |
| 猪八戒 | 2013-2-5 | 12 |
+-----------+-----------+-------+
2 rows in set (0.00 sec)
mysql> explain select a.user_name, b.timestr, b.kills from user1 a join user_kills b on a.id = b.user_id join user_kills c on c.user_id=b.user_id group by a.user_name, b.timestr, b.kills having b.kills = max(c.kills)\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: b
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 5
filtered: 100.00
Extra: Using where; Using temporary; Using filesort
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: c
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 5
filtered: 20.00
Extra: Using where; Using join buffer (Block Nested Loop)
*************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: a
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: test1.b.user_id
rows: 1
filtered: 100.00
Extra: NULL
3 rows in set, 1 warning (0.00 sec)

如何实现分组选择

每一组数据中选出前几名的情景,可以分组逐个进行查询,这种情况下,分组比较多,需要执行同一次查询很多次,而且增加了应用程序与数据库的交互次数,增加了数据库执行查询的次数,不符合批处理的原则,也会增加网络流量,因此实际中不使用这种方式。

优化方法:

1
2
3
4
5
6
7
8
9
mysql> select d.user_name, c.timestr, kills from (select user_id, timestr, kills, (select count(*) from user_kills b where b.user_id=a.user_id and a.kills<=b.kills) as cnt from user_kills a group by user_id, timestr, kills) c join user1 d on c.user_id=d.id where cnt<=2;
+-----------+-----------+-------+
| user_name | timestr | kills |
+-----------+-----------+-------+
| 猪八戒 | 2013-2-5 | 12 |
| 猪八戒 | 2013-1-10 | 10 |
| 沙僧 | 2013-1-10 | 3 |
+-----------+-----------+-------+
3 rows in set (0.00 sec)

如何进行行列转换

行转列

报表统计,汇总显示会经常使用行转列的操作。

  • 打印流水单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    mysql> select a.user_name, b.kills from user1 a join user_kills b on a.id=b.user_id;
    +-----------+-------+
    | user_name | kills |
    +-----------+-------+
    | 猪八戒 | 10 |
    | 猪八戒 | 2 |
    | 猪八戒 | 12 |
    | 沙僧 | 3 |
    | 猪八戒 | 6 |
    | 孙悟空 | 50 |
    | 孙悟空 | 12 |
    | 孙悟空 | 5 |
    | 猪八戒 | 15 |
    | 沙僧 | 123 |
    | 沙僧 | 14 |
    | 沙僧 | 10 |
    +-----------+-------+
    12 rows in set (0.00 sec)
  • 汇总通过sum和group by

    1
    2
    3
    4
    5
    6
    7
    8
    9
    mysql> select a.user_name, sum(b.kills) from user1 a join user_kills b on a.id=b.user_id group by a.user_name;
    +-----------+--------------+
    | user_name | sum(b.kills) |
    +-----------+--------------+
    | 孙悟空 | 67 |
    | 沙僧 | 150 |
    | 猪八戒 | 45 |
    +-----------+--------------+
    3 rows in set (0.00 sec)
  • 通过cross join来实现行转列, 存在性能问题。

    1
    2
    3
    4
    5
    6
    7
    mysql> select * from (select sum(b.kills) as '猪八戒' from user1 a join user_kills b on a.id=b.user_id and user_name='猪八戒') a cross join (select sum(b.kills) as '孙悟空' from user1 a join user_kills b on a.id=b.user_id and user_name='孙悟空') b cross join (select sum(b.kills) as '沙僧' from user1 a join user_kills b on a.id=b.user_id and user_name='沙僧') c;
    +--------+--------+------+
    | 猪八戒 | 孙悟空 | 沙僧 |
    +--------+--------+------+
    | 45 | 67 | 150 |
    +--------+--------+------+
    1 row in set (0.00 sec)
  • 使用case语句来实现行转列

    1
    2
    3
    4
    5
    6
    7
    mysql> select sum(case when user_name='孙悟空' then kills end) as '孙悟空', sum(case when user_name='猪八戒' then kills end) as '猪八戒', sum(case when user_name='沙僧' then kills end) as '沙僧' from user1 a join user_kills b on a.id = b.user_id;
    +--------+--------+------+
    | 孙悟空 | 猪八戒 | 沙僧 |
    +--------+--------+------+
    | 67 | 45 | 150 |
    +--------+--------+------+
    1 row in set (0.00 sec)

列转行

属性拆分,比如权限分隔。

  • 利用序列表实现列转行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    mysql> SELECT
    user_name,REPLACE(substring(SUBSTRING_INDEX(mobile,',',a.id),CHAR_LENGTH(SUBSTRING_INDEX(mobile,',',a.id-1))+1),',','') AS mobile
    FROM tb_sequence AS a
    CROSS JOIN
    (SELECT user_name, CONCAT(mobile,',') AS mobile, (LENGTH(mobile)-LENGTH(REPLACE(mobile,',',''))+1) AS size FROM user1) AS b
    ON a.id <= b.size;
    +-----------+----------+
    | user_name | mobile |
    +-----------+----------+
    | 唐僧 | 12123123 |
    | 唐僧 | 123 |
    | 唐僧 | 123432 |
    | 唐僧 | 2131 |
    | 唐僧 | 4213 |
    | 唐僧 | 3421 |
    | 猪八戒 | 12123123 |
    | 猪八戒 | 123 |
    | 猪八戒 | 123432 |
    | 猪八戒 | 2131 |
    | 猪八戒 | 4213 |
    | 猪八戒 | 3421 |
    | 孙悟空 | 12123123 |
    | 孙悟空 | 123 |
    | 孙悟空 | 123432 |
    | 孙悟空 | 2131 |
    | 孙悟空 | 4213 |
    | 孙悟空 | 3421 |
    | 沙僧 | 12123123 |
    | 沙僧 | 123 |
    | 沙僧 | 123432 |
    | 沙僧 | 2131 |
    | 沙僧 | 4213 |
    | 沙僧 | 3421 |
    +-----------+----------+
    24 rows in set (0.00 sec)
  • 使用union join实现行列转换

原数据

1
2
3
4
5
6
7
8
9
10
mysql> select user_name, arms, clothing, shoe from user1 a join user1_equipment b on a.id = b.user_id;
+-----------+----------+------------+------------+
| user_name | arms | clothing | shoe |
+-----------+----------+------------+------------+
| 唐僧 | 九环锡杖 | 袈裟 | 僧鞋 |
| 猪八戒 | 九齿钉耙 | 僧衣 | 僧鞋 |
| 孙悟空 | 金箍棒 | 梭子黄金甲 | 藕丝云步履 |
| 沙僧 | 降妖宝杖 | 僧衣 | 僧鞋 |
+-----------+----------+------------+------------+
4 rows in set (0.00 sec)

结果集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select user_name, 'arms' as equipment, arms from user1 a join user1_equipment b on a.id = b.user_id
-> union all
-> select user_name, 'clothing' as equipment, clothing from user1 a join user1_equipment b on a.id = b.user_id
-> union all
-> select user_name, 'shoe' as equipment, shoe from user1 a join user1_equipment b on a.id = b.user_id
-> order by user_name;
+-----------+-----------+------------+
| user_name | equipment | arms |
+-----------+-----------+------------+
| 唐僧 | clothing | 袈裟 |
| 唐僧 | arms | 九环锡杖 |
| 唐僧 | shoe | 僧鞋 |
| 孙悟空 | arms | 金箍棒 |
| 孙悟空 | shoe | 藕丝云步履 |
| 孙悟空 | clothing | 梭子黄金甲 |
| 沙僧 | shoe | 僧鞋 |
| 沙僧 | clothing | 僧衣 |
| 沙僧 | arms | 降妖宝杖 |
| 猪八戒 | shoe | 僧鞋 |
| 猪八戒 | clothing | 僧衣 |
| 猪八戒 | arms | 九齿钉耙 |
+-----------+-----------+------------+
12 rows in set (0.00 sec)
  • 使用序列号进行列转行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    mysql> select user_name,
    -> case when c.id=1 then 'arms'
    -> when c.id=2 then 'clothing'
    -> when c.id=3 then 'shoe'
    -> end as equipment,
    -> coalesce(case when c.id=1 then arms end,
    -> case when c.id=2 then clothing end,
    -> case when c.id=3 then shoe end) as eq_name
    -> from user1 a
    -> join user1_equipment b on a.id=b.user_id
    -> cross join tb_sequence c where c.id<=3 order by user_name;
    +-----------+-----------+------------+
    | user_name | equipment | eq_name |
    +-----------+-----------+------------+
    | 唐僧 | shoe | 僧鞋 |
    | 唐僧 | arms | 九环锡杖 |
    | 唐僧 | clothing | 袈裟 |
    | 孙悟空 | shoe | 藕丝云步履 |
    | 孙悟空 | arms | 金箍棒 |
    | 孙悟空 | clothing | 梭子黄金甲 |
    | 沙僧 | clothing | 僧衣 |
    | 沙僧 | shoe | 僧鞋 |
    | 沙僧 | arms | 降妖宝杖 |
    | 猪八戒 | clothing | 僧衣 |
    | 猪八戒 | shoe | 僧鞋 |
    | 猪八戒 | arms | 九齿钉耙 |
    +-----------+-----------+------------+
    12 rows in set (0.00 sec)

如何生成唯一序列号

需要的场景:

  • 数据库主键,聚集索引方式
  • 业务序列号如发票号,车票号,订单号

生成序列号的方法:

  • MySQL:auto_increment
  • Oracle:sequence
  • SQLServer:identity/sequence

如何选择生成序列号的方式:

  • 优先选择系统提供的序列号生成方式,需要注意数据空洞的问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    mysql> create table t (id int auto_increment not null, primary key(id));
    Query OK, 0 rows affected (0.04 sec)
    mysql> insert into t value(),();
    Query OK, 2 rows affected (0.01 sec)
    Records: 2 Duplicates: 0 Warnings: 0
    mysql> select * from t;
    +----+
    | id |
    +----+
    | 1 |
    | 2 |
    +----+
    2 rows in set (0.00 sec)
    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    mysql> insert into t values();
    Query OK, 1 row affected (0.00 sec)
    mysql> select * from t;
    +----+
    | id |
    +----+
    | 1 |
    | 2 |
    | 3 |
    +----+
    3 rows in set (0.00 sec)
    mysql> rollback;
    Query OK, 0 rows affected (0.01 sec)
    mysql> insert into t values();
    Query OK, 1 row affected (0.00 sec)
    mysql> select * from t;
    +----+
    | id |
    +----+
    | 1 |
    | 2 |
    | 4 |
    +----+
    3 rows in set (0.00 sec)
    • 使用SQL方式生成序列号,特殊情况下使用。如生成订单序列号,要求序列号的格式为YYYYMMDDNNNNNNN

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      CREATE PROCEDURE usp_seqnum()
      BEGIN
      #定义变量并获取相关值
      DECLARE v_cnt INT;
      DECLARE v_timestr INT;
      SET v_timestr = DATE_FORMAT(NOW(),'%Y%m%d');
      SELECT ROUND(RAND()*100,0)+1 INTO v_cnt;
      #新建表
      DROP TABLE IF EXISTS im_orderseq;
      CREATE TABLE im_orderseq(
      timestr NVARCHAR(8) NOT NULL ,
      ordersn INT(3)
      );
      START TRANSACTION;
      #更新表的最值
      UPDATE im_orderseq SET ordersn = ordersn + v_cnt WHERE timestr = v_timestr;
      IF ROW_COUNT() = 0 THEN
      #插入数据
      INSERT INTO im_orderseq(timestr,ordersn) VALUES(v_timestr,v_cnt);
      END IF;
      SELECT CONCAT(v_timestr,LPAD(ordersn,7,0))AS ordersn
      FROM im_orderseq WHERE timestr = v_timestr;
      COMMIT;
      END;
      mysql> call usp_seqnum();
      +-----------------+
      | ordersn |
      +-----------------+
      | 201804270000009 |
      +-----------------+
      1 row in set (0.04 sec)
      Query OK, 0 rows affected (0.05 sec)

如何删除重复元素

产生原因:

* 人为原因,重复录入数据,重复提交数据
* 系统原因,由于系统升级或设计的原因使原来可以重复的数据变为不能重复了。

查询数据是否重复:group by + having

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
mysql> select * from user1_test;
+----+-----------+----------+------------------------------------+
| id | user_name | over | mobile |
+----+-----------+----------+------------------------------------+
| 1 | 唐僧 | 唐僧成佛 | 12123123,123,123432,2131,4213,3421 |
| 2 | 猪八戒 | 净坛使者 | 12123123,123,123432,2131,4213,3421 |
| 3 | 孙悟空 | 齐天大圣 | 12123123,123,123432,2131,4213,3421 |
| 4 | 沙僧 | 金身罗汉 | 12123123,123,123432,2131,4213,3421 |
| 8 | 唐僧 | 唐僧成佛 | 12123123,123,123432,2131,4213,3421 |
| 9 | 猪八戒 | 净坛使者 | 12123123,123,123432,2131,4213,3421 |
| 10 | 孙悟空 | 齐天大圣 | 12123123,123,123432,2131,4213,3421 |
| 11 | 沙僧 | 金身罗汉 | 12123123,123,123432,2131,4213,3421 |
+----+-----------+----------+------------------------------------+
8 rows in set (0.00 sec)
mysql> select user_name, count(*) from user1_test group by user_name having count(*)>1;
+-----------+----------+
| user_name | count(*) |
+-----------+----------+
| 唐僧 | 2 |
| 孙悟空 | 2 |
| 沙僧 | 2 |
| 猪八戒 | 2 |
+-----------+----------+
4 rows in set (0.00 sec)

删除重复数据,相同数据保留ID最大的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
mysql> select * from user1_test;
+----+-----------+----------+------------------------------------+
| id | user_name | over | mobile |
+----+-----------+----------+------------------------------------+
| 1 | 唐僧 | 唐僧成佛 | 12123123,123,123432,2131,4213,3421 |
| 2 | 猪八戒 | 净坛使者 | 12123123,123,123432,2131,4213,3421 |
| 3 | 孙悟空 | 齐天大圣 | 12123123,123,123432,2131,4213,3421 |
| 4 | 沙僧 | 金身罗汉 | 12123123,123,123432,2131,4213,3421 |
| 8 | 唐僧 | 唐僧成佛 | 12123123,123,123432,2131,4213,3421 |
| 9 | 猪八戒 | 净坛使者 | 12123123,123,123432,2131,4213,3421 |
| 10 | 孙悟空 | 齐天大圣 | 12123123,123,123432,2131,4213,3421 |
| 11 | 沙僧 | 金身罗汉 | 12123123,123,123432,2131,4213,3421 |
+----+-----------+----------+------------------------------------+
8 rows in set (0.00 sec)
mysql> delete a from user1_test a join (
-> select user_name, count(*), max(id) as id
-> from user1_test
-> group by user_name
-> having count(*)>1) b
-> on a.user_name=b.user_name
-> where a.id < b.id;
Query OK, 4 rows affected (0.01 sec)
mysql> select * from user1_test;
+----+-----------+----------+------------------------------------+
| id | user_name | over | mobile |
+----+-----------+----------+------------------------------------+
| 8 | 唐僧 | 唐僧成佛 | 12123123,123,123432,2131,4213,3421 |
| 9 | 猪八戒 | 净坛使者 | 12123123,123,123432,2131,4213,3421 |
| 10 | 孙悟空 | 齐天大圣 | 12123123,123,123432,2131,4213,3421 |
| 11 | 沙僧 | 金身罗汉 | 12123123,123,123432,2131,4213,3421 |
+----+-----------+----------+------------------------------------+
4 rows in set (0.00 sec)

如何在子查询中匹配两个值

  • 子查询:当一个查询是另一个查询的条件时,成为子查询。

解决同属性多值过滤的问题

如何计算累进税问题

Springmvc总结

发表于 2019-04-21 | 分类于 Java , Springmvc

1、Springmvc介绍

  • Spring MVC 是一个模型 - 视图 - 控制器(MVC)的Web框架建立在中央前端控制器servlet(DispatcherServlet),它负责发送每个请求到合适的处理程序,使用视图来最终返回响应结果的概念。Spring MVC 是 Spring 产品组合的一部分,它享有 Spring IoC容器紧密结合Spring松耦合等特点,因此它有Spring的所有优点。

  • Springmvc处理流程

    Alt text

2、入门程序

1、开发环境

  • IDEA 2017.2
  • JDK 1.8.0.141
  • Tomcat 9.0.0 M22
  • Springmvc 4.1.3

2、需求

使用Springmvc实现商品列表的展示:

  • 请求的url:/itemList.action

  • 参数:无

  • 数据:静态数据

3、开发步骤

  1. 创建一个Javaweb工程

  2. 导入jar包

    Alt text

  3. 创建itemList.jsp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    <%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
    <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>查询商品列表</title>
    </head>
    <body>
    <form action="${pageContext.request.contextPath }/item/queryitem.action" method="post">
    查询条件:
    <table width="100%" border=1>
    <tr>
    <td><input type="submit" value="查询"/></td>
    </tr>
    </table>
    商品列表:
    <table width="100%" border=1>
    <tr>
    <td>商品名称</td>
    <td>商品价格</td>
    <td>生产日期</td>
    <td>商品描述</td>
    <td>操作</td>
    </tr>
    <c:forEach items="${itemList }" var="item">
    <tr>
    <td>${item.name }</td>
    <td>${item.price }</td>
    <td><fmt:formatDate value="${item.createtime}" pattern="yyyy-MM-dd HH:mm:ss"/></td>
    <td>${item.detail }</td>
    <td><a href="${pageContext.request.contextPath }/itemEdit.action?id=${item.id}">修改</a></td>
    </tr>
    </c:forEach>
    </table>
    </form>
    </body>
    </html>
  4. 创建ItemsController

    ItemController是一个普通的java类,不需要实现任何接口,只需要在类上添加@Controller注解即可。@RequestMapping注解指定请求的url,其中“.action”可以加也可以不加。在ModelAndView对象中,将视图设置为“/WEB-INF/jsp/itemList.jsp”。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    @Controller
    publicclass ItemController {
    @RequestMapping("/itemList")
    public ModelAndView itemList() throws Exception {
    List<Items>itemList = new ArrayList<>();
    //商品列表
    Items items_1 = new Items();
    items_1.setName("联想笔记本_3");
    items_1.setPrice(6000f);
    items_1.setDetail("ThinkPad T430 联想笔记本电脑!");
    Items items_2 = new Items();
    items_2.setName("苹果手机");
    items_2.setPrice(5000f);
    items_2.setDetail("iphone6苹果手机!");
    itemList.add(items_1);
    itemList.add(items_2);
    //创建modelandView对象
    ModelAndView modelAndView = new ModelAndView();
    //添加model
    modelAndView.addObject("itemList", itemList);
    //添加视图
    modelAndView.setViewName("/WEB-INF/jsp/itemList.jsp");
    // modelAndView.setViewName("itemsList");
    returnmodelAndView;
    }
    }
  5. 创建springmvc.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
    http://code.alibabatech.com/schema/dubbo
    http://code.alibabatech.com/schema/dubbo/dubbo.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    <!--配置controller注解扫描-->
    <context:component-scan base-package="cn.itheima.controller"/>
    </beans>
  6. 配置前端控制器:在web.xml中添加DispatcherServlet的配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!--springmvc前端控制器-->
    <servlet>
    <servlet-name>springMvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--指定配置文件-->
    <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath*:springmvc.xml</param-value>
    </init-param>
    <!--启动加载-->
    <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
    <servlet-name>springMvc</servlet-name>
    <url-pattern>*.action</url-pattern>
    </servlet-mapping>

3、Springmvc架构

1、框架结构

Alt text

2、架构流程

  1. 用户发送请求至前端控制器DispatcherServlet

  2. DispatcherServlet收到请求调用HandlerMapping(处理器映射器)

  3. 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器一并返回给DispatcherServlet

  4. DispatcherServlet通过HandlerAdapter(处理器适配器)调用处理器

  5. 执行处理器(Controller,也叫后端控制器)

  6. Controller执行完成返回ModelAndView

  7. HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet

  8. DispatcherServlet将ModelAndView传给ViewResolver(视图解析器)

  9. ViewResolver解析后返回具体View

  10. DispatcherServlet对View进行渲染视图(也就是讲模型数据填充至视图中)

  11. DispatcherServlet相应用户

3、组件说明

  • DispatcherServlet:前段控制器,用户请求到达前端控制器,它就相当于mvc模式中的c,dispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,dispatcherServlet的存在降低了组件之间的耦合性。

  • HandlerMapping:处理器映射器,HandlerMapping负责根据用户请求找到Handler即处理器,springmvc提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。

  • Handler:处理器,Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。由于Handler涉及到具体的用户业务请求,所以一般情况需要程序员根据业务需求开发Handler。

  • HandlAdapter:处理器适配器,通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。

  • View Resolver:视图解析器,View Resolver负责将处理结果生成View视图,View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。

  • View:视图,springmvc框架提供了很多的View视图类型的支持,包括:jstlView、freemarkerView、pdfView等。我们最常用的视图就是jsp。一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由程序员根据业务需求开发具体的页面。

    在springmvc的各个组件中,处理器映射器、处理器适配器、视图解析器称为springmvc的三大组件。

    需要用户开放的组件有handler、view

4、框架默认加载组件:DispatcherServelet.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

5、注解映射器和适配器

  1. 组件扫描器:使用组件扫描器省去在Spring容器中配置每个controller类的繁琐。使用自动标记@controller的控制器类,多个包中间使用半角逗号分隔

    1
    2
    <!--配置controller注解扫描-->
    <context:component-scan base-package="cn.itheima.controller"/>
  2. RequestMappingHandlerMapping

    • 注解式处理器映射器,对类中标记@ResquestMapping的方法进行映射,根据ResquestMapping定义的url匹配ResquestMapping标记的方法,匹配成功返回HandlerMethod对象给前端控制器,HandlerMethod对象中封装url对应的方法Method。从spring3.1版本开始,废除了DefaultAnnotationHandlerMapping的使用,推荐使用RequestMappingHandlerMapping完成注解式处理器映射。

      1
      2
      3
      4
      5
      6
      7
      <!--如果没有显示的配置处理器映射器和处理器适配器,那么SpringMVC会去默认
      的dispatcherServlet.properties中查找,对应的处理器映射器和处理器适配器来使用。
      导致每个请求都要扫描一次他的默认文件,效率很低,降低访问速度,需要显示的配置
      处理器映射器和处理器适配器
      -->
      <!--注解形式的处理器映射器-->
      <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
  3. RequestMappingHandlerAdapter

    • 注解式处理器适配器,对标记@ResquestMapping的方法进行适配。从spring3.1版本开始,废除了AnnotationMethodHandlerAdapter的使用,推荐使用RequestMappingHandlerAdapter完成注解式处理器适配。

      1
      2
      <!--注解形式的处理器适配器-->
      <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>
  4. <mvc:annotation-driven>

    • springmvc使用自动加载RequestMappingHandlerMapping和RequestMappingHandlerAdapter,可用在springmvc.xml配置文件中使用替代注解处理器和适配器的配置。

      1
      2
      <!--//注解驱动:自动配置最新版的处理器映射器和处理器适配器-->
      <mvc:annotation-driven/>

6、视图解析器

  • InternalResourceViewResolver:支持JSP视图解析
  • viewClass:JstlView表示JSP模板页面需要使用JSTL标签库,所以classpath中必须包含jstl的相关jar 包。此属性可以不设置,默认为JstlView。
  • prefix 和suffix:查找视图页面的前缀和后缀,最终视图的址为:前缀+逻辑视图名+后缀,逻辑视图名需要在controller中返回ModelAndView指定,比如逻辑视图名为hello,则最终返回的jsp视图地址 “WEB-INF/jsp/hello.jsp”
1
2
3
4
5
6
7
8
<!--配置视图解析器,在controller中指定路径名称时不用谢完整的扩展名名称,只写名称就可以-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--真正的页面路径 = 前缀 + 去掉后缀名的页面名称 + 后缀-->
<!--前缀-->
<property name="prefix" value="/WEB-INF/jsp/"/>
<!--后缀-->
<property name="suffix" value=".jsp"/>
</bean>

4、Springmvc整合mybatis

控制层采用Springmvc,持久层使用mybatis。

1、需求

实现商品查询列表,从MySQL数据库查询商品信息。

2、jar包

Alt text

3、工程搭建

1、整合思路

DAO层:

  1. SqlMapConfig.xml,空文件,需要头文件。

  2. application.xml中配置:

    • 数据库连接池

    • SqlSessionFactory对象,需要Spring和mybatis整合包下的

    • 配置mapper文件扫描器

Service层:

  1. applicationContext-service.xml包扫描器,扫描@Service注解的类

  2. applicationContext-trans.xml配置事务

表现层:

  1. springmvc.xml:

    • 包扫描器,扫描@Controller注解的类

    • 配置注解驱动

    • 视图解析器

  2. Web.xml:

    • 配置前端控制器

2、sqlMapConfig.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
</configuration>

3、applicationContext-dao.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 加载配置文件 -->
<context:property-placeholder location="classpath:db.properties" />
<!-- 数据库连接池 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="maxActive" value="10" />
<property name="maxIdle" value="5" />
</bean>
<!-- mapper配置 -->
<!-- 让spring管理sqlsessionfactory 使用mybatis和spring整合包中的 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 数据库连接池 -->
<property name="dataSource" ref="dataSource" />
<!-- 加载mybatis的全局配置文件 -->
<property name="configLocation" value="classpath:SqlMapConfig.xml" />
</bean>
<!-- 配置Mapper扫描器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="cn.itheima.dao"/>
</bean>
</beans>

db.properties:

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/springmvc?characterEncoding=utf-8
jdbc.username=root
jdbc.password=linux

4、applicationContext-service.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!--@Serviece扫描-->
<context:component-scan base-package="cn.itheima.service"/>
</beans>

5、applicationContext-transaction.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--传播行为-->
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
<!--切面-->
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* cn.itheima.service.*.*(..))"/>
</aop:config>
</beans>

6、springmvc.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!--配置注解扫描-->
<context:component-scan base-package="cn.itheima.controller"/>
<!--配置注解驱动-->
<!--<mvc:annotation-driven/>-->
<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--前缀-->
<property name="prefix" value="/WEB-INF/jsp/"/>
<!--后缀-->
<property name="suffix" value=".jsp"/>
</bean>
</beans>

7、web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!--解决post乱码问题-->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--SpringMVC的前端控制器-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:SpringMvc.xml</param-value>
</init-param>
<!--在Tomcat启动的时候就加载这个Servlet-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
<!--配置Spring监听器-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext-*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>

4、Dao

通过mybatis逆向工程生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public interface ItemsMapper {
int countByExample(ItemsExample example);
int deleteByExample(ItemsExample example);
int deleteByPrimaryKey(Integer id);
int insert(Items record);
int insertSelective(Items record);
List<Items> selectByExampleWithBLOBs(ItemsExample example);
List<Items> selectByExample(ItemsExample example);
Items selectByPrimaryKey(Integer id);
int updateByExampleSelective(@Param("record") Items record, @Param("example") ItemsExample example);
int updateByExampleWithBLOBs(@Param("record") Items record, @Param("example") ItemsExample example);
int updateByExample(@Param("record") Items record, @Param("example") ItemsExample example);
int updateByPrimaryKeySelective(Items record);
int updateByPrimaryKeyWithBLOBs(Items record);
int updateByPrimaryKey(Items record);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface UserMapper {
int countByExample(UserExample example);
int deleteByExample(UserExample example);
int deleteByPrimaryKey(Integer id);
int insert(User record);
int insertSelective(User record);
List<User> selectByExample(UserExample example);
User selectByPrimaryKey(Integer id);
int updateByExampleSelective(@Param("record") User record, @Param("example") UserExample example);
int updateByExample(@Param("record") User record, @Param("example") UserExample example);
int updateByPrimaryKeySelective(User record);
int updateByPrimaryKey(User record);
}

5、Service

Service由Spring管理,Spring对service进行事务控制。

1、ItemService接口

1
2
3
public interface ItemsService {
public List<Items> list() throws Exception;
}

2、ItemsServiceImplement实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class ItemsServiceImpl implements ItemsService{
@Autowired
private ItemsMapper itemsMapper;
@Override
public List<Items> list() throws Exception {
ItemsExample example = new ItemsExample();
List<Items> list = itemsMapper.selectByExampleWithBLOBs(example);
return list;
}
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
//窄化请求映射
@RequestMapping("/items")
public class ItemsController {
@Autowired
private ItemsService itemsService;
@RequestMapping("/list")
public ModelAndView itemsList() throws Exception {
List<Items> list = itemsService.list();
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("itemList", list);
modelAndView.setViewName("itemList");
return modelAndView;
}
}

访问http://localhost:8080//items/list.action进行测试。

5、参数绑定

1、简单数据类型

需求:打开商品编辑页面,展示商品信息。

  • 编辑商品信息,需要根据id查询商品信息,然后展示到页面,

  • 请求:/itemsEdit.action,

  • 参数:id,

  • 相应结果:商品编辑页面,展示商品详细信息

1、service

1
2
3
4
@Override
public Items findItemsById(Integer id) throws Exception {
return itemsMapper.selectByPrimaryKey(id);
}

2、Controller参数绑定

根据id查询商品数据,需要从请求的参数中把请求的id提取出来。Id应该包含在Request对象中。可以从Request中取id。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RequestMapping("/itemEdit")
public ModelAnd itemEdit(HttpServletRequest reuqest, Model model) throws Exception{
//从Request中取id
String strId = request.getParameter("id");
Integer id = null;
//如果id有值则转换成int类型
if (strId != null&& !"".equals(strId)) {
id = newInteger(strId);
} else {
//出错
returnnull;
}
Items items = itemService.getItemById(id);
//创建ModelAndView
ModelAndView modelAndView = new ModelAndView();
//向jsp传递数据
modelAndView.addObject("item", items);
//设置跳转的jsp页面
modelAndView.setViewName("editItem");
returnmodelAndView;
}

3、Springmvc默认支持的类型

处理器形参中添加如下类型的参数,处理适配器会默认识别并进行赋值。

  • HttpServletRequest:通过request对象获取请求信息

  • HttpServletResponse: 通过response处理相应信息

  • HttpSession:通过Session对象得到session中存放的对象

  • Model/ModelMap:ModelMap是Model接口的实现类,通过Model或ModelMap向页面传递数据:

    1
    2
    3
    //调用service查询商品信息
    Items item = itemService.findItemById(id);
    model.addAttribute("item", item);
    • 页面通过${item.xxx}获取Model中的属性。实际上使用Model和ModelMap效果一样,使用Model,springmvc会实例化ModelMap。

    • 如果使用Model则可以不使用ModelAndView对象,Model对象可以向页面传递数据,View对象则可以使用String返回值替代。不管是Model还是ModelAndView,其本质都是使用Request对象向jsp传递数据。

    • 使用ModelAndView则方法可以变为:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @RequestMapping("/itemEdit")
      public String itemEdit(HttpServletRequest reuqest, Model model) throws Exception{
      String idStr = reuqest.getParameter("id");
      Items items = itemsService.findItemsById(Integer.parseInt(idStr));
      model.addAttribute("item", items);
      //如果springmvc方法返回一个简单的字符串,nameSpringmvc就会认为这个字符串是页面的名称
      return "editItem";
      }

4、绑定简单类型

当请求的参数名称和处理器形参 名称一致 时,会将请求参数与形参进行绑定。从request取参数的方法可以进一步简化:

1
2
3
4
5
6
7
8
@RequestMapping("/itemEdit")
public String itemEdit(Integer id, Model model) {
Items items = itemService.getItemById(id);
//向jsp传递数据
model.addAttribute("item", items);
//设置跳转的jsp页面
return"editItem";
}
  1. 支持的数据类型:参数类型推荐使用包装数据类型,因为基础数据类型不可以为null

    • Integer,int

    • Float Float

    • Double Double

    • Boolean Boolean :对于Boolean类型的参数,请求的参数值为true或者false。

  2. @RequestParam :使用@RequestParam用于处理简单类型的绑定

    • value:参数名字,即入参的请求参数名字,如value=“item_id”表示请求的参数区中的名字为item_id的参数的值将传入;

    • required:是否必须,默认是true,表示请求中一定要有相应的参数,否则将报:TTP Status 400 - Required Integer parameter ‘XXXX’ is not present

    • defaultValue:默认值,表示如果请求中没有同名参数时的默认值

      1
      public String editItem(@RequestParam(value="item_id",required=true) String id) {}

      形参名称为id,这里使用value=”item_id”限定请求的参数名为item_id,所以页面出传递的参数名必须为item_id。如果请求参数中没有该参数会报500错误。同时通过require=true限定item_id参数Wie必穿阐述,如果不传递则会报400错误。可以使用defaultvalue设置默认值。

3、绑定pojo类型

1、需求

将页面修改后的商品信息保存到数据库中。

  • 请求的url:/updateitem.action结尾的

  • 参数:表单中的数据

  • 相应内容:更新成功页面

2、使用pojo接收表单数据

如果提交的参数很多,或者提交的表单的内容很多的时候可以使用pojo接收数据。要求pojo对象中的属性名和表单input的name属性一致。

页面定义如下:

1
2
3
4
5
6
7
8
<tr>
<td>商品名称</td>
<td><input type="text" name="name" value="${item.name }" /></td>
</tr>
<tr>
<td>商品价格</td>
<td><input type="text" name="price" value="${item.price }" /></td>
</tr>

Pojo定义

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Items {
private Integer id;
private String name;
private Float price;
private String pic;
private Date createtime;
private String detail;
}

Controller:

1
2
3
4
5
6
7
8
9
//可以直接绑定pojo对象,pojo属性名称和页面传过来的参数名称需要一致,
// 按照名称来进行参数绑定
@RequestMapping("/updateitem")
public String update(Items items) throws Exception {
itemsService.updateItem(items);
return "success";
}

此时提交的表单中不能有日期类型的数据,否则会报400错误。需要自定义参数绑定才可以提交日期类型的数据。

3、解决post提交的乱码问题:在web.xml中加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--解决post乱码问题-->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

而对于get请求中文参数的乱码问题解决方案有两个:

  1. 修改Tomcat配置文件添加编码与工程编码一致:<Connector URIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>

  2. 对参数重新编码:String userName new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8")

4、pojo包装类型

  • 需求

使用包装的pojo接收商品信息的查询条件。

  • 包装对象定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
public class QueryVo {
// 商品对象
private Items items;
public Items getItems() {
return items;
}
public void setItems(Items items) {
this.items = items;
}
}
  • 页面定义:
1
2
3
4
5
6
<tr>
<%--如果Controller中接收的是VO,name页面上的属性值要等于VO的属性.属性。属性--%>
<td>商品名称:<input type="text" name="items.name"/></td>
<td>商品价格:<input type="text" name="items.price"/></td>
<td><input type="submit" value="查询"/></td>
</tr>
  • Controller:
1
2
3
4
5
@RequestMapping("/search")
public String search(QueryVo queryVo) throws Exception {
System.out.println(queryVo);
return "success";
}

5、自定义参数绑定

  • 需求

在商品修改页面可以修改商品的日期,并且根据业务需求自定义日期格式。

由于日期数据有多种格式,所以springmvc没办法把字符串转换成日期类型。所以需要自定义参数绑定。前端控制器接收到请求后,找到注解形式的处理器适配器,对RequestMapping标记的方法进行适配,并对方法中的形参进行参数绑定。在springmvc这可以在处理器适配器上自定义Converter进行参数绑定。如果使用可以在此标签上进行扩展。

  • 自定义Convert

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class CustomGlobalStrToDataConvert implements Converter<String, Date>{
    @Override
    public Date convert(String s) {
    try {
    Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(s);
    return date;
    } catch (ParseException e) {
    e.printStackTrace();
    }
    return null;
    }
    }
  • 配置Convert:此方法需要独立配置处理器映射器,处理器适配器,不能使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <!--配置自定义转换器-->
    <mvc:annotation-driven conversion-service="conversionService"/>
    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="converters">
    <set>
    <bean class="cn.itheima.controller.convert.CustomGlobalStrToDataConvert"/>
    </set>
    </property>
    </bean>
    <!-- 自定义webBinder -->
    <beanid="customBinder" class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
    <propertyname="conversionService"ref="conversionService"/>
    </bean>
    <!--注解适配器 -->
    <beanclass="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <propertyname="webBindingInitializer"ref="customBinder"></property>
    </bean>
    <!-- 注解处理器映射器 -->
    <beanclass="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>

6、Springmvc和Struts2的区别

  1. springmvc的入口是一个servlet,也就是前端控制器,Struts2入口是一个filter过滤器。

  2. springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或者多例。Struts2是基于类开发的,传递参数是通过类的属性,只能设计为多例。

  3. Struts2采用值栈存储请求和相应的数据,通过OGNL表达式存取数据。springmvc通过参数解析器是将request中请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,通过又将ModelAndView中的模型数据通过request域传输到页面。JSP视图解析器默认使用jstl。

7、高级参数绑定

1、数组类型的参数绑定

1、需求

在商品列表页面选中多个商品,然后删除,此功能要求商品列表页面的每个商品签名有个复选框,选中多个商品后点击删除按钮,将商品id传递给Controller,根据id删除商品信息。

2、jsp页面修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<c:forEach items="${itemList }" var="item" varStatus="status">
<tr>
<td>
<input type="checkbox" name="ids" value="${item.id }"/>
</td>
<td><input type="text" name="item.name" value="${item.name }"/></td>
<td><input type="text" name="item.price" value="${item.price }"/></td>
<td><input type="text" name="item.createtime"
value="<fmt:formatDate value="${item.createtime}" pattern="yyyy-MM-dd HH:mm:ss"/>"/></td>
<td><input type="text" name="item.detail" value="${item.detail }"/></td>
<td><a href="${pageContext.request.contextPath }/items/itemEdit/${item.id}">修改</a></td>
</tr>
</c:forEach>

3、Controller

Controller方法可以用String[]接收,或者pojo的String[]属性接收。

1
2
3
4
5
6
@RequestMapping("/delAll")
public String delAll(QueryVo vo) throws Exception {
// 如果批量删除,一堆input框,name可以提交数组,选中的会自动进入到数组中
System.out.println(vo);
return "";
}

Vo:

1
2
3
4
5
6
7
8
public class QueryVo {
// 商品对象
private Items items;
//批量删除使用
private Integer[] ids;
//批量修改使用
private List<Items> itemsList;
}

2、List类型的参数绑定

1、需求

实现商品数据的批量修改。要想实现商品数据的批量修改,需要在商品列表中可以对商品信息进行修改,并且可以批量提交修改后的商品数据。

2、pojo类:List中存放对象,并将定义的List放入包装类中,使用包装类pojo对象接收。

1
2
3
4
5
6
7
8
public class QueryVo {
// 商品对象
private Items items;
//批量删除使用
private Integer[] ids;
//批量修改使用
private List<Items> itemsList;
}

3、jsp修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<c:forEach items="${itemList }" var="item" varStatus="status">
<tr>
<!-- name属性名称要等于vo中的接收的属性名 -->
<!-- 如果批量删除,可以用List<pojo>来接收,页面上input框的name属性值= vo中接收的集合属性名称+[list的下标]+.+list泛型的属性名称 -->
<td>
<input type="checkbox" name="ids" value="${item.id }"/>
<input type="hidden" name="itemsList[${status.index }].id" value="${item.id }"/>
</td>
<td><input type="text" name="itemsList[${status.index }].name" value="${item.name }"/></td>
<td><input type="text" name="itemsList[${status.index }].price" value="${item.price }"/></td>
<td><input type="text" name="itemsList[${status.index }].createtime"
value="<fmt:formatDate value="${item.createtime}" pattern="yyyy-MM-dd HH:mm:ss"/>"/></td>
<td><input type="text" name="itemsList[${status.index }].detail" value="${item.detail }"/></td>
<td><a href="${pageContext.request.contextPath }/items/itemEdit/${item.id}">修改</a></td>
</tr>
</c:forEach>

其中name属性必须是包装pojo的list属性+下表+元素属性。

  • varStatus属性常用参数总结:

    • ${status.index} 输出行号,从0开始。
    • ${status.count} 输出行号,从1开始。
    • ${status.current} 当前这次迭代的(集合中的)项
    • ${status.first} 判断当前项是否为集合中的第一项,返回值为true或false
    • ${status.last} 判断当前项是否为集合中的最后一项,返回值为true或false
    • begin、end、step分别表示:起始序号,结束序号,跳跃步伐。

4、Controller

1
2
3
4
5
@RequestMapping("/updateAll")
public String updateAll(QueryVo vo) throws Exception {
System.out.println(vo);
return "";
}

接收List类型的数据必须是pojo的属性,方法的形参为List类型无法正确接收到数据

8、@RequestMapping注解的使用

通过@RequestMapping注解可以定义不同的处理器映射规则。

  • @RequestMapping(value="/item")或者@RequestMapping("/item"), value的值是数组,可以将多个url映射到一个方法中。

  • 窄化请求映射,在class上添加@RequestMapping(url)指定通用请求前缀,限制此类下的所有方法请求url必须以请求前缀开头,通过此方法对url进行分类管理。例如:在类名上添加@RequestMapping("/item"),在方法名上添加@RequestMapping("/list"),则访问地址为:/item/list

  • 方法请求限定:

    • 限定Get方法:@RequestMapping(method = RequestMethod.GET) 如果通过post请求会报错:HTTP Status 405 - Request method 'POST' not supported

    • 限定POST方法:@RequestMapping(method = RequestMethod.POST)如果通过get请求则报错:HTTP Status 405 - Request method 'GET' not supported

    • GET和POST都可以:@RequestMapping(method={RequestMethod.GET,RequestMethod.POST})

9、Controller方法的返回值

  1. 返回ModelAndView:controller方法中定义ModelAndView对象并返回,对象中可添加model数据,指定view。

  2. 返回Void:在controller方法形参上可以定义request和response,使用request或者response指定响应结果:

    • 使用request转向页面:request.getRequestDispatcher("/WEB-INF/jsp/success.jsp").forward(request, response);

    • 使用response重定向:response.sendRedirect("url")

    • 也可以通过response指定响应结果:

      1
      2
      3
      response.setCharacterEncoding("utf-8");
      response.setContentType("application/json;charset=utf-8");
      response.getWriter().write("json串");
  3. 返回字符串

    • 逻辑视图名

      • controller方法返回字符串可以指定逻辑视图名,通过视图解析器解析为物理视图地址。

        1
        2
        //指定逻辑视图名,经过视图解析器解析为jsp物理路径:/WEB-INF/jsp/item/editItem.jsp
        return "item/editItem";
    • Redirect重定向

      • Controller方法返回结果重定向到一个url地址吗,如下商品修改提交后重定向到商品查询方法,参数无法带到商品查询方法中

        1
        2
        //重定向到queryItem.action地址,request无法带过去
        return "redirect:queryItem.action";
      • redirect方式相当于response.sendRedirect(),转发后浏览器的地址栏变为转发后的地址,因为转发执行了一个新的request和response。由于发起了一个request原来的参数在转发时就不能传递到下一个url,如果要传入参数可以在url中添加

    • forward转发

      • controller方法执行后继续执行另一个controller方法,如下商品修改提交后转向到商品修改页面,修改商品的id参数可以带到商品修改方法中去。

        1
        2
        //结果转发到editItem.action,request可以带过去
        return "forward:editItem.action";
      • forward方式相当于request.getRequestDispatcher().forward(request,response),转发后浏览器地址栏还是原来的地址,转发并没有执行新的request和response,而是和转发前的请求公用一个request和response。所以转发请求的参数在转发后仍然可以读取到。

10、Springmvc中的异常处理

springmvc在处理请求过程中出现异常信息交由异常处理器进行处理,自定义异常处理器可以实现一个系统的异常处理逻辑。

1、异常处理思路

  • 系统中异常分为两类:

    • 预期异常,通过捕获异常从而获取异常

    • 运行时异常,规范代码开发

  • 系统的dao、service、controller出现都通过throws Exception向上抛出,最后由springmvc的前端控制器交由异常处理器进行异常处理

Alt text

2、自定义异常类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CustomException extends Exception {
/** serialVersionUID*/
private static final long serialVersionUID = -5212079010855161498L;
public CustomException(String message){
super(message);
this.message = message;
}
//异常信息
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

3、自定义异常处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CustomExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
ex.printStackTrace();
CustomException customException = null;
//如果抛出的是系统自定义异常则直接转换
if(ex instanceof CustomException){
customException = (CustomException)ex;
}else{
//如果抛出的不是系统自定义异常则重新构造一个系统错误异常。
customException = new CustomException("系统错误,请与系统管理 员联系!");
}
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("message", customException.getMessage());
modelAndView.setViewName("error");
return modelAndView;
}
}

4、配置异常处理器

1
2
<!-- 异常处理器 -->
<bean id="handlerExceptionResolver" class="cn.itcast.ssm.controller.exceptionResolver.CustomExceptionResolver"/>

11、 图片上传处理

1、Tomcat上配置图片虚拟目录

Alt text

2、jar包

CommonsMultipartResolver解析器依赖commons-fileupload和commons-io

3、配置解析器

1
2
3
4
5
6
7
<!--配置文件上传-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--设置上传文件的最大尺寸为5M-->
<property name="maxUploadSize">
<value>5242880</value>
</property>
</bean>

4、图片上传实现

  • Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @RequestMapping("/updateitem")
    public String update(MultipartFile pictureFile, Items items) throws Exception {
    //1获取图片完整名称
    String fileStr = pictureFile.getOriginalFilename();
    //2使用随机生成的字符串+原图片扩展名,放置图片重名
    String newFileStr = UUID.randomUUID().toString() + fileStr.substring(fileStr.lastIndexOf("."));
    //3将图片保存到硬盘
    pictureFile.transferTo(new File("E:\\pic\\" + newFileStr));
    //4将图片名称保存到数据库
    items.setPic(newFileStr);
    itemsService.updateItem(items);
    return "redirect:itemEdit/" + items.getId();
    }
  • 页面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <form id="itemForm" action="${pageContext.request.contextPath }/items/updateitem" method="post" enctype="multipart/form-data">
    ...
    <tr>
    <td>商品图片</td>
    <td>
    <c:if test="${item.pic !=null}">
    <img src="/pic/${item.pic}" width=100 height=100/>
    <br/>
    </c:if>
    <input type="file" name="pictureFile"/>
    </td>
    </tr>
    ...
    </form>

12、Json数据交互

1、@RequestBody

  • @RequestBody注解用于读取http请求的内容,通过springmvc提供的HttpMessageConvert接口架构读到的内容转换为json,xml等格式的数据并绑定到controller方法的参数上。

2、@ResponseBody

  • 该注解用于将Controller的方法返回的对象,通过HttpMessageConverter接口转换为指定格式的数据如:json,xml等,通过Response响应给客户端

3、请求json,相应json的实现

  1. 环境准备:springmvc默认用MappingJacksonHttpMesseageConvert对json数据进行转换,需要加入Jackson的包:

    • jackson-core-asl-1.9.11.jar

    • jackson-mapper-asl-1.9.11.jar

  2. 配置json转换器:在注解适配器中加入messageConvert

    1
    2
    3
    4
    5
    6
    7
    8
    <!--注解适配器 -->
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="messageConverters">
    <list>
    <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"></bean>
    </list>
    </property>
    </bean>

    如果使用了 则不用定义上边的内容。

  3. Controller编写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //导入jsckson的jar包在controller的方法中可以使用@RequestBody,让springmvc将
    // json格式字符串自动转换成java中的pojo对象,要求json的key的值等于pojo中的属性
    @RequestMapping("/sendJson")
    //controller方法返回pojo类型的对象,添加@ResponseBody注解,
    // springmvc会自动将对象转换成json格式字符串并传递给页面
    @ResponseBody
    public Items json(@RequestBody Items items) throws Exception {
    System.out.println(items);
    return items;
    }
  4. 页面js方法的编写

    • 引入js:

    • ajax请求:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      <script type="text/javascript">
      function sendJson(){
      //请求json响应json
      $.ajax({
      type:"post",
      url:"${pageContext.request.contextPath }/items/sendJson.action",
      contentType:"application/json;charset=utf-8",
      data:'{"name":"测试商品","price":99.9}',
      success:function(data){
      alert(data);
      }
      });
      }
      </script>

13、Springmvc实现Restful

1、RESTful定义

  • Restful就是一个资源定位及资源操作的风格。不是标准也不是协议,只是一种风格,是对http协议的诠释。

  • 资源定位:互联网所有的事物都是资源,要求url中没有动词,只有名词。没有参数:Url格式:http://blog.csdn.net/beat_the_world/article/details/45621673

  • 资源操作:使用put、delete、post、get,使用不同方法对资源进行操作。分别对应添加、删除、修改、查询。一般使用时还是post和get。Put和Delete几乎不使用。

2、需求

RESTful方式实现商品信息查询,返回json数据。

3、添加DispatcherServlet的rest配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--SpringMVC的前端控制器-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:SpringMvc.xml</param-value>
</init-param>
<!--在Tomcat启动的时候就加载这个Servlet-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<!--
<url-pattern>*.action</url-pattern>
*.action 拦截后缀名为.action结尾的
/ 拦截所有到那时不包括.jsp
/* 拦截所有,包括.jsp
-->
<url-pattern>/</url-pattern>
</servlet-mapping>

4、URL模板模式映射

  • @RequestMapping(value="/ viewItems/{id}"):{×××}占位符,请求的URL可以是“/viewItems/1”或“/viewItems/2”,通过在方法中使用@PathVariable获取{×××}中的×××变量。

  • @PathVariable用于将请求URL中的模板变量映射到功能处理方法的参数上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RequestMapping("/itemEdit/{id}")
    public String itemEdit(@PathVariable("id") Integer id, HttpServletRequest reuqest, Model model) throws Exception{
    // String idStr = reuqest.getParameter("id");
    Items items = itemsService.findItemsById(id);
    // if (items == null) {
    // throw new CustomException("商品信息不存在!");
    // }
    model.addAttribute("item", items);
    //如果springmvc方法返回一个简单的字符串,nameSpringmvc就会认为这个字符串是页面的名称
    return "editItem";
    }
    • 如果RequestMapping中表示为”/itemEdit/{id}”,id和形参名称一致,使用@PathVariable不用指定名称。

5、静态资源访问

  • 如果在DispatcherServlet中设置url-pattern为 /则必须对静态资源进行访问处理。spring mvc 的实现对静态资源进行映射访问。

  • 如下是对js文件访问配置:<mvc:resources location="/js/" mapping="/js/**"/>

14、拦截器

1、定义

  • Spring Web MVC 的处理器拦截器类似于Servlet 开发中的过滤器Filter,用于对处理器进行预处理和后处理。

2、拦截器定义

  • 实现HandlerInterceptor接口,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    public class Inteceptor1 implements HandlerInterceptor{
    /**
    * 执行时间:controller方法没有被执行, modelAndView没有被返回
    * 使用场景:权限验证
    * @param httpServletRequest
    * @param httpServletResponse
    * @param o
    * @return true:放行, false:拦截
    * @throws Exception
    */
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
    System.out.println("==========Inteceptor1===========pre===========");
    return true;
    }
    /**
    * 执行时机:controller方法已经执行,modelAndView没有返回
    * 使用场景:可以在此方法中设置全局的数据处理业务,
    * @param httpServletRequest
    * @param httpServletResponse
    * @param o
    * @param modelAndView
    * @throws Exception
    */
    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    System.out.println("==========Inteceptor1===========post===========");
    }
    /**
    * 执行时机:controller方法已经执行,modelAndView已经返回
    * 使用场景:记录操作日志,记录登录用户的IP,时间等
    * @param httpServletRequest
    * @param httpServletResponse
    * @param o
    * @param e
    * @throws Exception
    */
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    System.out.println("==========Inteceptor1===========after===========");
    }
    }

3、拦截器配置

1、针对某种mapping配置拦截器

1
2
3
4
5
6
7
8
9
10
11
<bean
class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="handlerInterceptor1"/>
<ref bean="handlerInterceptor2"/>
</list>
</property>
</bean>
<bean id="handlerInterceptor1" class="springmvc.intercapter.HandlerInterceptor1"/>
<bean id="handlerInterceptor2" class="springmvc.intercapter.HandlerInterceptor2"/>

2、针对所有mapper配置全局拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--配置拦截器-->
<mvc:interceptors>
<!--多个拦截器的执行顺序,等于其配置顺序-->
<mvc:interceptor>
<!--&lt;!&ndash;配置拦截请求的路径 /**代表所有&ndash;&gt;-->
<mvc:mapping path="/**"/>
<!--&lt;!&ndash;指定拦截器的位置&ndash;&gt;-->
<bean class="cn.itheima.interceptor.Inteceptor1"/>
</mvc:interceptor>
<mvc:interceptor>
<!--&lt;!&ndash;配置拦截请求的路径 /**代表所有&ndash;&gt;-->
<mvc:mapping path="/**"/>
<!--&lt;!&ndash;指定拦截器的位置&ndash;&gt;-->
<bean class="cn.itheima.interceptor.Inteceptor2"/>
</mvc:interceptor>
</mvc:interceptors>

4、测试流程

  • 定义两个拦截器分别为HandlerInterceptor1和HandlerInterceptor2,每个拦截器的preHandler方法都返回true:

    1
    2
    3
    4
    5
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
    System.out.println("==========Inteceptor1===========pre===========");
    return true;
    }
  • 运行流程:

    • HandlerInterceptor1..preHandle..
    • HandlerInterceptor2..preHandle..
    • HandlerInterceptor2..postHandle..
    • HandlerInterceptor1..postHandle..
    • HandlerInterceptor2..afterCompletion..
    • HandlerInterceptor1..afterCompletion..

5、终端流程测试

  • 定义两个拦截器分别为HandlerInterceptor1和HandlerInterceptor2

  • 运行流程:

    • preHandler按拦截器定义顺序调用

    • postHandler按拦截器定义逆序调用

    • afterCompletion按拦截器定义逆序调用

    • postHandler在拦截器链内所有拦截器返回成功调用

    • afterCompletion只有preHandler返回true才调用

6、拦截器应用

1、处理流程

  1. 有一个登录页面,需要写一个controler访问页面

  2. 登录页面有一项提交表单的动作,需要在controller中处理。

    • 判断用户名是否正确

    • 如果正确,向session中写入用户信息

    • 返回登录成功,或者跳转到登录页面

  3. 拦截器

    • 拦截用户请求,判断用户是否登录

    • 如果已经登录,放行

    • 如果用户未登录,跳转到登录页面

2、用户身份认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package cn.itheima.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object arg2) throws Exception {
//判断当前访问路径是否为登录的路径,如果是则放行
if(request.getRequestURI().indexOf("/login") >= 0){
return true;
}
//判断session中是否有登录信息,如果没有则跳转到登录页面,如果有则放行
HttpSession session = request.getSession();
if(session.getAttribute("username") != null){
return true;
}
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response);
return false;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}

3、用户登录controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Controller
@RequestMapping("/login")
public class LoginController {
//跳转到登录页面
@RequestMapping("/login")
public String login() throws Exception{
return "login";
}
@RequestMapping("/submit")
public String submit(String username, String pwd ,HttpServletRequest request) throws Exception{
HttpSession session = request.getSession();
//判断用户名密码的正确性,如果正确则将登录信息放入session中
//这里简写,真正项目中需要去数据库中校验用户名和密码
if(username != null){
session.setAttribute("username", username);
}
//跳转到列表页
return "redirect:/items/list";
}
}

15、重点总结

  1. springMvc:是一个表现层框架

    • 作用:就是从请求中接收传入的参数,将处理后的结果数据返回给页面展示
  2. ssm整合:

    1. Dao层

      • pojo和映射文件以及接口使用逆向工程生成

      • SqlMapConfig.xml mybatis核心配置文件

      • ApplicationContext-dao.xml 整合后spring在dao层的配置

      • 数据源

      • 会话工厂

      • 扫描Mapper

    2. service层

      • 事务 ApplicationContext-trans.xml

      • @Service注解扫描 ApplicationContext-service.xml

    3. controller层

      • SpringMvc.xml

        • 注解扫描:扫描@Controller注解

        • 注解驱动:替我们显示的配置了最新版的处理器映射器和处理器适配器

        • 视图解析器:显示的配置是为了在controller中不用每个方法都写页面的全路径

    4. web.xml

      • springMvc前端控制器配置

      • spring监听

  3. 参数绑定:(从请求中接收参数)

    1. 默认支持的类型:Request,Response,Session,Model

    2. 基本数据类型(包含String)

    3. Pojo类型

    4. Vo类型

    5. Converter自定义转换器

    6. 数组

    7. List

  4. controller方法返回值(指定返回到哪个页面, 指定返回到页面的数据)

    1. ModelAndView

      • modelAndView.addObject(“itemList”, list); 指定返回页面的数据

      • modelAndView.setViewName(“itemList”); 指定返回的页面

    2. String(推荐使用)

      • 返回普通字符串,就是页面去掉扩展名的名称, 返回给页面数据通过Model来完成

      • 返回的字符串以forward:开头为请求转发

      • 返回的字符串以redirect:开头为重定向

    3. 返回void(使用它破坏了springMvc的结构,所以不建议使用)

      • 可以使用request.setAttribut 来给页面返回数据

      • 可以使用request.getRquestDispatcher().forward()来指定返回的页面

      • 如果controller返回值为void则不走springMvc的组件,所以要写页面的完整路径名称

      • 相对路径:相对于当前目录,也就是在当前类的目录下,这时候可以使用相对路径跳转

      • 绝对路径:从项目名后开始.

      • 在springMvc中不管是forward还是redirect后面凡是以/开头的为绝对路径,不以/开头的为相对路径例如:forward:/items/itemEdit.action 为绝对路径,forward:itemEdit.action为相对路径

  5. 架构级别异常处理:

    • 主要为了防止项目上线后给用户抛500等异常信息,所以需要在架构级别上整体处理.hold住异常首先自定义全局异常处理器实现HandlerExceptionResolver接口,在spirngMvc.xml中配置生效
  6. 上传图片:

    1. 在tomcat中配置虚拟图片服务器

    2. 导入fileupload的jar包

    3. 在springMvc.xml中配置上传组件

    4. 在页面上编写上传域,更改form标签的类型

    5. 在controller方法中可以使用MultiPartFile接口接收上传的图片

    6. 将文件名保存到数据库,将图片保存到磁盘中

  7. Json数据交互:

    • 需要加入jackson的jar包

    • @Requestbody:将页面传到controller中的json格式字符串自动转换成java的pojo对象

    • @ResponseBody:将java中pojo对象自动转换成json格式字符串返回给页面

  8. RestFul支持:

    • 就是对url的命名标准,要求url中只有能名词,没有动词(不严格要求),但是要求url中不能用问号?传参
      传参数:

      • 页面:${pageContext.request.contextPath }/items/itemEdit/${item.id}

      • 方法: @RquestMapping(“/itemEdit/{id}”)

      • 方法: @PathVariable(“id”) Integer idd

  9. 拦截器:

    • 作用:拦截请求,一般做登录权限验证时用的比较多

      1. 需要编写自定义拦截器类,实现HandlerInterceptor接口

      2. 在spirngMvc.xml中配置拦截器生效

  10. 登录权限验证:

    1. 编写登录的controller, 编写跳转到登录页面的方法, 编写登录验证方法

    2. 编写登录页面

    3. 编写拦截器

    • 运行过程:
    1. 访问随意一个页面,拦截器会拦截请求,会验证session中是否有登录信息,如果已登录,放行, 如果未登录,跳转到登录页面

    2. 在登录页面中输入用户名,密码,点击登录按钮,拦截器会拦截请求,如果是登录路径放行,在controller方法中判断用户名密码是否正确,如果正确则将登录信息放入session

Spring Data

发表于 2019-04-21 | 分类于 java , Spring

概念

Spring Data JPA 是 Spring Data 系列的一部分,可以轻松实现基于 JPA 的存储库。 该模块处理对基于 JPA 的数据访问层的增强的支持。 这使得使用数据访问技术构建Spring 的应用程序变得更加容易。

传统方式访问数据库JDBC驱动

创建Maven项目

  • maven工程的目录结构
    |– pom.xml
    |– src
    | |– main
    | | -- java | |– resources
    | | -- filters |– test
    | | -- java | |– resources
    | | -- filters |– it
    | -- assembly |– site
    |– LICENSE.txt
    |– NOTICE.txt
    |– README.txt
  • 添加依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!--MySQL Driver-->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.38</version>
    </dependency>
    <!--junit-->
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.10</version>
    <scope>test</scope>
    </dependency>

数据表准备

  • Create database spring_data;
  • create table student(
    id int not null auto_increment,
    name varchar(20) not null,
    age int not null,
    primary key(id)
    );
  • Insert into student (name, age) values (“zhangsan”,20);
  • Insert into student (name, age) values (“lisi”,21);
  • Insert into student (name, age) values (“wangwu”,22);

    开发JDBCUtil工具类

  • 获取Connection,关闭Connection、Statement、ResultSet
  • 注意事项
    配置的属性放置在配置文件中,通过代码的方式将配置文件中的属性加载进程序

    建立对象模型,DAO

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    package com.imooc.domain;
    /**
    * Student实体类
    */
    public class Student {
    /**
    * 主键字段
    */
    private int id;
    /**
    * 姓名
    */
    private String name;
    /**
    * 年龄
    */
    private int age;
    public int getId() {
    return id;
    }
    public void setId(int id) {
    this.id = id;
    }
    public String getName() {
    return name;
    }
    public void setName(String name) {
    this.name = name;
    }
    public int getAge() {
    return age;
    }
    public void setAge(int age) {
    this.age = age;
    }
    @Override
    public String toString() {
    return "Student{" +
    "id=" + id +
    ", name='" + name + '\'' +
    ", age=" + age +
    '}';
    }
    }

使用Spring JDBC的方式操作数据库

添加依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>

配置beans.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring_data"/>
<property name="username" value="root"/>
<property name="password" value="linux"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="studentDAO" class="com.imooc.dao.StudentDAOSprignJDBCImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>

开发Spring JDBC版本的query和save方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.imooc.dao;
import com.imooc.domain.Student;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowCallbackHandler;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* StudentDAO访问接口实现类:通过SPring JDBC的方式操作
*/
public class StudentDAOSprignJDBCImpl implements StudentDAO{
private JdbcTemplate jdbcTemplate;
public List<Student> query() {
final List<Student> students = new ArrayList<Student>();
String sql = "select id, name, age from student";
jdbcTemplate.query(sql, new RowCallbackHandler() {
public void processRow(ResultSet rs) throws SQLException {
Student student = new Student();
int id = rs.getInt("id");
String name = rs.getString("name");
int age = rs.getInt("age");
student.setId(id);
student.setAge(age);
student.setName(name);
students.add(student);
}
});
return students;
}
public void save(Student student) {
String sql = "insert into student (name, age) values (?, ?)";
jdbcTemplate.update(sql, student.getName(), student.getAge());
}
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}

传统方式访问数据库的缺点

  1. DAO里面代码很多
  2. DAOImpl有很多重复的代码
  3. 开发分页和其他功能需要重新封装

Spring Data JPA快速起步

开发环境搭建

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<dependencies>
<!--MySQL Driver-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
</dependency>
<!--spring-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>
<!--spring data jpa-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.8.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.6.Final</version>
</dependency>
</dependencies>

beans.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!--配置数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring_data"/>
<property name="username" value="root"/>
<property name="password" value="linux"/>
</bean>
<!--配置EntityManagerFactory-->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
</property>
<property name="packagesToScan" value="com.imooc"/>
<property name="jpaProperties">
<props>
<prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
</bean>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<!--支持注解的事务-->
<tx:annotation-driven transaction-manager="transactionManager"/>
<!--配置Spring data-->
<jpa:repositories base-package="com.imooc" entity-manager-factory-ref="entityManagerFactory"/>
<context:component-scan base-package="com.imooc" />
</beans>

Spring data JPA进阶

Repository接口讲解

  1. Repository是Spring Data的核心接口,不提供任何方法
  2. Public interface Repository{ }
  3. @RepositoryDefinition注解的使用
  4. Repository是一个空接口,也叫做标记接口(没有包含方法声明的接口)
  5. 如果我们定义的EmployeeRepository extends Repository,如果我们自己的接口没有继承Repository接口,会报错
  6. 添加注解能够达到不用extends Repository接口 的功能
    1
    2
    3
    4
    @RepositoryDefinition(domainClass = Employee.class, idClass = Integer.class)
    public interface EmployeeRepository{
    public Employee findByName(String name);
    }

Repository子接口讲解

  1. CrudRepository:继承Repository,实现CRUD相关的方法
  2. PagingAndSortingRepositoy:继承CrudRepository,实现了分页排序相关的方法
  3. JPARepository:继承PagingAndSortingRepository,实现JPA规范相关的方法

Repository中查询方法定义规则和使用

  • Alt text
  • Alt text
  • 接口代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public interface EmployeeRepository extends Repository<Employee, Integer>{
    public Employee findByName(String name);
    // where name like ?% and age < ?
    public List<Employee> findByNameStartingWithAndAgeLessThan(String name, Integer age);
    // where name like %? and age < ?
    public List<Employee> findByNameEndingWithAndAgeLessThan(String name, Integer age);
    // where name in (?,?) or age < ?
    public List<Employee> findByNameInOrAgeLessThan(List<String> names, Integer age);
    // where name in (?,?) and age < ?
    public List<Employee> findByNameInAndAgeLessThan(List<String> names, Integer age);
    }
  • 按照方法命名规则来使用的话的弊端:

    1. 方法名比较常:约定大于配置
    2. 对于一些复杂查询,很难实现

4. Query注解的使用

1. 在Repository方法中使用,不需要遵循查询方法命名规则
1
2
@Query("select o from Employee o where id=(select max (id) from Employee t1)")
public Employee getEmployeeByMaxId();
2. 只需要将@Query定义在Repository中的方法之上即可
    3. 支持命名参数和索引参数的使用
1
2
3
4
5
6
@Query("select o from Employee o where o.name=?1 and o.age=?2")
public List<Employee> queryParams1(String name, Integer age);
@Query("select o from Employee o where o.name=:name and o.age=:age")
public List<Employee> queryParams2(@Param("name") String name,
@Param("age") Integer age);
4. 支持本地查询
1
2
@Query(nativeQuery = true, value = "select COUNT(1) from employee")
public long getCount();

5. 更新和删除操作整合事务的使用

  1. `@Modifying`注解使用
  2. `@Modifying`结合@Query注解执行更新操作
  2. `@Transactional`在Spring Data中的使用
  4. 事务在Spring中的使用:
      * 事务一般是在Service层
* @Query、@Modifying、@Transactional的综合使用

Spring Data JPA高级

CrudRepository接口使用详解

PagingAndSortingRepository接口使用详解

  1. 分页和排序
  2. 带排序的查询

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Test
    public void testPage() {
    //page: index是从0开始的
    Pageable pageable = new PageRequest(0,5);
    Page<Employee> page = employeePagingAndSortingRepository.findAll(pageable);
    System.out.println("查询的总页数:" + page.getTotalPages());
    System.out.println("查询的总记录数:" + page.getTotalElements());
    System.out.println("查询当前第几页:" + page.getNumber() + 1);
    System.out.println("查询的当前页面的集合:" + page.getContent());
    System.out.println("查询的当前页面的记录数:" + page.getNumberOfElements());
    }
  3. 带排序的分页查询

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Test
    public void testPageAndSort() {
    Sort.Order order = new Sort.Order(Sort.Direction.DESC, "id");
    Sort sort = new Sort(order);
    //page: index是从0开始的
    Pageable pageable = new PageRequest(0,5, sort);
    Page<Employee> page = employeePagingAndSortingRepository.findAll(pageable);
    System.out.println("查询的总页数:" + page.getTotalPages());
    System.out.println("查询的总记录数:" + page.getTotalElements());
    System.out.println("查询当前第几页:" + page.getNumber() + 1);
    System.out.println("查询的当前页面的集合:" + page.getContent());
    System.out.println("查询的当前页面的记录数:" + page.getNumberOfElements());
    }

JpaRepository接口使用详解

  1. findAll
  2. findAll(Sort sort)
  3. Save(entities)
  4. Flush
  5. deleteInBatche(enties)

    JpaSpecificationExcutor接口使用详解

  • 封装了JPA Criteria查询条件

1_Spring的核心

发表于 2019-04-21 | 分类于 Java , Spring

Spring简介


Spring的根本使命是:简化Java的开发,采用了四种策略:

  • 基于POJO的轻量级和最小浸入性编程,避免了大量的不必要的冗余代码
  • 通过依赖注入和面向接口事项松耦合,每个对象负责管理与自己相互协作的对象的引用会造成高度的耦合性,测试比较复杂,另一种方式是采用依赖注入,对象的依赖关系将由负责协调系统中各个对象的第三方组件在创建对象时设定,对象无需自行创建或者管理器依赖关系,依赖关系会自动的注入到需要的对象中去。
  • 基于切面和惯例进行声明式编程
  • 通过切面和模板减少样式代码,如JDBC查询模板语句

Spring容器

  • Spring应用中,对象存在于Spring容器中,容器创建对象,装配对象,配置对象,管理对象的声明周期。
  • Spring容器可以分为两种类型:
    • Bean工厂:最简单的容器,提供基本的DI支持
    • 应用上下文:基于beanFactory之上构建,提供面向应用的服务,可以从属性文件解析文本信息
      • ClassPathXMLApplicationContext: 从类路径下的XML配置文件中加载上下文定义,把应用上下文定义文件当做类资源
      • FileSystemXmlApplocationcontext:读取文件系统下的XML配置文件并加载上下文定义
      • XmlWebApplocationContext:读取web应用下的XML配置文件并装载上下文定义。
  • Bean的生命周期:
    • Spring对Bean进行实例化
    • Spring将值和Bean的引用注入进bean对应的属性中去
    • 如果Bean实现了BeanNameAware接口,Spring将Bean的ID传递给setBeanName()接口方法
    • 如果Bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()接口方法,将BeanFactory容器实例传入。
    • 如果Bean实现了ApplicationContextAweare接口,Spring将调用他们的setApplicationContext()接口方法,将应用上下文的引用传入
    • 如果Bean实现了BeanPostProcessor接口,Spring将调用他们的postProcessBeforeInitialization()接口方法
    • 如果Bean实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()接口方法。类似的,如果Bean使用init-method声明了初始化方法,该方法也会被调用。
    • 如果Bean实现了BeanPostProcessor接口,Spring将调用他们的postProcessAfterInitialization()接口方法
    • 此时此刻,Bean已经准备就绪,可以被应用程序调用,Bean会一直驻留在应用上下文中,直到该应用上下文被销毁
    • 如果Bean实现了DisposableBea接口,Spring将调用他们的destroy()接口方法。同样如果Bean使用destroy-method声明了销毁方法,该方法也会被调用。

Spring的六大模块

  • 核心Spring容器:负责Bean的创建,配置和管理。BeanFactory提供了依赖注入,应用上下文及其他企业服务。
  • AOP模块:对面向切面编程提供了丰富的支持。
  • 数据访问与集成:JDBC和DAO层模块封装了数据库样板,提供了ORM模块,
  • Web和远程调用
  • 测试

装配Bean


声明Bean

XML命名空间

  • Java自带了许多XML命名空间,通过这些命名空间可以配置Spring

    • aop:为声明切面以及将@AspectJ注解的类代理为Spring切面提供了配置元素
    • beans:支持声明Bean和装配Bean,是Spring最核心也是最原始的命名空间
    • context:为配置Spring应用上下文提供了配置元素,包括自动检测和自动装配bean,注入非Spring直接管理的对象
    • jee:提供了与Java EE API的继承,例如JNDI和EJB
    • jms:为声明消息驱动的POJO提供了配置元素
    • lang:支持配置由Groovy、JRuby或BeanShell等脚本实现的Bean
    • mvc:启动Spring MVC的能力,例如面向注解的控制器、视图控制器和拦截器
    • oxm:支持Spring的对象到XML的映射配置
    • tx:提供声明式事务配置
    • util:提供各种各样的工具类元素,包括把集合配置为Bean,支持属性占位符元素
  • 创建Spring配置:

    1
    2
    3
    4
    5
    6
    7
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
    >
    <!--Bean declarations go here -->
    </beans>

beans 元素中放置所有的Spring配置文件。

声明Bean

定义一个Perfomer接口:

1
2
3
4
5
package com.springinaction.springidol;
public interface Performer {
void perform() throws Exception;
}

新建一个Bean,实现了Performer接口,从而可以进行表演扔袋子的节目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.springinaction.springidol;
public class Juggler implements Performer{
private int beanBags = 3;
public Juggler() {
}
public Juggler(int beanBags) {
this.beanBags = beanBags;
}
@Override
public void perform() throws Exception {
System.out.println("JUGGLING " + beanBags + " BEANBAGS");
}
}

在Spring配置文件中声明一个Bean:

1
<bean id="duke" class="com.springinaction.springidol.Juggler" />

元素是Spring中最基本的配置单元,通过该元素,Spring可以创建一个对象,duke相当于Juggler的一个实例,main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"com/springinaction/springidol/spring-idol.xml"
);
Performer performer = (Performer) ctx.getBean("duke");
try {
performer.perform();
} catch (Exception e) {
e.printStackTrace();
}
}
}

打印结果:JUGGLING 3 BEANBAGS

构造器注入

XML文件中,让duke成为可以抛15个袋子的杂技师:

1
2
3
<bean id="duke" class="com.springinaction.springidol.Juggler" >
<constructor-arg value="15"/>
</bean>

当不配置constructor-arg标签时,Spring将使用默认的构造方法,配置之后会使用另外的构造方法来实例化对象。

  • 构造器注入对象的引用
    Poem接口:
1
2
3
4
5
package com.springinaction.springidol;
public interface Poem {
void recite();
}

注入对象引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.springinaction.springidol;
public class PoeticJuggler extends Juggler{
private Poem poem;
public PoeticJuggler(Poem poem) {
super();
this.poem = poem;
}
public PoeticJuggler(int beanBags, Poem poem) {
super(beanBags);
this.poem = poem;
}
@Override
public void perform() throws Exception {
super.perform();
System.out.println("While reciting...");
poem.recite();
}
}

实现Poem接口的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.springinaction.springidol;
public class Sonnet29 implements Poem{
private static String[] LINES = {
"when I was young",
"I am a boy.....",
"end of the poem"
};
public Sonnet29() {
}
@Override
public void recite() {
for (int i = 0; i < LINES.length; i++)
System.out.println(LINES[i]);
}
}

XML中进行配置:

1
2
3
4
5
<bean id="duke" class="com.springinaction.springidol.PoeticJuggler" >
<constructor-arg value="15"/>
<constructor-arg ref="sonnet29"/>
</bean>
<bean id="sonnet29" class="com.springinaction.springidol.Sonnet29"/>

这相当于执行了如下的Java语句:

1
2
Poem sonnet29 = new Sonnet29();
Performer duke = new PoeticJuggler(15, sonnet29);

打印输出为:

1
2
3
4
5
JUGGLING 15 BEANBAGS
While reciting...
when I was young
I am a boy.....
end of the poem
  • 工厂方法构造Bean
    Spring支持通过元素的factory-method属性来装配工厂创建的Bean,Stage实例如下,没有公开的构造方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.springinaction.springidol;
public class Stage {
public Stage() {
}
//返回实例
public static Stage getInstance() {
return StageSingletonHolder.instance;
}
//延迟加载实例
private static class StageSingletonHolder {
static Stage instance = new Stage();
}
}
1
2
<!--通过工厂方法创建Bean-->
<bean id="theStage" class="com.springinaction.springidol.Stage" factory-method="getInstance"/>

Bean的作用域

Spring的Bean作用域允许用户配置所创建的Bean属于哪一种作用域:

* singleton:在每一个Spring容器中,一个Bean定义只有一个对象实例(默认)
* prototype:允许Bean的定义可以被实例化任意次
* request:在一次HTTP请求中,每个Bean定义对应一个实例。
* session:在依次HTTP Session中,每个Bean定义对应一个实例。
* global-session:在一个全局的HTTP Session中,每个Bean定义对应一个实例。

初始化和销毁Bean

实现开关灯的实体类:

1
2
3
4
5
6
7
8
9
10
11
package com.springinaction.springidol;
public class Auditorium {
public void turnOnLights(){
System.out.println("trun on the lights!!!!");
}
public void trunOffLights() {
System.out.println("turn off the lights!!!!");
}
}

XML配置:

1
2
<!--初始化和销毁bean-->
<bean id="auditorium" class="com.springinaction.springidol.Auditorium" init-method="turnOnLights" destroy-method="trunOffLights" />

Audiorium实例化后会立刻调用init-method,该Bean移除之后会调用destroy-method。

默认的init-method和destroy-method:

1
2
3
4
5
6
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
default-init-method="turnOnLights"
default-destroy-method="trunOffLights"
>

为上下文所有的Bean配置默认的初始化和销毁方法。

注入Bean属性

JavaBean的属性通常是私有的,Spring可以借助其set方法配置属性的值,实现setter注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.springinaction.springidol;
public class Instrumentalist implements Performer{
public Instrumentalist() {
}
@Override
public void perform() throws Exception {
System.out.println("Playing " + song + " : ");
instrument.play();
}
private String song;
public String getSong() {
return song;
}
//注入歌曲
public void setSong(String song) {
this.song = song;
}
public String screanSong() {
return song;
}
private Instrument instrument;
// 注入乐器
public void setInstrument(Instrument instrument) {
this.instrument = instrument;
}
}

其有两个属性,song和Instrument,Instrument接口如下:

1
2
3
4
5
package com.springinaction.springidol;
public interface Instrument {
public void play();
}

XML中配置其默认的构造方法生成Bean kenny:

1
<bean id="kenny" class="com.springinaction.springidol.Instrumentalist"/>

此时kenny没有歌曲也没有乐器,需要对其进行注入属性。

注入简单值

使用元素配置Bean的属性。一旦Instrumentalist被实例化,Spring就会调用元素所指定属性的setter方法为该属性注入值。

1
2
3
4
<!--注入Bean属性-->
<bean id="kenny" class="com.springinaction.springidol.Instrumentalist" >
<property name="song" value="Jingle Bells" />
</bean>

引用其他Bean

定义两个实现了Instrument接口的类:

1
2
3
4
5
6
7
8
9
10
11
package com.springinaction.springidol;
public class saxophone implements Instrument{
public saxophone() {
}
@Override
public void play() {
System.out.println("tttttttoooooooo");
}
}

1
2
3
4
5
6
7
8
9
10
11
package com.springinaction.springidol;
public class Piano implements Instrument{
public Piano() {
}
@Override
public void play() {
System.out.println("play the piano!!!");
}
}

Spring支持将其他Bean注入到当前Bean:

1
2
3
4
5
6
7
<bean id="kenny" class="com.springinaction.springidol.Instrumentalist" >
<property name="song" value="Jingle Bells" />
<property name="instrument" ref="saxophone"/>
<property name="instrument" ref="piano" />
</bean>
<bean id="saxophone" class="com.springinaction.springidol.saxophone" />
<bean id="piano" class="com.springinaction.springidol.Piano" />

  • 注入内部Bean
    注入的Bean仅当前Bean可以使用,此时可以采用内部Bean的注入方法:
    1
    2
    3
    4
    5
    6
    7
    <!-- 内部BEAN -->
    <bean id="kenny" class="com.springinaction.springidol.Instrumentalist">
    <property name="song" value="Jingle Bells" />
    <property name="instrument">
    <bean class="com.springinaction.springidol.saxophone" />
    </property>
    </bean>

除了标签外,标签同样可以注入内部Bean

Spring命名空间的P装配属性

XML声明:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
default-init-method="turnOnLights"
default-destroy-method="trunOffLights"
>

p标签和property功能类似,但是更加简洁:

1
<bean id="kenny" class="com.springinaction.springidol.Instrumentalist" p:song = "Jingle Bells" p:instrument-ref="saxophone"/>

装配集合

  • :装配list类型的值,不允许重复
  • :装配set类型的值,不允许重复
  • :装配map类型的值,名称和值可以是任意类型
  • :装配properties类型的值,名称和值必须都是String类型

属性实际定义的集合类型和选择、没有任何的关系。新建一人可以同时演奏多种乐器的实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.springinaction.springidol;
import java.util.Collection;
public class OneManBand implements Performer{
public OneManBand() {
}
@Override
public void perform() throws Exception {
for (Instrument instrument : instruments) {
instrument.play();
}
}
private Collection<Instrument> instruments;
public void setInstruments(Collection<Instrument> instruments) {
this.instruments = instruments;
}
}

装配list Set Array分别为:

1
2
3
4
5
6
7
8
<bean id="hank" class="com.springinaction.springidol.OneManBand">
<property name="instruments">
<list>
<ref bean="saxophone"/>
<ref bean="piano"/>
</list>
</property>
</bean>

list实际上可以包含另一个list作为其成员构成多维列表,无论还是都可以用来装配类型为Collection的任意实现或者数组的属性。

1
2
3
4
5
6
7
8
<bean id="hank2" class="com.springinaction.springidol.OneManBand2">
<property name="instruments">
<map>
<entry key="PIANO" value-ref="piano"/>
<entry key="SAX" value-ref="saxophone"/>
</map>
</property>
</bean>

装配map类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.springinaction.springidol;
import java.util.Collection;
import java.util.Map;
public class OneManBand2 implements Performer{
public OneManBand2() {
}
@Override
public void perform() throws Exception {
for (String key : instruments.keySet()) {
System.out.println(key + " : ");
instruments.get(key).play();
}
}
private Map<String, Instrument> instruments;
public void setInstruments(Map<String, Instrument> instruments) {
this.instruments = instruments;
}
}

1
2
3
4
5
6
7
8
<bean id="hank2" class="com.springinaction.springidol.OneManBand2">
<property name="instruments">
<map>
<entry key="PIANO" value-ref="piano"/>
<entry key="SAX" value-ref="saxophone"/>
</map>
</property>
</bean>

使用表达式装配

Spring引入了表达式语言Spring Expression Language,SpEL通过运行期执行的表达式将值装配到Bean的属性和构造器参数中去。SpEL的特性包括:

* 使用Bean的ID来引用Bean
* 调用方法和访问对象的属性
* 对值进行算数、关系和逻辑运算
* 正则表达式匹配
* 集合操作

基本要素

#{}会提示Spring这个标记里的内容时SpEL表达式,其格式可以为整型数,浮点数,科学计数法,字符串类型(单引号或者双引号都可以),bool类型

  • 字面值:

    1
    2
    3
    4
    5
    6
    <property name="count" value="#{5}"/>
    <property name="frequency" value="#{89.22}"/>
    <property name="capacity" value="#{1e6}"/>
    <property name="name" value="#{"Chuck"}"/>
    <property name="name" value="#{'Chuck'}"/>
    <property name="enable" value="#{false}"/>
  • 引用Bean属性和方法
    SpEL表达式不仅可以装配字面值,还可以装配Bean的属性和方法:

    1
    2
    3
    <bean id="carl" class="com.springinaction.springidol.Instrumentalist">
    <property name="song" value="#{kenny.song}"/>
    </bean>
1
2
3
<bean id="carl" class="com.springinaction.springidol.Instrumentalist">
<property name="song" value="#{songSelector.selectSong().toUpperCase()}"/>
</bean>

返回值

1
2
3
<bean id="carl" class="com.springinaction.springidol.Instrumentalist">
<property name="song" value="#{songSelector.selectSong().toUpperCase()}"/>
</bean>

为NULL会报空指针错误,解决空指针错误的方法为使用null-safe存取器:

1
2
3
<bean id="carl" class="com.springinaction.springidol.Instrumentalist">
<property name="song" value="#{songSelector.selectSong()?.toUpperCase()}"/>
</bean>

  • 操作类
    T()运算符会调用类作用域的方法和常量。例如T(java.lang.Math),该运算符可以调用指定类的静态方法和常量,当需要把PI装配到Bean中去:
    1
    <property name="PI" value="#{T(java.lang.Math).PI}"/>

值操作

  • 运算符

    • 算数运算:+、-、*,/, %, ^

      1
      <property name="adjustedAmount" value="#{counter.total-20}"/>
    • 关系运算:<、>、==、 >=、 <=、lt、 gt、 eq、 le、 ge

      1
      <property name="equal" value="#{counter.total == 100}"/>
    • 逻辑运算:and、 or、 not、 |

      1
      <property name="largeCircle" value="#{shape.kind == 'circle' and shape.perimeter gt 100000}"/>
    • 条件运算:?:(ternary)、?:(Elvis)

      1
      <property name="instrument" value="#{songSelector.selectSong()=='Jingle Bells'?piano:saxophone}"/>
    • 正则表达式:matches

      1
      <property name="validEmail" value="#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}"/>

SpEL中筛选集合

设定一个City类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.springinaction.springidol;
public class City {
private String name;
private String state;
private int population;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public int getPopulation() {
return population;
}
public void setPopulation(int population) {
this.population = population;
}
}

通过Spring的元素定义一个City的List集合:

1
2
3
4
5
<util:list id="cities">
<bean class="com.springinaction.springidol.City" p:name="Chicago" p:state="IL" p:population="285114"/>
<bean class="com.springinaction.springidol.City" p:name="Atlanta" p:state="GA" p:population="537958"/>
<bean class="com.springinaction.springidol.City" p:name="Dallas" p:state="TX" p:population="1279910"/>
</util:list>

  • 访问集合成员:
    1
    2
    <property name="chosenCity" value="#{cities[1]}"/>
    <property name="chosenCity" value="#{cities[T(java.lang.Math).random * cities.size()]}"/>

选择属性的方式:

  • systemEnvironment:包含应用程序所在机器上的所有的环境变量,Propert集合
    <property name="homePath" value="#{systemEnvironment['HOME']}"/>
  • systemProperties:包含了Java应用程序启动时所设置的所有的属性
  • 查询集合成员
    查询运算符.?[],会创建一个新的集合存放符合条件的CITY:

    1
    <property name="bigCities" value="#{cities.?[population gt 100000]}"/>
    • .^[]:从集合中查询出第一个匹配项
    • .$[]:从集合中查询出最后一个匹配项
  • 投影集合
    .![]:从集合中的每一个成员中取出特定的属性放入一个新的集合中

    1
    2
    3
    <property name="CityName" value="#{cities.![name]}"/>
    <property name="CityName" value="#{cities.![name + ', ' + state]}"/>
    <property name="bigCities" value="#{cities.?[population gt 100000].![name + ', ' + state]}" />

最小化Spring XML配置


Spring提供了几种技巧解决大量Bean下XML文件的配置:

  • 自动装配(autowiring):有助于减少甚至消除配置元素和元素,让Spring自动识别如何装配Bean的依赖关系。
  • 自动检测(autodiscovery):比自动装配更进一步,让Spring能够自动识别哪些类需要被配置成Spring Bean,减少对的使用。

自动装配Bean属性

四种类型的自动装配

  • byName:把与Bean的属性具有相同名字(ID)的其他Bean自动装配到Bean的对应属性中。如果没有和属性名字相匹配的Bean,则该属性不装配。
    kenny Bean中,使用property标签显式配置了instrument的属性,如果将saxophone Bean的名字改为instrument:

    1
    2
    3
    4
    <bean id="kenny" class="com.springinaction.springidol.Instrumentalist" >
    <property name="song" value="Jingle Bells" />
    <property name="instrument" ref="saxophone"/>
    </bean>

    等价于:

    1
    2
    3
    4
    <bean id="kenny3" class="com.springinaction.springidol.Instrumentalist" autowire="byName">
    <property name="song" value="Jingle Bells" />
    </bean>
    <bean id="instrument" class="com.springinaction.springidol.saxophone"/>

    byName自动装配遵循的一项约定:为属性自动装配ID与该属性的名字相同的Bean

  • byType:把与Bean的属性具有相同类型的其他Bean自动装配到Bean的对应属性中,如果没有跟属性的类型相匹配的Bean,则该属性不装配。
    与byName类似,不过是检查属性的类型,Spring会寻找哪一个Bean的类型与属性的类型相匹配,但是当Spring找到多个Bean的类型与属性相同时,会抛出错误,应用只允许一个与属性类型相同的Bean。解决方法是为:

    * 自动装配标示一个首选Bean,有且仅有一个Bean的primary=true
    
    1
    <bean id="instrument" class="com.springinaction.springidol.saxophone" primary="true"/>
    * 取消某个Bean自动装配的资格。
    1
    <bean id="instrument" class="com.springinaction.springidol.saxophone" autowire-candidate="false" />
  • constructor:把与Bean的构造器入参具有相同类型的其他Bean自动装配到Bean构造器对应入参中去。

    1
    <bean id="duke1" class="com.springinaction.springidol.PoeticJuggler" autowire="constructor"/>

    Spring会去寻找和Bean的构造方法中的属性类型相同的Bean注入到当前Bean,局限性和byType一样。多个合适的Bean或者多个构造器都会出错。

  • autodetect:首先尝试用constructor进行自动装配,如果失败,再尝试用byType进行自动装配。

默认自动装配

1
<beans ... default-autowire="byType">

该参数的默认值为none,特定Bean可以设定其自动装配方式来覆盖默认的装配方式。

混合使用自动装配和显示装配

对某个Bean选择了自动装配策略,同时也可以进行显示装配。可以通过显示装配来解决自动装配中存在的不确定的问题,覆盖掉其自动装配的值。

1
2
3
4
<bean id="kenny4" class="com.springinaction.springidol.Instrumentalist" autowire="byType">
<property name="song" value="Jingle Bell" />
<property name="instrument"><null /></property>
</bean>

  • NOTE:不能混合使用constructor自动装配策略和元素

使用注解装配

Spring默认不使用注解,使用注解需要在XML中进行配置:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<context:annotation-config />
</beans>

Spring支持几种不同的用于自动装配的注解:

* Spring自带的@Autowired注解
* JSR-330的@Inject注解
* JSR-250的@Resource注解

使用@Autowired注解

对属性的setter方法添加注解:

1
2
3
4
@Autowired
public void setInstrument(Instrument instrument) {
this.instrument = instrument;
}

Autowired注解可以用在setter方法,任意方法以及构造方法。当对构造器进行标注时,Autowired注解表示当创建Bean时,即使在Spring XML文件中没有使用元素配置Bean,该构造器也需要进行自动装配。我们也可以使用@AutoWired注解直接标注属性,并删除setter方法。@AutoWired存在问题在于没有Bean装配到注解属性或者多个Bean装配到注解属性:

  • 可选的自动装配:@Autowired所标注的属性和参数必须是可装配的,如果没有Bean装配到所标注的属性和参数时,会报错。设定required参数来限定@Autowired,当没有找到可装载的Bean时,属性和参数的值为NULL。

    1
    2
    @Autowired(required = false)
    private Instrument instrument;

    但是当其注解构造方法时,只能有一个构造器的required的值为true。

  • 限定歧义性的依赖:当有同时满足装配的条件的多个Bean存在时,Spring会抛出NoSuchBeanDefineException错误,表明装配失败。使用@Qualifier注解来制定装配的具体Bean:

    1
    2
    3
    @AutoWired
    @Qualifier("guitar")
    private Instrument instrument;

    XML配置也可以同样使用qualifier标签来缩小Bean搜索范围:

    1
    2
    3
    <bean class="com.springinaction.springidol.Piano">
    <qualifier value="piano"/>
    </bean>
  • 自定义的限定器
    使用@Qualifier作为自定义注解的元注解:@

    1
    2
    3
    @Qualifier
    public @Interface StringedInstrument{
    }

    这样就可以用@StringedInstrument注解来标注guitar

    1
    2
    3
    @StringedInstrument
    public class Guitar implements Instrument{
    }

    自动装配时对Instrument进行限定,从而查找所有被StringedInstrument标注的Bean,不能确定到一个Bean会再进行其他限定。

    使用@Autowired的缺点在于引入了对Spring注解的依赖,可以使用Java的@Inject注解来解决这个问题

@Inject注解

与Spring的@Autowired注解类似,JCP的@Inject注解可以用来自动装配属性、方法和构造器;区别在于@Inject没有required属性,所有其所标注的依赖关系必须存在!

  • 限定@Inject使用注解@Named

    1
    2
    3
    @Inject
    @Named("guitar")
    private Instrument Instrument;

    @Named注解表明选择了一个Bean,@Qualifier表明满足条件的Bean

  • 自定义的JSR-330 Qualifier

    1
    2
    3
    @Qualifier
    public @Interface StringedInstrument{
    }

    区别在于这里的@Qualifier来自JSR-330而不是Spring

注解中使用表达式

@Value可以用来装配String类型的值和基本类型的值:

1
2
@value("Eruption")
private String song;

SpEL和注解结合,实现注解驱动的装配方式:

1
2
@value("#{systemProperties.myFavoriteSong}")
private String song;

自动检测Bean

<context:annotation-config>可以消除Bean里面的配置,<context:component-scan>还可以完成自动检测Bean的功能,从而XML中不需要创建Bean:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<context:component-scan base-package="com.springinaction.springidol"/>
</beans>

标注Bean

context:component-scan默认查找构造型注解所标注的类:

  • @Component 通用的构造型注解,标示该类为Spring组件
  • @Controller 标示该类定义为SpringMVC controller
  • @Repository 标识将该类定义为数据仓库
  • @Service 标识将该类定义为服务
  • @Component也可以标注自定义注解
1
2
3
4
5
6
7
8
9
10
11
package com.springinaction.springidol;
import org.springframework.stereotype.Component;
@Component
public class Guitar implements Instrument{
@Override
public void play() {
System.out.println("ssss ddd fff .");
}
}

Spring扫描com.springinaction.springidol包时,会发现@Component注解标识的Guitar类,将其自动注册为Spring Bean。默认ID为guitar。

1
2
3
4
5
6
7
8
9
package com.springinaction.springidol;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component("eddie")
public class Instrumentalist implements Performer{
...
}

过滤组件扫描

Spring默认会扫描包下的所有类的@Component注解来生成Bean,我们可以自己定义过滤规则来实现自动注册特定的类:

1
2
3
<context:component-scan base-package="com.springinaction.springidol">
<context:include-filter type="assignable" expression="com.springinaction.springidol.Instrument"
</context:component-scan>

可以自动将所有派生与Instrument的类注册到Spring Bean中。
过滤器类型包含5种:

  • annotation:顾虑器扫描使用指定注解所标注的那些类,通过expression属性指定要扫描的注解
  • assignable:过滤器通过扫描派生于expression属性所指定的那些类
  • aspectj:过滤器扫描与expression属性所指定的AspectJ表达式所匹配的类
  • custom:使用自定义的org.springframework.core.type.TypeFilter实现类,该类由expression属性指定
  • regex:过滤器扫描类的名称与expression属性所指定的正则表达式所匹配的那些类。

context:exclude-filter表示不注册那些类:

1
2
3
4
<context:component-scan base-package="com.springinaction.springidol">
<context:include-filter type="assignable" expression="com.springinaction.springidol.Instrument"/>
<context:exclude-filter type="assignable" expression="com.springinaction.springidol.Instrumentalist"/>
</context:component-scan>

使用Spring基于Java的配置

启动Java配置

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<context:component-scan base-package="com.springinaction.springidol"/>
</beans>

context:component-scan会自动加载@Configuration注解的类。

定义一个配置类

1
2
3
4
5
6
7
8
package com.springinaction.springidol;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringIdolConfig {
}

@Configuration注解会告知Spring,这个类将包含一个或者多个Bean的定义。

声明一个Bean

1
2
3
4
@Bean
public Performer duke() {
return new Juggler();
}

等价于XML中配置标签。@Bean告知Spring这个方法将返回一个对象,该对象应该被注册到Spring应用上下文中的一个Bean。

使用Spring的基于Java的配置进行注入

1
2
3
4
@Bean
public Performer duke10() {
return new Juggle(15);
}
1
2
3
4
5
6
7
8
9
@Bean
private Poem sonnet29(){
return new sonnet29();
}
@Bean
private Performer pueticDuke(){
return new PoeticJuggler(sonnet29());
}

面向切面的Spring

两个概念:

  • 横切关注点:分布于应用中的多处的功能,例如安全、日志、事务管理等。
  • AOP:将横切关注点和业务逻辑进行分离

面向切面编程

Spring中首先在一个地方定义通用功能,然后通过声明的方式定义一个功能以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类成为切面。其好处有两点:

  • 每个关注点集中与移除
  • 服务模块更简洁

定义AOP术语

  • 通知(Advice):通知定义了切面是什么以及如何使用。Spring切面可以应用的5中类型的通知:

    • Before:在方法被调用之前调用通知
    • After:在方法完成之后调用通知,无论方法执行是否成功
    • After-retruning:在方法成功执行之后调用通知
    • After-throwing:在方法抛出异常后调用通知
    • Around:通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
  • 连接点(Joinpoint):在应用执行过程中能够插入切面的一个点。

  • 切点(Poincut):指定了切面应用的范围。
  • 切面(Aspect):切面是切点和通知的结合。
  • 引入(Introduction):引用允许我们向现有的类添加新方法和属性。
  • 织入(Weaving):织入是将切面应用到目标对象来创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中去。在目标对象的声明周期中有多个点可以进行织入:
    • 编译期:需要特殊的编译器,AspectJ的织入编译器就是以这种方式织入的。
    • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),在引入之前增强该类的字节码。
    • 运行期:切面在应用运行的某个时刻被织入。一般在织入切面时,AOP容器会为目标对象动态的创建一个代理对象。Spring AOP就是这样。

Spring对AOP的支持

Spring提供了4种AOP支持:

  • 基于代理的经典AOP
  • @AspectJ注解驱动的切面
  • 纯POJO切面
  • 注入式AspectJ切面

一些关键点:

  • Spring通知是Java编写的
  • Spring在运行期间通知对象
  • Spring只支持方法连接点

使用切点选择连接点

Spring AOP中需要使用AspectJ的切点表达式语言来定义切点。

AspectJ指示器 描述
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配AOP代理的Bean引用为指定类型的类
target() 限制连接点匹配目标对象为指定类型的类
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解
within() 限制连接点匹配指定的类型
@within() 限制连接点匹配指定注解所标注的类型
@annotation() 限制匹配带有指定注解连接点

只有execution指示器是唯一的执行匹配,其他都是限制匹配。

编写切点

execution(* com.springinaction.springidol.Instrument.play(..))
我们使用execution()指示器去选择Instrument的play()方法。方法表达式以开始,标示了方法返回值的类型,然后指定全限定类名和方法名,使用(..)标示切点选择任意的play方法。
如果需要限定匹配该包下的play方法,需要添加限定
`execution(
com.springinaction.springidol.Instrument.play(..)) and withi n(com.springinaction.springidol.*)`

使用Spring的bean()指示器

bean()指示器允许我们在切点表达式中使用Bean的ID来标识Bean。
execution(* com.springinaction.springidol.Instrument.play() and bean(eddie))表示限定Bean的ID为Eddie。

在XML中声明切面

Spring的AOP配置元素简化了基于POJO切面的声明:

AOP配置元素 描述
定义AOP通知器
定义AOP后置通知,不管通知的方法是否执行成功
定义AOP After-retruning通知
定义After-throwing通知
定义AOP环绕通知
定义切面
启用@AspectJ注解驱动的切面
定义AOP前置通知
顶层的AOP配置元素,大多数的元素必须在元素内部
为被通知的对象引入额外的接口,并透明的实现
定义切点

选秀节目需要装配观众,每一个选手都需要装配同样的观众,创建一个观众类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.springinaction.springidol;
public class Audience {
//before perform
public void takeSeats() {
System.out.println("The Audience is taking their seats!");
}
public void turnOffCellPhone() {
System.out.println("The audience is turning off their cell phones.");
}
//after perform
public void applaud() {
System.out.println("Clap! Clap! Clap!");
}
//after perform failure
public void demandRefund() {
System.out.println("We want our money back!");
}
}

将其注册为Spring上下文的一个Bean:<bean id="audience" class="com.springinaction.springidol.Audience"/>

Redis-redis底层数据结构

发表于 2019-04-21 | 分类于 redis

Redis介绍

redis是一个基于键值对的分布式存储系统,与memcached类似,却优于memcached的一个高性能的key-value数据库。其中键值对的key总是为一个字符串对象,字符串的值可以为字符串对象,列表对象,哈希对象,集合对象,有序集合对象之一。

Redis中value的值为上面五种,但是底层数据结构包括:

  • 简单动态字符串
  • 链表
  • 字典
  • 跳跃表
  • 整数集合
  • 压缩列表
  • 对象

简单动态字符串(simple dynamic string)

介绍

Redis底层是由C编写的,但是其对C中字符串做了一个封装,构建了种名为简单动态字符串的抽象类型,并将SDS设置为redis默认采用的字符串表示

1
2
redis>SET msg "sdsTest"
OK

key是一个字符串对象,字符串对象的低层是一个保存着“msg”的SDS;
value是一个字符串对象,低层是一个保存着字符串“sdsTest”的SDS。

SDS的定义

redis中定义动态字符串的结构:

1
2
3
4
5
6
7
8
typedef char *sds;
//指向sdshdr结构buf成员的地址
struct sdshdr {
int len; //buf中已占用空间的长度
int free; //buf中剩余可用空间的长度
char buf[]; //初始化sds分配的数据空间
};

则msg可以被表示为:

Alt text

  • len为7,表示这个sds的长度为7
  • free为0,表示这个sds可用空间为0
  • buf是一个char数组,分配了len + 1 + free个字节的长度,前len个字节保存着SDS的信息,接下来一个字符保存字符串终止符’\0’,剩下free个字节是未使用的。

SDS与C字符串的区别

redis不直接使用C字符串而采用SDS的原因主要在于传统C语言使用长度为N+1的字符数组来存储长度为N的字符串,这样在获取字符串长度,字符串扩展操作时效率低,不能满足redis对字符串在安全性、功能、效率等的要求。

  1. 获取字符串长度

    • 传统C语言:获取字符串长度需要遍历字符数组,复杂度为O(N);
    • Redis SDS:获取len属性的值就是字符串的长度 O(1);
  2. 杜绝缓冲区溢出

    • C语言中字符串不记录字符串的长度,容易造成缓冲区溢出。比如拼接字符串过程容易发生缓冲区溢出。
    • redis中sds的空间分配策略杜绝了发生缓冲区溢出:redis会在拼接字符串之前,预先检查给定SDS空间是否足够,如果不够会先扩展SDS空间,然后再执行拼接操作。
  3. 减少修改字符串时带来的内存重新分配次数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    /* Enlarge the free space at the end of the sds string so that the caller
    * is sure that after calling this function can overwrite up to addlen
    * bytes after the end of the string, plus one more byte for nul term.
    *
    * Note: this does not change the *length* of the sds string as returned
    * by sdslen(), but only the free buffer space we have. */
    sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    size_t avail = sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s;
    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)
    newlen *= 2;
    else
    newlen += SDS_MAX_PREALLOC;
    type = sdsReqType(newlen);
    /* Don't use type 5: the user is appending to the string and type 5 is
    * not able to remember empty space, so sdsMakeRoomFor() must be called
    * at every appending operation. */
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;
    hdrlen = sdsHdrSize(type);
    if (oldtype==type) {
    newsh = s_realloc(sh, hdrlen+newlen+1);
    if (newsh == NULL) return NULL;
    s = (char*)newsh+hdrlen;
    } else {
    /* Since the header size changes, need to move the string forward,
    * and can't use realloc */
    newsh = s_malloc(hdrlen+newlen+1);
    if (newsh == NULL) return NULL;
    memcpy((char*)newsh+hdrlen, s, len+1);
    s_free(sh);
    s = (char*)newsh+hdrlen;
    s[-1] = type;
    sdssetlen(s, len);
    }
    sdssetalloc(s, newlen);
    return s;
    }
    • C语言在进行字符串的扩展和收缩的时候,都会面临着内存空间的重新分配问题。字符串拼接会产生字符串的内存空间的扩充,忘记申请分配空间会导致内存溢出;而字符串切割的时候,如果没有对内存空间重新分配会造成内存泄漏。

    • redis对SDS进行扩展,修改是SDS的长度,通过预分配侧率,SDS将连续增长的N次字符串所需的内存重分配次数从固定N次,降低为最多N次。

  4. 惰性空间的释放

    redis字符串进行收缩时,修改free属性来记录剩余的空间,并不直接回收那些不使用的内存,这样可以避免下次对字符串再次修改过程中可能存在的空间扩展,避免了缩短字符串所需要的内存重分配操作和将来可能存在的增长字符串操作。

  5. 二进制安全
    C语言中字符串以空字符串为结束符,所以传统字符串不能保存图片视频等二级制文件。redis中依靠len来判断字符串是否结束,而不是寻找空字符串,这样可以用来保存图片,音频,视频等二进制数据,实现了二级制安全。SDS遵循C字符串以空字符串结尾的惯例。

链表

链表在redis中使用很多,比如列表件的低层实现之一就是链表。而且在redis中链表的结构被实现为双向链表,收尾操作很快。

链表的数据结构

1
2
3
4
5
6
7
/* Node, List, and Iterator are the only data structures used currently. */
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
  • 双向链表通过prev和next指针来会获取当前节点的前驱节点和后继节点的复杂度为O(1);

我们可以直接通过list来操作链表结构:

1
2
3
4
5
6
7
8
typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned long len;
} list;

Alt text

  • 其中head指针指向链表头,tail指针指向链表尾,获取收尾节点复杂度O(1)
  • len记载链表的长度 获取长度复杂度为O(1);
  • dup,free,match指针实现多态,链表节点listNode使用空指针来保存节点的值,表头list使用dup,free, match指针来针对链表中存放非不同对象来实现不同的方法。

字典

或者成为map,是一种用于保存键值对的抽象数据结构。字典中一个key可以和一个value进行关联,每一个key独一无二并且指向一个value,一个value可以被多个可以同时指定。C语言中不存在字典这种数据结构,redis实现了字典的数据结构。redis存取数据采用的都是key-value的形式,其中key为一个字符串对象,而value取值可以是字符串,可以是列表,可以是集合等。

字典的实现

redis中字典是通过哈希表实现的,一个哈希表有多个节点,每个节点保存一个键值对。

1
2
3
4
5
6
7
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int iterators; /* number of iterators currently running */
} dict;
  • type: 指针指向dictType结构,该结构使得key和value能够存储任何类型的数据。
  • privdata: 私有数据,存放dictType结构中的函数的参数
1
2
3
4
5
6
7
8
typedef struct dictType {
unsigned int (*hashFunction)(const void *key); //计算hash值的函数
void *(*keyDup)(void *privdata, const void *key); //复制key的函数
void *(*valDup)(void *privdata, const void *obj); //复制value的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2); //比较key的函数
void (*keyDestructor)(void *privdata, void *key); //销毁key的析构函数
void (*valDestructor)(void *privdata, void *obj); //销毁val的析构函数
} dictType;
  • ht[2]: 两张哈希表
1
2
3
4
5
6
typedef struct dictht {
dictEntry **table; //哈希表节点dictEntry位置
unsigned long size; //哈希表大小,初始4
unsigned long sizemask; //将哈希值映射到table索引
unsigned long used; //记录哈希表已有节点数量
} dictht;
1
2
3
4
5
6
7
8
9
10
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; //解决哈希冲突
} dictEntry;
  • rehashidx: rehash的标志位
  • iterators:记录迭代器的数量

Redis中使用的哈希算法

  • 计算int的哈希函数
1
2
3
4
5
6
7
8
9
10
unsigned int dictIntHashFunction(unsigned int key)
{
key += ~(key << 15);
key ^= (key >> 10);
key += (key << 3);
key ^= (key >> 6);
key += ~(key << 11);
key ^= (key >> 16);
return key;
}
  • MurmurHash2哈希算法:redis使用MurmurHash2算法来计算法哈希值,能产生32-bit或64-bit哈希值。该算法对硬件的要求在于:1.机器可以从任意一个地址读取一个4-bit大小的值,并且int的大小为4bit。seed默认值为5381,可以人为改变,不过没什么意义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
unsigned int dictGenHashFunction(const void *key, int len) {
/* 'm' and 'r' are mixing constants generated offline.
They're not really 'magic', they just happen to work well. */
uint32_t seed = dict_hash_function_seed;
const uint32_t m = 0x5bd1e995;
const int r = 24;
/* Initialize the hash to a 'random' value */
uint32_t h = seed ^ len;
/* Mix 4 bytes at a time into the hash */
const unsigned char *data = (const unsigned char *)key;
while(len >= 4) {
uint32_t k = *(uint32_t*)data;
k *= m;
k ^= k >> r;
k *= m;
h *= m;
h ^= k;
data += 4;
len -= 4;
}
/* Handle the last few bytes of the input array */
switch(len) {
case 3: h ^= data[2] << 16;
case 2: h ^= data[1] << 8;
case 1: h ^= data[0]; h *= m;
};
/* Do a few final mixes of the hash to ensure the last few
* bytes are well-incorporated. */
h ^= h >> 13;
h *= m;
h ^= h >> 15;
return (unsigned int)h;
}
  • djb哈希算法: seed和字符串的ascii值,len次变换之后得到hash值。
1
2
3
4
5
6
7
8
/* And a case insensitive hash function (based on djb hash) */
unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len) {
unsigned int hash = (unsigned int)dict_hash_function_seed;
while (len--)
hash = ((hash << 5) + hash) + (tolower(*buf++)); /* hash * 33 + c */
return hash;
}

rehash

当哈希表的大小不能满足需求,可能会有两个以上的键值被分配到同一个位置上,这时候就发生了冲突,redis中解决冲突的方法和HashMap中类似,使用链表法。但是当这种情况频繁发生,会影响查找的性能,需要尽力避免。这个时候希望通过哈希表的load factor来动态的决定扩展哈希表或者收缩哈希表。

  • rehash流程:
    1. 根据要求扩展或者收缩ht[1]
    2. 将ht[0]上的节点rehash到ht[1]上
    3. 释放ht[0], 将ht[1]设置为ht[0],新建一个ht[1]

扩展操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
/* Incremental rehashing already in progress. Return. */
if (dictIsRehashing(d)) return DICT_OK;
/* If the hash table is empty expand it to the initial size. */
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
/* If we reached the 1:1 ratio, and we are allowed to resize the hash
* table (global setting) or we should avoid it but the ratio between
* elements/buckets is over the "safe" threshold, we resize doubling
* the number of buckets. */
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
return dictExpand(d, d->ht[0].used*2);
}
return DICT_OK;
}

收缩操作:

1
2
3
4
5
6
7
8
9
10
int dictResize(dict *d)
{
int minimal;
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used;
if (minimal < DICT_HT_INITIAL_SIZE)
minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal);
}

根据传入的size来跳帧过着创建字典d的哈希表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
dictht n; /* the new hash table */
unsigned long realsize = _dictNextPower(size);
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;
/* Rehashing to the same table size is not useful. */
if (realsize == d->ht[0].size) return DICT_ERR;
/* Allocate the new hash table and initialize all pointers to NULL */
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.used = 0;
/* Is this the first initialization? If so it's not really a rehashing
* we just set the first hash table so that it can accept keys. */
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
/* Prepare a second hash table for incremental rehashing */
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}

rehash过程不是一次性集中完成的,而是分多次,渐进式的,断续进行的,这样可以减少对服务器的影响。

渐进式rehash(incremental rehashing)

  1. dict中有一个成员为rehashidx,表示rehash的状态标志,-1表示不进行rehash,0表示开始进行rehash。
  2. rehash过程中,每次对字典的CRUD时,会检查rehashidx标志位,如果在进行rehash,CRUD结束之后顺带进行单步rehash,同时rehashidx+1;
  3. rehash结束之后,将rehashidx该为-1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d,1);
}
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
while(n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
assert(d->ht[0].size > (unsigned long)d->rehashidx);
while(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht[0].table[d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT */
while(de) {
unsigned int h;
nextde = de->next;
/* Get the index in the new hash table */
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}

跳跃表

跳跃表示一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而可以快速到达要访问的节点。跳跃表是一个有序链表,其查找复杂度平均为O(logN),最坏O(N)。redis中将跳跃表应用在有序集合键。

Redis中跳跃表主要有两部分组成:zskiplist和中skiplistNode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level; //跳跃表中节点的最大层数,除了第一个节点
} zskiplist;
typedef struct zskiplistNode {
robj *obj;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
  • level:层,每个元素包含一个指向其他节点的指针,实现跳跃查询的关键。

整数集合

整数结合是集合键低层实现之一。集合键另一实现为值为空的散列表。整数集合是为了节省使用散列表存储整形对象时的空间浪费。

1
2
3
4
5
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
  • encoding:编码格式,默认用2个字节存储,还有4个字节和8个字节
  • length:集合元素的数量
  • contents:数组类型

新的元素插入到集合的流程:

  1. 判断新元素的编码格式
1
2
3
4
5
6
7
8
9
/* Return the required encoding for the provided value. */
static uint8_t _intsetValueEncoding(int64_t v) {
if (v < INT32_MIN || v > INT32_MAX)
return INTSET_ENC_INT64;
else if (v < INT16_MIN || v > INT16_MAX)
return INTSET_ENC_INT32;
else
return INTSET_ENC_INT16;
}
  1. 调整内存空间
1
2
3
4
5
6
/* Resize the intset */
static intset *intsetResize(intset *is, uint32_t len) {
uint32_t size = len*intrev32ifbe(is->encoding);
is = zrealloc(is,sizeof(intset)+size);
return is;
}
  1. 根据编码格式设置对应的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Set the value at pos, using the configured encoding. */
static void _intsetSet(intset *is, int pos, int64_t value) {
uint32_t encoding = intrev32ifbe(is->encoding);
if (encoding == INTSET_ENC_INT64) {
((int64_t*)is->contents)[pos] = value;
memrev64ifbe(((int64_t*)is->contents)+pos);
} else if (encoding == INTSET_ENC_INT32) {
((int32_t*)is->contents)[pos] = value;
memrev32ifbe(((int32_t*)is->contents)+pos);
} else {
((int16_t*)is->contents)[pos] = value;
memrev16ifbe(((int16_t*)is->contents)+pos);
}
}

数组集合的好处:提升灵活性并节省内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* Upgrades the intset to a larger encoding and inserts the given integer. */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
uint8_t curenc = intrev32ifbe(is->encoding);
uint8_t newenc = _intsetValueEncoding(value);
int length = intrev32ifbe(is->length);
int prepend = value < 0 ? 1 : 0;
/* First set new encoding and resize */
is->encoding = intrev32ifbe(newenc);
is = intsetResize(is,intrev32ifbe(is->length)+1);
/* Upgrade back-to-front so we don't overwrite values.
* Note that the "prepend" variable is used to make sure we have an empty
* space at either the beginning or the end of the intset. */
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
/* Set the value at the beginning or the end. */
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}

压缩列表

ziplist是哈希键的低层实现之一。为特殊编码的双向链表,和整数集合一样是为了提高内存的存储效率而的设计的。当保存的对象是小整数,或者长度较短的字符串,redis就会使用压缩列表来作为哈希键的实现。

压缩列表的构成

1
2
3
4
5
6
7
8
9
10
11
12
/* Macro to determine type */
#define ZIP_IS_STR(enc) (((enc) & ZIP_STR_MASK) < ZIP_STR_MASK)
/* Utility macros */
#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
#define ZIPLIST_END_SIZE (sizeof(uint8_t))
#define ZIPLIST_ENTRY_HEAD(zl) ((zl)+ZIPLIST_HEADER_SIZE)
#define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
#define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)

Alt text

压缩链表可以包含任意多个节点,每个节点保存一个字节数组或者一个整数值。

MySQL数据库优化

发表于 2019-04-21 | 分类于 MySQL , database

序言

学习了慕课网上的MySQL数据库优化课程,链接为:https://www.imooc.com/video/3688, 记录下自己学习过程中的收获。

  • 数据库优化的目的

    • 避免出现网页访问错误
      • 由于数据库连接超时产生页面5XX的错误
      • 由于慢查询造成页面无法加载
      • 由于阻塞造成数据无法提交
    • 增加数据库的稳定性
      • 许多数据库问题都是由于低效的查询引起的
    • 优化用户体验
      • 流畅页面的访问速度
      • 良好的网站功能体验
  • 数据库优化可以从哪几方面进行:(成本从低到高,效果从高到底)

    • SQL及索引优化
    • 数据库表结构优化
    • 系统配置优化
    • 硬件优化

SQL语句优化

  • 如何发现有问题的SQL:使用MySQL慢查询日志对效率有问题的SQL进行监控

    1
    2
    3
    4
    5
    6
    7
    mysql> show variables like 'slow_query_log';
    +----------------+-------+
    | Variable_name | Value |
    +----------------+-------+
    | slow_query_log | ON |
    +----------------+-------+
    1 row in set, 1 warning (0.00 sec)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    mysql> show variables like 'slow%';
    +---------------------+--------------------------+
    | Variable_name | Value |
    +---------------------+--------------------------+
    | slow_launch_time | 2 |
    | slow_query_log | ON |
    | slow_query_log_file | DESKTOP-EKS47TB-slow.log |
    +---------------------+--------------------------+
    3 rows in set, 1 warning (0.00 sec)
  • 慢查日志包含的内容:

    • 执行SQL的主机信息
    • SQL执行信息
    • SQL执行时间
    • SQL的内容
  • 慢查询日志分析工具

    • mysqldumpslow
    • pt-query-digest
      • 查询次数多且每次查询占用时间长的SQL
      • IO大的SQL
      • 未命中索引的SQL
  • explain查询和分析查询语句:

1
2
3
4
5
6
7
mysql> explain select customer_id, first_name, last_name from customer;
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | customer | NULL | ALL | NULL | NULL | NULL | NULL | 599 | 100.00 | NULL |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
  • table 数据来自哪张表
  • type 连接使用的类型,最好到最坏的链接类型分别为const,eq_reg,ref,range,index,ALL.
  • possible_keys:显示可能应用在这张表上的索引
  • key:实际使用的索引
  • key_len:使用的索引长度
  • ref:显示索引哪一列被使用了,如果可能的话,是一个常数
  • rows:Mysql认为必须检查的用来返回请求的行数
  • extra
    • Using filesort:查询需要优化,MySQL需要进行额外的步骤来发现如何对返回的行排序。它根据连接类型以及存储排序的键值和匹配条件的全部行的行指针来排序全部行。
    • Using temporary:需要优化,MySQL此时创建了一个临时表来存储结果,这通常发生在对不同的列集进行ORDER by上,而不是group by。
  • Count()和Max()优化方法

    • 查询最后支付时间:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      mysql> explain select max(payment_date) from payment \G
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: payment
      partitions: NULL
      type: ALL
      possible_keys: NULL
      key: NULL
      key_len: NULL
      ref: NULL
      rows: 16086
      filtered: 100.00
      Extra: NULL
      1 row in set, 1 warning (0.00 sec)
    • 在payment字段上创建索引:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      mysql> create index idx_paydate on payment(payment_date);
      Query OK, 0 rows affected (0.06 sec)
      Records: 0 Duplicates: 0 Warnings: 0
      mysql> explain select max(payment_date) from payment \G
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: NULL
      partitions: NULL
      type: NULL
      possible_keys: NULL
      key: NULL
      key_len: NULL
      ref: NULL
      rows: NULL
      filtered: NULL
      Extra: Select tables optimized away
      1 row in set, 1 warning (0.00 sec)
    • 在一条SQL中同时查出2006年和2007年电影的数量-优化count()函数

      • 错误实例:

        1
        2
        3
        4
        5
        6
        7
        mysql> select count(release_year='2006' or release_year='2007') from film;
        +---------------------------------------------------+
        | count(release_year='2006' or release_year='2007') |
        +---------------------------------------------------+
        | 1000 |
        +---------------------------------------------------+
        1 row in set (0.01 sec)
      • 正确的:

        1
        2
        3
        4
        5
        6
        7
        mysql> select count(release_year='2006' or null) as '2006电影数量', count(release_year='2007' or null) as '2007电影数量' from film;
        +--------------+--------------+
        | 2006电影数量 | 2007电影数量 |
        +--------------+--------------+
        | 1000 | 0 |
        +--------------+--------------+
        1 row in set (0.00 sec)

        ‘count(*)计算空值,count(id)不计算空值’

    • 子查询优化

      • 通常情况下,需要把子查询优化为join查询,但在优化时要注意关联键是否有一对多的关系,要注意重复数据。
    • 优化group by查询,使用了临时表

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      mysql> explain select actor.first_name, actor.last_name, COUNT(*) from sakila.film_actor inner join sakila.actor using(actor_id) group by film_actor.actor_id\G
      *************************** 1. row ***************************
      id: 1
      select_type: SIMPLE
      table: actor
      partitions: NULL
      type: ALL
      possible_keys: PRIMARY
      key: NULL
      key_len: NULL
      ref: NULL
      rows: 200
      filtered: 100.00
      Extra: Using temporary; Using filesort
      *************************** 2. row ***************************
      id: 1
      select_type: SIMPLE
      table: film_actor
      partitions: NULL
      type: ref
      possible_keys: PRIMARY,idx_fk_film_id
      key: PRIMARY
      key_len: 2
      ref: sakila.actor.actor_id
      rows: 27
      filtered: 100.00
      Extra: Using index
      2 rows in set, 1 warning (0.00 sec)

      优化之后:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      mysql> explain select actor.first_name, actor.last_name, c.cnt from sakila.actor inner join (select actor_id, COUNT(*) as cnt from sakila.film_actor group by actor_id) as c using(actor_id)\G
      *************************** 1. row ***************************
      id: 1
      select_type: PRIMARY
      table: actor
      partitions: NULL
      type: ALL
      possible_keys: PRIMARY
      key: NULL
      key_len: NULL
      ref: NULL
      rows: 200
      filtered: 100.00
      Extra: NULL
      *************************** 2. row ***************************
      id: 1
      select_type: PRIMARY
      table: <derived2>
      partitions: NULL
      type: ref
      possible_keys: <auto_key0>
      key: <auto_key0>
      key_len: 2
      ref: sakila.actor.actor_id
      rows: 27
      filtered: 100.00
      Extra: NULL
      *************************** 3. row ***************************
      id: 2
      select_type: DERIVED
      table: film_actor
      partitions: NULL
      type: index
      possible_keys: PRIMARY,idx_fk_film_id
      key: PRIMARY
      key_len: 4
      ref: NULL
      rows: 5462
      filtered: 100.00
      Extra: Using index
      3 rows in set, 1 warning (0.00 sec)
      • 优化Limit查询:limit常用于分页处理,会伴随着order by从句使用,因此大多数时候会使用Filesorts这样会造成大量的IO问题。
        执行结果:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        mysql> select film_id, description from sakila.film order by title limit 50, 5;
        +---------+---------------------------------------------------------------------------------------------------------------------------------+
        | film_id | description |
        +---------+---------------------------------------------------------------------------------------------------------------------------------+
        | 51 | A Insightful Panorama of a Forensic Psychologist And a Mad Cow who must Build a Mad Scientist in The First Manned Space Station |
        | 52 | A Thrilling Documentary of a Composer And a Monkey who must Find a Feminist in California |
        | 53 | A Epic Drama of a Madman And a Cat who must Face a A Shark in An Abandoned Amusement Park |
        | 54 | A Awe-Inspiring Drama of a Car And a Pastry Chef who must Chase a Crocodile in The First Manned Space Station |
        | 55 | A Awe-Inspiring Story of a Feminist And a Cat who must Conquer a Dog in A Monastery |
        +---------+---------------------------------------------------------------------------------------------------------------------------------+
        5 rows in set (0.00 sec)

        执行计划:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        mysql> explain select film_id, description from sakila.film order by title limit 50, 5\G
        *************************** 1. row ***************************
        id: 1
        select_type: SIMPLE
        table: film
        partitions: NULL
        type: ALL
        possible_keys: NULL
        key: NULL
        key_len: NULL
        ref: NULL
        rows: 1000
        filtered: 100.00
        Extra: Using filesort
        1 row in set, 1 warning (0.00 sec)

        可以看到使用了filesort方式,需要优化。

        • 使用有索引的列或者主键进行order by操作

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          mysql> explain select film_id, description from sakila.film order by film_id limit 50, 5\G
          *************************** 1. row ***************************
          id: 1
          select_type: SIMPLE
          table: film
          partitions: NULL
          type: index
          possible_keys: NULL
          key: PRIMARY
          key_len: 2
          ref: NULL
          rows: 55
          filtered: 100.00
          Extra: NULL
          1 row in set, 1 warning (0.00 sec)
        • 记录上次返回的主键,在下一次查询时使用主键过滤

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          mysql> explain select film_id, description from sakila.film where film_id>55 and film_id<=60 order by film_id limit 1, 5\G
          *************************** 1. row ***************************
          id: 1
          select_type: SIMPLE
          table: film
          partitions: NULL
          type: range
          possible_keys: PRIMARY
          key: PRIMARY
          key_len: 2
          ref: NULL
          rows: 5
          filtered: 100.00
          Extra: Using where
          1 row in set, 1 warning (0.00 sec)

索引优化

如何选择合适的列创建索引

  1. where从句,group by从句,order by从句,on从句中出现的列
  2. 索引字段越小越好
  3. 离散度大的列放到索引列的前面:下面的例子中,customer_id需要放在staff_id前面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mysql> desc payment;
+--------------+----------------------+------+-----+-------------------+-----------------------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+----------------------+------+-----+-------------------+-----------------------------+
| payment_id | smallint(5) unsigned | NO | PRI | NULL | auto_increment |
| customer_id | smallint(5) unsigned | NO | MUL | NULL | |
| staff_id | tinyint(3) unsigned | NO | MUL | NULL | |
| rental_id | int(11) | YES | MUL | NULL | |
| amount | decimal(5,2) | NO | | NULL | |
| payment_date | datetime | NO | MUL | NULL | |
| last_update | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+--------------+----------------------+------+-----+-------------------+-----------------------------+
7 rows in set (0.00 sec)
mysql> select count(distinct customer_id), count(distinct staff_id) from payment;
+-----------------------------+--------------------------+
| count(distinct customer_id) | count(distinct staff_id) |
+-----------------------------+--------------------------+
| 599 | 2 |
+-----------------------------+--------------------------+
1 row in set (0.01 sec)

索引的维护及优化–重复及冗余索引

  1. 重复索引是指相同的列以相同的顺序建立的同类型的索引。如:

  2. 冗余索引指的是多个索引的前缀列相同,或是在联合索引中包含了主键。

  3. 查询方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    mysql> select a.table_schema as '数据名',
    a.table_name as '表名',
    a.index_name as '索引1',
    a.column_name as '重复列名'
    from statistics a join statistics b
    on a.table_schema=b.table_schema
    and a.table_name = b.table_name
    and a.seq_in_index = b.seq_in_index
    and a.column_name = b. column_name
    where a.seq_in_index=1 and a.index_name <> b.index_name\G
  4. 删除不使用的索引,MySQL只能查询慢查日志来分析。

数据库结构优化

选择合适的数据类型

  1. 使用可以存下你的数据的最小的数据类型

  2. 使用简单的数据类型,int要比varcahr类型在MySQL上面处理简单。

  3. 尽可能的使用not null定义字段。

  4. 尽量少用text类型,非用不可时最好考虑分表。

表的范式化和反范式话

  1. 范式话是指数据库设计的规范,目前说的范式话一般是指第三设计范式,也就是要求数据表中不存在非关键字段对任意候选关键字段的函数依赖则符合第三范式。

    • 不符合范式的问题:解决方法为对表进行拆分。

      • 数据冗余
      • 数据的插入异常
      • 数据的更新异常
      • 数据的删除异常
  2. 反范式化是指为了查询效率的考虑把原来符合第三范式的表的适当增加冗余,以达到优化查询效率的目的,反范式化是一种以空间来换取时间的操作。

表的垂直拆分

垂直拆分指的是把原有一个很多列的表拆分成多个表,这解决了表的宽度问题。拆分原则如下:

  1. 不常用字段放入到一个表中
  2. 把大字段独立存放到一个表中
  3. 经常一起使用的字段放到一个表中

表的水平拆分

水平拆分是为了解决表的数据量过大的问题,水平拆分的表每一个表的结构都是完全一致的。

常用拆分方法为:对id进行hash运算,如果要拆分为100个子表,则对100取余,根据不同的结果将其存放到对应的表中。存在的挑战包括跨分区表进行数据查询,统计及后台报表操作。

系统配置优化

操作系统配置优化

数据库是基于操作系统的,目前大多数MySQL都是安装在Linux上,所以对于操作系统的一些参数配置也会影响到MySQL的性能。

  1. tcp支持的队列数
  2. 减少断开连接时,资源回收
  3. 打开文件数的限制,ulimit -a查询各位限制。
  4. 关闭iptables,selinux等防火墙

Mysql配置文件优化

MySQL可以通过启动时指定配置参数和使用配置文件两种方式进行配置,在大多数情况下,配置文件位于/etc/my.cnf或是/etc/mysql/my.cnf。如果存在多个位置存在配置文件,后面的会对前面的进行覆盖。

  • innodb_buffer_pool_size:配置Innodb的缓冲池,如果数据库中只有Innodb表,则推荐配置量为总内存的75%。

  • innodb_buffer_pool_instance:可以控制缓冲池的个数,默认一个

  • innodb_log_buffer_size:Innodb log 缓冲的大小,由于日志最长每秒刷新一次,一般不大

  • innodb_flush_log_at_trx_commit:关键参数,数据库多久将变更写入磁盘,对Innodb的IO效率影响很大。默认为1,每次提交都会将变更刷新到磁盘。0每次提交不刷新,每1s刷新到磁盘一次。2将每次提交刷新到缓冲区,每1s将缓冲区刷新到磁盘。一般设置为2。

  • innodb_read_io_threads,innodb_write_io_threads:Innodb读写的IO进程数,默认为4

  • innode_file_per_table:关键参数,控制Innodb每一个表使用独立的表空间。默认为OFF,也就是所有的表都会建立在共享表空间中。

  • innodb_stats_on_metadata:决定MySQL什么情况下会刷新innodb表的统计信息。

第三方配置工具,Percona Tools

服务器硬件优化

  1. CPU:单核频率更高,MySQL对CPU服务器
  2. 磁盘IO:RAID

    • RAID0:条带,多个磁盘链接成一个硬盘使用,IO最好,但是一个损坏,则所有数据丢失
    • RAID1:镜像,至少两个磁盘, 每组磁盘存储的数据相同。
    • RAID5:多个硬盘合并成一个逻辑盘使用,数据读写时会建立奇偶校验信息,并且奇偶校验信息和相对应的数据分别存储于不同的磁盘上。当RAID5的一个磁盘数据发生损坏后,利用剩下的数据的相应的奇偶校验信息取恢复被损坏的数据。
    • RAID1+0:RAID0和RAID1结合使用,数据库建议使用这个级别。
  3. SNA和NAT是否使用数据库:

    • 常用于高可用解决方案
    • 顺序读写效率很高,但是随机读写很差
    • 数据库随机读写的比率很高

Redis-redis基础总结

发表于 2019-04-21 | 分类于 Java , redis

redis简介

redis是一个开源的,C语言编写的、支持网络交互的,可基于内存也可以持久化的key-value数据库,是当前最为热门的非关系型数据库。其官网为redis.io。

redis的安装

  1. 下载redis源码并解压缩
  2. 进入redis源码目录,make编译一下。
  3. make install PREFIX=/usr/local/redis

安装完毕,其目录如下:

./redis-benchmark //用于进行redis性能测试的工具
./redis-check-dump //用于修复出问题的dump.rdb文件
./redis-cli //redis的客户端
./redis-server //redis的服务端
./redis-check-aof //用于修复出问题的AOF文件
./redis-sentinel //用于集群管理

redis的使用

./redis-server redis.conf 可指定配置文件并启动redis服务,默认端口号为6379。可以通过redis-cli或者可视化的客户端如redis management来远程登录操作服务器。

redis的五种数据类型

  1. String

字符串是redis最基本的数据类型,一个key对应一个value.String类型是二进制安全的。也就是说redis的String可以包含任何数据。比如JPG图片或者序列化的对象。String类型是redis最基本的数据类型,一个redis中字符串value最多可以是512M。

基本操作包括get、set、incr、decr

1
2
3
4
5
6
7
8
127.0.0.1:6379> set mynum "2"
OK
127.0.0.1:6379> get mynum
"2"
127.0.0.1:6379> incr mynum
(integer) 3
127.0.0.1:6379> get mynum
"3"

考虑到INCR等指令本身就具有原子操作的特性,可以利用redis的INCR,DECR等指令来实现原子计数,用这个特性来实现业务上的统计技术需求。

  1. Hash

Hash是一个键值对集合,相当于一个key对应一个map,map中还有键值对。使用hash对key进行归类。

基本操作包括hset、hget。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> HMSET user:001 username antirez password P1pp0 age 34
OK
127.0.0.1:6379> HGETALL user:001
1) "username"
2) "antirez"
3) "password"
4) "P1pp0"
5) "age"
6) "34"
127.0.0.1:6379> HSET user:001 password 12345
(integer) 0
127.0.0.1:6379> HGETALL user:001
1) "username"
2) "antirez"
3) "password"
4) "12345"
5) "age"
6) "34"
  1. List

列表在低层的实现为链表,则其插入和删除的效率非常高。其存在的缺点在于链表中元素的定位比较慢。

其基本操作包括lpush,rpush,lrange等,也就是从左侧插入,右侧插入以及指定一个范围来提取元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> lpush mylist "1"
(integer) 1
127.0.0.1:6379> rpush mylist "2"
(integer) 2
127.0.0.1:6379> lpush mylist "0"
(integer) 3
127.0.0.1:6379> lrange mylist 0 1
1) "0"
2) "1"
127.0.0.1:6379> lrange mylist 0 -1
1) "0"
2) "1"
3) "2

list的使用场景包括:

  • 利用lists来实现一个消息队列,可以确保消息的顺序执行。
  • 利用LRANGE实现分页功能
  1. Set

集合和Java中的集合定义类似,无序,不重合。可以对集合进行添加,删除,取交集,取并集,取差集等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
127.0.0.1:6379> sadd myset "one"
(integer) 1
127.0.0.1:6379> sadd myset "two"
(integer) 1
127.0.0.1:6379> smembers myset
1) "one"
2) "two"
127.0.0.1:6379> sismember myset "one"
(integer) 1
127.0.0.1:6379> sismember myset "three"
(integer) 0
127.0.0.1:6379> sadd yourset "1"
(integer) 1
127.0.0.1:6379> sadd yourset "2"
(integer) 1
127.0.0.1:6379> smembers yourset
1) "1"
2) "2"
127.0.0.1:6379> sunion myset yourset
1) "1"
2) "one"
3) "2"
4) "two"
  1. zset

有序集合,每个元素都关联一个score,据此来进行排序。常用操作有zrange,zadd,zreevrange,zrangebyscore等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
127.0.0.1:6379> zadd myzset 1 baidu.com
(integer) 1
127.0.0.1:6379> zadd myzset 3 360.com
(integer) 1
127.0.0.1:6379> zadd myzset 2 google.com
(integer) 1
127.0.0.1:6379> zrange myzset 0 -1 with scores
1) "baidu.com"
2) "1"
3) "google.com"
4) "2"
5) "360.com"
6) "3"
127.0.0.1:6379> zrange myzset 0 -1
1) "baidu.com"
2) "google.com"
3) "360.com"

redis持久化

redis提供了两种持久化方案:RDB和AOF。

RDB-Redis Database

  • rdb是将redis某一个时刻的数据持久化到磁盘中,是一种快照式的持久化方法。redis在持久化过程中,会将数据都写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的dump.rdb。保证快照文件试完整可用的,可以随时进行备份。

  • RDB进行持久化过程时,redis会单独fork一个子进程来进行持久化,而主进程不会进行任何的IO操作,从而保证了redis的高性能。

    • fork的作用是复制一个和当前进程一样的进程。新进程的所有数据数值和原进程一致,但是是一个全新的进程,并作为原进程的子进程。
  • RDB快照的触发方法

    • 配置文件中默认的快照配置,cp后重新使用。
    • 命令save、bgsave,其中save时只保存,其他全部阻塞,bgsave会在后台进行快照操作,快照同时还可以响应客户端的请求,可以通过lastsave命令获取最后一次成功执行快照的时间。
    • 使用flushall命令,不过里面是空的,无意义。
  • RDB的优势和劣势

    • 优势:如果需要进行大规模的数据恢复,且对于数据恢复的完整性不是非常敏感,nameRDB方式要比AOF方式更加的高效。
    • 劣势:RDB的缺点在于如果对于数据完整性要求比较高,则当redis故障时,会丢失掉最后一次快照后的所有数据。而如果持久化频率太高,会增加服务器的多余压力。同时fork时,内存中的数据被复制了一份,内存膨胀需要考虑。

AOF-Append Only File

  • AOF,只允许对文件追加。AOF采用的持久化方案是将执行过的写指令记录下来,在数据恢复时按照从前到后的顺序将指令都执行一遍。AOF的开启可以通过修改redis.conf中的appendonly yes来设置。此时,如果有写操作,都会追加到AOF文件的末尾。默认的AOF持久化策略为每秒种fsync一次,由于向磁盘中写一个指令对系统的压力很小,因此此时redis保持高效运行。而redis重启时根据日志文件顺次执行其指令来完成数据恢复。

  • redis出现故障,丢失的数据也就是这1s没有被同步的数据。同时在追加日志时,恰好遇到磁盘空间满,断电等情况导致日志写入不完整,此时redis提供了redis-check-aof工具来修复日志。

    如果AOF文件出现了被写坏的情况,可以通过以下步骤来修复:

    * 备份被写坏的AOF文件
    * 运行redis-check-aof-fix进行修复
    * 用diff -u来查看两个文件的差异,确认问题点
    * 重启redis,加载修改后的AOF文件
    
  • 重写机制:bgrewriteaof

    • 追加方式会导致AOF文件越来越大,因此redis提供了AOF文件重写机制,也就是当AOF文件的大小超过所设定的阈值时,redis会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。比如,如果我们队一个字符串的值进行了1000次INCR,AOF中应该记录1000条指令,这时候我们完全可以用一个set的值,将其设置为正确的数据。同时AOF重写过程中,仍然是采用先写临时文件,全部完成后再替换的流程,所以断电、磁盘满等问题都不会影响AOF文件的可用性。

    • 重写原理:

      • 重写时会fork一个新进程来将文件进行重写,先写临时文件然后rename覆盖。重写过程为遍历此时内存中的数据,每条数据有一条对应的set语句。重写AOF文件的操作并不是读取旧的AOF文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的AOF文件,类似于快照。
    • 重写触发条件:

      • redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后的大小的一倍且文件大于64M时触发。
  • AOF的优势和劣势

    • AOF的优势:

      • 每秒同步,异步操作,每秒记录,如果1s以内的宕机,有数据丢失
      • 不需要同步
    • AOF的缺陷主要有两个方面:

      • 同样数据规模的情况下,AOF文件要比RDB文件的体积大。
      • AOF方式的恢复速度也要慢于RDB方式。

选择方案

RDB持久化方式能够在指定时间间隔内对你的数据进行快照存储。而AOF持久化方式能记录每次对服务器写的操作,当服务器重启时重新执行这些命令来回复原始数据,AOF命令以redis协议追加保存每次写的操作到文件末尾。Redis还能对AOF文件进行后台重写,使得AOF文件不至于过大。

  • 两种方式都开启来提供更加可靠的持久化方案

这种情况下,当redis重启时会优先载入AOF文件来回复原始的数据,因为在通常的情况下AOF文件保存的数据比RDB文件要更加完整。而RDB作为候选方案,对当前内存的数据进行定期的备份,来防范AOF可能存在的BUG。

  • 性能建议

考虑到RDB做数据备份,建议在Slave上持久化RDB文件。

如果这时候Enable AOF,好处是最坏的情况下丢失的数据也不会超过2s,启动脚本简单的load自己的AOF文件就可以看,代价是带来了持续的IO,以及rewrite过程中将产生的新数据写到新文件造成的阻塞难以避免。在磁盘允许的情况下,应该尽量减少AOF重写的频率。

如果不Enable AOF,仅仅依靠主从复制来实现高可靠也是可以的。这样能节省一大笔IO操作,也减少了rewrite时带来的系统资源占用,但是如果master和slave同时坏掉,会丢失十几分钟的数据,启动脚本要比较Master、Slave中的RDB文件,载入比较新的哪一个。

redis的主从复制

redis支持主从同步的,同时支持一主多从以及多级从结构。主从结构的优点在于:

  • 冗余备份
  • 提升读性能。

redis主从结构中,一般推荐关闭主服务器的数据持久化功能,只让从服务器进行数据持久化,这样可以提高主服务器的处理性能,。。

主从架构中,从服务器通常设置为只读模式,这样可以避免从服务器的数据被误修改。但是从服务器仍然可以接受config等指令,不应该将从服务器直接暴露到不安全的网络环境中。如果必须如此,需要考虑给其指令进行重命名,避免误操作。

主从同步的配置方法

  1. 配置的是Slave库。
  2. 从库配置:slaveof 主库ip 主库port, 每次与master断开之后,都需要进行重新连接,除非你配置进redis.conf中的info replication。
  3. 修改配置文件的细节操作:
    • 拷贝多个redis.conf文件
    • 开启daemonize yes
    • pid文件名字
    • 指定端口
    • log文件名称
    • dump.rdb文件名称。

一主二仆的配置

  • 初始化启动一个Master和两个Slave
  • 查看日志包括主机,备份机的日志,以及info replication
  • 上一个Slave可以是下一个Slave的Master,Slave同样可以接受其他Slave的连接和同步请求,那么该Slave作为了链条中的下一个的Master,可以有效减轻master的写压力。
  • 中途变更转向时会清除之前的数据,重新拷贝最新的slaveof ip port
  • slaveof no one, 使得当前的数据库停止与其他数据库的同步,转成主数据库。

主从复制原理

  1. Slave启动成功连接到master之后会发送一个sync命令。
  2. Master接到命令后,调用bgsave指令来创建一个子进程专门进行数据持久化工作,也就是讲主服务器的数据写入RDB文件中。在数据持久化过程中,主服务器将执行的写指令都缓存在内存中。
  3. bgsave指令完成之后,主服务器会将持久化好的RDB文件发送给服务器,从服务器接到此文件后会将其存储到磁盘上,然后再将读取到内存中。这个动作完成之后,主服务器会将这段时间缓存的写指令以redis协议的格式发送给从服务器。也就是全量复制和增量复制结合使用。
  • 全量复制与增量复制
    • 全量复制:slave服务在接收到数据库文件数据之后,将其存盘并加载到内存中。
    • 增量复制:master继续将新的收集到的修改命令依次传给slave,完成同步。
  • 多个从服务器同时发来SYNC指令,主服务器也只会执行一个bgsave,然后把持久化好的RDB文件发送给多个下游节点。redis2.8之前,从服务器和主服务器断开连接之后,都会进行一次主从之间的全量数据同步,2.8之后,redis支持效率更高的增量同步策略。

  • 主服务器会在内存中维护一个缓冲区,缓冲区中存储着将要发给从服务器的内容。从服务器在与主服务器出现网络瞬间断开之后,从服务器会尝试与主服务器连接,一旦连接成功,从服务器就会把希望同步的主服务器ID和希望请求的数据偏移量位置发送给主服务器。主服务器接收到这种同步请求之后,首先会验证主服务器ID是否和自己的ID匹配,其次会检查请求的偏移量是否存在于自己的缓冲区中,如果两者都满足的话,主服务器会向服务器发送增量内容。

redis事务管理

redis事务处理的基础为MULTI、EXEC、DISCARD、WATCH四个指令。

MULTI:用来组装一个事务,标记事务的开始
EXEC:用来执行一个事务
DISCARD:用来取消一个事务
WATCH:用来监视一些可以,一旦这些key在事务执行前被改变,则取消事务的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
redis> MULTI
OK
redis> INCR user_id
QUEUED
redis> INCR user_id
QUEUED
redis> INCR user_id
QUEUED
redis> PING
QUEUED
redis> EXEC
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG
  • 其中QUEUED表示我们在用MULTI组装事务时,每一个命令都会进入到内存队列中缓存起来,如果出现QUEUED则表示我们这个命令成功的插入了缓存队列,将来执行EXEC时,这些被QUEUED的命令都会被组装成一个事务来执行。

  • 事务执行完成之后,如果redis开启了AOF持久化的话,那么一旦事务被成功执行,事务中的命令就会通过write命令一次性写入到磁盘中,如果写入过程中出现故障则会造成部分命令AOF持久化,此时可以使用redis-check-aof工具来修复这个问题,这个问题会将AOF文件中不完整的信息溢出,确保AOF文件完整可用。

  • 事务遇到的错误分为:

    • 调用EXEC之前的错误:导致某个命令无法成功写入缓冲队列,调用redis会拒绝执行这个事务。

      • 语法错误
      • 内存不足

        1
        2
        3
        4
        5
        6
        7
        8
        127.0.0.1:6379> multi
        OK
        127.0.0.1:6379> haha
        (error) ERR unknown command 'haha'
        127.0.0.1:6379> ping
        QUEUED
        127.0.0.1:6379> exec
        (error) EXECABORT Transaction discarded because of previous errors.
    • 调用EXEC之后的错误:redis会忽略这些错误,而是继续向下执行事务中的其他命令。也就是某一条命令失败并不会影响其他命令的执行。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      127.0.0.1:6379> multi
      OK
      127.0.0.1:6379> set age 23
      QUEUED
      127.0.0.1:6379> sadd age 15
      QUEUED
      127.0.0.1:6379> set age 29
      QUEUED
      127.0.0.1:6379> exec
      1) OK
      2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
      3) OK
      127.0.0.1:6379> get age
      "29"
    • WATCH指令可以使用类似于乐观锁的效果也就是CAS,WATCH本身作用是监视某些key是否被改动过,只要还没有真正的触发事务,WATCH都会监视指定的key,一旦发现某个key被修改了,在执行EXEC时就会返回nil,表示事务无法触发。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    127.0.0.1:6379> set age 23
    OK
    127.0.0.1:6379> watch age
    OK
    127.0.0.1:6379> set age 24
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set age 25
    QUEUED
    127.0.0.1:6379> get age
    QUEUED
    127.0.0.1:6379> exec
    (nil)

mybatis总结

发表于 2019-04-21 | 分类于 Java , mybatis

一、mybatis简介

MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。 MyBatis是一个优秀的持久层框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建connection、创建statement、手动设置参数、结果集检索等jdbc繁杂的过程代码。
Mybatis通过xml或注解的方式将要执行的各种statement(statement、preparedStatement、CallableStatement)配置起来,并通过java对象和statement中的sql进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射成java对象并返回。

二、mybatis入门

1、使用jdbc操作数据库存在的问题

  • JDBC编程步骤:

    1. 加载数据库驱动

    2. 创建并获取数据库连接

    3. 创建jdbc statement对象

    4. 设置SQL语句

    5. 传递SQL语句参数

    6. 执行statement获取结果

    7. 解析SQL执行结果

    8. 释放资源

  • jdbc程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package cn.itheima.jdbc;
import java.sql.*;
public class JDBCTest {
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
//加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
//通过驱动管理类获取数据库链接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "linux");
//定义sql语句 ?表示占位符
String sql = "select * from user where username = ?";
//获取预处理statement
preparedStatement = connection.prepareStatement(sql);
//设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
preparedStatement.setString(1, "王五");
//向数据库发出sql执行查询,查询出结果集
resultSet = preparedStatement.executeQuery();
//遍历查询结果集
while(resultSet.next()){
System.out.println(resultSet.getString("id")+" "+resultSet.getString("username"));
}
} catch (Exception e) {
e.printStackTrace();
}finally{
//释放资源
if(resultSet!=null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(preparedStatement!=null){
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
  • jdbc存在的问题

    1. 数据库连接创建,释放频繁造成系统资源浪费从而影响系统性能,可以使用数据库连接池技术来解决这个问题。

    2. SQL语句在代码中硬编码,不便于维护,实际应用中修改SQL语句需要修改java代码,不便于修改。

    3. 使用preparedStatement语句向占位符传参存在硬编码,实际上SQL语句的where条件不固定,修改困难

    4. 对结果集存在硬编码(查询列名),SQL变化导致解析代码变化,系统不易维护,将数据库记录封装成pojo对象解析比较方便。

2、mybatis架构

Alt text

  1. mybatis配置

    • SqlMapConfig.xml为mybatis的全局配置文件,配置了mybatis的运行环境等信息。
    • mapper.xml为SQL映射文件,文件中配置了操作数据库的SQL语句,需要加载在SQLMapConfig.xml文件中。
  2. 通过mybatis环境等配置信息构造SqlSessioNFactory,也就是会话工厂。

  3. 由会话工厂创建SqlSession,操作数据库需要通过会话来实现。

  4. mybatis低层自定义了Executor执行器操作数据库,Execotor接口有两个实现,一个是基本执行器,一个是缓存执行器。

  5. MappedStatement也是mybatis的一个低层封装对象,它包装了mybatis配置信息及SQL映射信息等。mapper.xml文件中一个SQL对应一个MappedStatement对象,SQL的id即MappedStatement的id。

  6. MappedStatement对sql执行输入参数进行定义,包括HashMap、基本类型、pojo,Executor通过Mapped Statement在执行sql前将输入的java对象映射至sql中,输入参数映射就是jdbc编程中对preparedStatement设置参数。

  7. MappedStatement对sql执行输出结果进行定义,包括HashMap、基本类型、pojo,Executor通过Mapped Statement在执行sql后将输出结果映射至java对象中,输出结果映射过程相当于jdbc编程中对结果的解析处理过程。

3、mybatis的入门程序

1、mybatis下载

mybatis的代码由GitHub管理,地址为:https://github.com/mybatis/mybatis-3/releases

文件结构为:

Alt text

2、实现的功能

  • 根据用户id查询一个用户信息

  • 根据用户名称模糊查询用户信息列表

  • 添加用户

  • 更新用户

  • 删除用户

3、工程搭建

  1. 使用IDEA 2017.2 创建Java工程,jdk为1.8.0_141

  2. 添加jar包,包括mybatis核心包,依赖包,数据库驱动包

    Alt text

  3. 日志的打印:mybatis默认使用log4j输出日志信息,在classpath下创建log4j.properties:

    1
    2
    3
    4
    5
    6
    # Global logging configuration
    log4j.rootLogger=DEBUG, stdout
    # Console output...
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
  4. SqlMapConfig.xml文件:配置了数据源,事务管理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
    <!-- 和spring整合后 environments配置将废除-->
    <environments default="development">
    <environment id="development">
    <!-- 使用jdbc事务管理-->
    <transactionManager type="JDBC" />
    <!-- 数据库连接池-->
    <dataSource type="POOLED">
    <property name="driver" value="${jdbc.driver}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
    </dataSource>
    </environment>
    </environments>
    </configuration>
  5. po类的编写:po类作为mybatis进行SQL映射使用,po类通常与数据表对应,User.java如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package cn.itheima.pojo;
    import java.util.Date;
    import java.util.List;
    public class User {
    private int id;
    private String username;// 用户姓名
    private String sex;// 性别
    private Date birthday;// 生日
    private String address;// 地址
    //omit the getter and setter...
    }
  6. 编写SQL映射文件:User.xml, namespace用于隔离SQL语句

    1
    2
    3
    4
    5
    6
    7
    8
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <!--命名空间的作用为SQL隔离-->
    <mapper namespace="test">
    </mapper>
  7. 加载映射文件,将User.xml添加在SQLMapConfig.xml中:

    1
    2
    3
    <mappers>
    <mapper resource="User.xml"/>
    </mappers>

4、根据id查询用户信息

  1. 在user.xml中添加SQL语句:

    1
    2
    3
    4
    5
    6
    7
    8
    <!--根据id获取用户信息-->
    <!--parameterType指定传入参数类型,
    resultMap指定结果集类型
    #{}为占位符,如果传入的是基本类型,#{}中的变量名称可以随意指定
    -->
    <select id="findUserById" parameterType="java.lang.Integer" resultType="cn.itheima.pojo.User">
    select * from user where id = #{id}
    </select>
  2. 测试程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    private SqlSessionFactory sqlSessionFactory;
    @Before
    public void createSqlSessionFactory() throws IOException {
    //配置文件
    String resource = "SqlMapConfig.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    //使用SqlSessionFactoryBuilder从XML配置文件中创建
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }
    @Test
    public void testFindUserById() {
    SqlSession sqlSession = null;
    try {
    //创建会话
    sqlSession = sqlSessionFactory.openSession();
    //第一个参数,namespace+id
    User user = sqlSession.selectOne("test.findUserById", 1);
    System.out.println(user);
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    if (sqlSession != null) {
    sqlSession.close();
    }
    }
    }

5、根据用户名查询用户信息

  1. 在url.xml中添加SQL语句:

    1
    2
    3
    4
    5
    6
    <!--如果返回结果为集合,则可以调用session中的selectList方法,resultType仍然为-->
    <!--${}拼接符,如果传入的是基本类型,其内部的变量名称必须为value-->
    <!--拼接符有SQL注入的风险,慎重使用,-->
    <select id="findUserByUserName" parameterType="java.lang.String" resultType="cn.itheima.pojo.User">
    select * from user where username like '%${value}%'
    </select>
  2. 测试程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    private SqlSessionFactory sqlSessionFactory;
    @Before
    public void createSqlSessionFactory() throws IOException {
    //配置文件
    String resource = "SqlMapConfig.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    //使用SqlSessionFactoryBuilder从XML配置文件中创建
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }
    @Test
    public void testFindUserByUserName() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    List<User> users = sqlSession.selectList("test.findUserByUserName", "王");
    for (User user : users) {
    System.out.println(user);
    }
    }

6、概念比较

  1. #{}和${}

    • #{}表示一个占位符号,通过#{}可以实现preparedStatement向占位符中设置值,自动进行java类型和jdbc类型转换,#{}可以有效防止SQL注入。#{}可以接收简单类型值或pojo属性值。如果parameterType传输单个简单类型值,#{}括号中可以是value或其他属性。

    • ${}表示拼接字符串,通过${}可以将parameterType 传入的内容拼接在sql中且不进行jdbc类型转换, ${}可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值,${}括号中只能是value。

  2. parameterType和resultType

    • parameterType:指定输入参数类型,mybatis通过OGNL表达式从对象中获取参数值拼接在SQL中。

    • resultType:指定输出结果类型,mybatis将SQL查询结果的一行记录数据映射为resultType指定类型的对象。

  3. selectOne和selectList

    • selectOne:查询一条记录,使用其查询多条记录会抛出异常

    • selectList:查询一条或者多条记录

7、添加用户

  1. 在SQLMapConfig.xml中添加:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!--如果从传入的是pojo类型,name#{}中对应的属.属性-->
    <!--如果要返回数据库自增组件,可以使用select last_insert_id()函数-->
    <insert id="insertUser" parameterType="cn.itheima.pojo.User">
    <!--执行数据库函数,返回最近增加的自增主键id
    keyProperty:将返回的主键放入传入的参数的ID中保存
    order:当前函数相对于insert语句的执行顺序
    resultType:keyProperty中的数据的类型
    -->
    <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer">
    select last_insert_id()
    </selectKey>
    insert into user (username, birthday, sex, address) values (#{username}, #{birthday}, #{sex}, #{address})
    </insert>
  2. 测试程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //omit the before
    @Test
    public void testInsertUser() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //创建插入数据库的用户对象
    User user = new User();
    user.setUsername("aaa");
    user.setBirthday(new Date());
    user.setSex("1");
    user.setAddress("北京");
    System.out.println(user);
    //执行SQL语句插入对象
    sqlSession.insert("test.insertUser", user);
    //提交事务,mybatis会自动开启事务,需要手动提交事务
    sqlSession.commit();
    System.out.println(user);
    }
  3. uuid实现主键:AFTER改为BEFORE,则在插入之前生成UUID并保存到数据库中

    1
    2
    3
    4
    5
    6
    7
    8
    <insert id="insertUser" parameterType="cn.itcast.mybatis.po.User">
    <selectKey resultType="java.lang.String" order="BEFORE"
    keyProperty="id">
    select uuid()
    </selectKey>
    insert into user(id,username,birthday,sex,address)
    values(#{id},#{username},#{birthday},#{sex},#{address})
    </insert>

8、删除用户

  1. 在SQLMapConfig.xml中添加:

    1
    2
    3
    <delete id="deleteUserById" parameterType="java.lang.Integer">
    delete from user where id=#{id}
    </delete>
  2. 测试程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //omit the before
    @Test
    public void testDeleteUserById() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //删除
    sqlSession.delete("test.deleteUserById", 28);
    //提交
    sqlSession.commit();
    }

9、修改用户

  1. 在SQLMapConfig.xml中添加:

    1
    2
    3
    <update id="updateUserById" parameterType="cn.itheima.pojo.User">
    update user set username = #{username} where id = #{id}
    </update>
  2. 测试程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Test
    public void testUpdateUserById() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    User user = new User();
    user.setId(29);
    user.setUsername("王麻子");
    sqlSession.update("test.updateUserById", user);
    sqlSession.commit();
    }

10、mybatis解决jdbc编程的问题

  1. 数据库连接创建,释放频繁造成系统资源浪费从而影响系统性能,可以使用数据库连接池技术来解决这个问题。

    解决:在SQLMapConfig.xml中配置数据连接池,使用连接池管理数据库连接

  2. SQL语句在代码中硬编码,不便于维护,实际应用中修改SQL语句需要修改java代码,不便于修改。

    解决:将SQL语句配置在XXmapper.xml文件中与java程序分离

  3. 使用preparedStatement语句向占位符传参存在硬编码,实际上SQL语句的where条件不固定,修改困难

    解决:mybatis自动将java对象映射至SQL语句,通过statement中的parameterType定义输入参数的类型

  4. 对结果集存在硬编码(查询列名),SQL变化导致解析代码变化,系统不易维护,将数据库记录封装成pojo对象解析比较方便。

    解决:mybatis自动将SQL执行结果映射至java对象,通过statement的resultType定义输出结果的类型

11、mybatis和hibernate的区别

  1. Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句,不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句,并将java对象和sql语句映射生成最终执行的sql,最后将sql执行的结果再映射生成java对象。

  2. Mybatis学习门槛低,简单易学,程序员直接编写原生态sql,可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁,一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。

  3. Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件(例如需求固定的定制化软件)如果用hibernate开发可以节省很多代码,提高效率。但是Hibernate的学习门槛高,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间如何权衡,以及怎样用好Hibernate需要具有很强的经验和能力才行。

三、DAO的开发方法

使用mybatis开发DAO,有两种方法,原始DAO方法和Mapper接口开发方法

1、需求

  • 根据用户id查询一个用户信息

  • 根据用户名称模糊查询用户信息列表

  • 添加用户信息

2、SqlSession的使用范围

SqlSession中封装了对数据库的操作,如:查询、插入、更新、删除等。通过SqlSessionFactory创建SqlSession,而SqlSessionFactory是通过SqlSessionFactoryBuilder进行创建。

  1. SqlSessionFactoryBuilder:用于创建SqlSessionFacoty,SqlSessionFacoty一旦创建完成就不需要SqlSessionFactoryBuilder了,因为SqlSession是通过SqlSessionFactory生产,所以可以将SqlSessionFactoryBuilder当成一个工具类使用,最佳使用范围是方法范围即方法体内局部变量。

  2. SqlSessionFactory:SQLSessionFactory是一个接口,接口中定义了openSession的不同重载方法,SqlSessionFactory的最佳使用范围是整个运行期间,一旦创建之后可以重复使用,通常以单例模式管理SqlSessionFactory。

  3. SqlSession: SqlSession是一个面向用户的接口,sqlSession中定义了数据库操作方法。每个线程都应该有它自己的SqlSession实例。SqlSession的实例不能共享使用,它也是线程不安全的。因此最佳的范围是请求或方法范围。绝对不能将SqlSession实例的引用放在一个类的静态字段或实例字段中。打开一个 SqlSession;使用完毕就要关闭它。通常把这个关闭操作放到 finally 块中以确保每次都能执行关闭。

3、原始DAO开发方式(需要编写接口和实现类)

  • 映射文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <!--命名空间的作用为SQL隔离-->
    <mapper namespace="test">
    <!--根据id获取用户信息-->
    <select id="findUserById" parameterType="java.lang.Integer" resultType="cn.itheima.pojo.User">
    select * from user where id = #{id}
    </select>
    <select id="findUserByUserName" parameterType="java.lang.String" resultType="cn.itheima.pojo.User">
    select * from user where username like '%${value}%'
    </select>
    <insert id="insertUser" parameterType="cn.itheima.pojo.User">
    <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer">
    select last_insert_id()
    </selectKey>
    insert into user (username, birthday, sex, address) values (#{username}, #{birthday}, #{sex}, #{address})
    </insert>
    </mapper>
  • Dao接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package cn.itheima.dao;
    import cn.itheima.pojo.User;
    import java.util.List;
    public interface UserDao {
    public User findUserById(Integer id);
    public List<User> findUserByUserName(String username);
    }
  • 实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package cn.itheima.dao;
import cn.itheima.pojo.User;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.Test;
import java.util.List;
public class UserDaoImpl implements UserDao{
private SqlSessionFactory sqlSessionFactory;
//通过构造方法注入
public UserDaoImpl(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public User findUserById(Integer id) {
//sqlSession是县城不安全的,所以他是最佳使用范围在方法体内
SqlSession openSession = sqlSessionFactory.openSession();
User user = openSession.selectOne("test.findUserById", id);
return user;
}
@Override
public List<User> findUserByUserName(String username) {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<User> users = sqlSession.selectList("test.findUserByUserName", username);
return users;
}
}
  • 测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    package test.cn.itheima.dao;
    import cn.itheima.dao.UserDao;
    import cn.itheima.dao.UserDaoImpl;
    import cn.itheima.pojo.User;
    import org.apache.ibatis.io.Resources;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    import org.junit.Test;
    import org.junit.Before;
    import org.junit.After;
    import java.io.InputStream;
    import java.util.List;
    /**
    * UserDaoImpl Tester.
    *
    * @author <Authors name>
    * @since <pre>一月 24, 2018</pre>
    * @version 1.0
    */
    public class UserDaoImplTest {
    private SqlSessionFactory sqlSessionFactory;
    private UserDao userDao;
    @Before
    public void before() throws Exception {
    String resource = "SqlMapConfig.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    userDao = new UserDaoImpl(sqlSessionFactory);
    }
    @After
    public void after() throws Exception {
    }
    /**
    *
    * Method: findUserById(Integer id)
    *
    */
    @Test
    public void testFindUserById() throws Exception {
    User user = userDao.findUserById(1);
    System.out.println(user);
    }
    /**
    *
    * Method: findUserByUserName(String username)
    *
    */
    @Test
    public void testFindUserByUserName() throws Exception {
    List<User> users = userDao.findUserByUserName("王");
    for (User user : users) {
    System.out.println(user);
    }
    }
    }

4、动态代理方法

1、开发规范

Mapper接口开发方法只需要程序员编写Mapper接口,由mybatis框架根据接口定义创建接口的动态代理对象,代理对象的方法体同上班DAO接口实现类方法。Mapper接口需要遵守以下规范:

  1. Mapper.xml文件中的namespace与mapper接口的类路径相同

  2. Mapper接口方法名和Mapper.xml中定义的每个statement的id相同

  3. Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType的类型相同

  4. Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType类型相同。

2、Mapper.xml(映射文件)

定义mapper映射文件UserMapper.xml,需要修改namespace的值为UserMapper接口路径,经UserMapper.xml放在mapper路径下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.itcast.mybatis.mapper.UserMapper">
<!-- 根据id获取用户信息 -->
<select id="findUserById" parameterType="int" resultType="cn.itcast.mybatis.po.User">
select * from user where id = #{id}
</select>
<!-- 自定义条件查询用户列表 -->
<select id="findUserByUsername" parameterType="java.lang.String"
resultType="cn.itcast.mybatis.po.User">
select * from user where username like '%${value}%'
</select>
<!-- 添加用户 -->
<insert id="insertUser" parameterType="cn.itcast.mybatis.po.User">
<selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer">
select LAST_INSERT_ID()
</selectKey>
insert into user(username,birthday,sex,address)
values(#{username},#{birthday},#{sex},#{address})
</insert>
</mapper>

3、Mapper.java(接口文件)

1
2
3
4
5
6
7
8
Public interface UserMapper {
//根据用户id查询用户信息
public User findUserById(int id) throws Exception;
//查询用户列表
public List<User> findUserByUsername(String username) throws Exception;
//添加用户信息
public void insertUser(User user)throws Exception;
}

接口定义有如下特点:

  1. Mapper接口名和Mapper.xml中定义的statement的id相同

  2. Mapper接口方法的输入参数类型和mapper.xml中定义的statement的parameterType的类型相同。

  3. Mapper接口方法的输出参数类型和mapper.xml中定义的statement的resultType的类型相同

4、在SQLMapConfig.xml中配置映射文件

1
2
3
4
<!-- 加载映射文件 -->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>

5、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Public class UserMapperTest extends TestCase {
private SqlSessionFactory sqlSessionFactory;
protected void setUp() throws Exception {
//mybatis配置文件
String resource = "sqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//使用SqlSessionFactoryBuilder创建sessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
Public void testFindUserById() throws Exception {
//获取session
SqlSession session = sqlSessionFactory.openSession();
//获取mapper接口的代理对象
UserMapper userMapper = session.getMapper(UserMapper.class);
//调用代理对象方法
User user = userMapper.findUserById(1);
System.out.println(user);
//关闭session
session.close();
}
@Test
public void testFindUserByUsername() throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> list = userMapper.findUserByUsername("张");
System.out.println(list.size());
}
Public void testInsertUser() throws Exception {
//获取session
SqlSession session = sqlSessionFactory.openSession();
//获取mapper接口的代理对象
UserMapper userMapper = session.getMapper(UserMapper.class);
//要添加的数据
User user = new User();
user.setUsername("张三");
user.setBirthday(new Date());
user.setSex("1");
user.setAddress("北京市");
//通过mapper接口添加用户
userMapper.insertUser(user);
//提交
session.commit();
//关闭session
session.close();
}
}

6、小结

  1. selectOne和selectList:动态代理对象调用sqlSession.selectOne()和sqlSession.selectList()是根据mapper接口的返回值决定,如果返回list则调用selectList方法,如果返回单个对象则调用selectOne方法。

  2. namespace:mybatis官方推荐使用mapper代理方法开发mapper接口,程序员不用编写mapper接口实现类,使用mapper代理方法时,输入参数可以使用pojo包装对象或map对象,保证dao的通用性。

四、SqlMapConfig.xml文件说明

1、配置内容:

  • properties(属性)

  • settings(全局配置参数)

  • typeAliases(类型别名)

  • typeHandlers(类型处理器)

  • objectFactory(对象工厂)

  • plugins(插件)

  • environments(环境集合属性对象)

  • environment(环境子属性对象)

  • transactionManager(事务管理)

  • dataSource(数据源)

  • mappers(映射器)

2、properties(属性)

SqlMapConfig.xml可以引用java属性文件中的配置信息,创建db.properties保存数据库属性:

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8
jdbc.username=root
jdbc.password=linux

修改SqlMapConfig.xml中引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<configuration>
<!--加载db.properties中的参数-->
<properties resource="db.properties"/>
<!-- 和spring整合后 environments配置将废除-->
<environments default="development">
<environment id="development">
<!-- 使用jdbc事务管理-->
<transactionManager type="JDBC" />
<!-- 数据库连接池-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>
</environments>
</configuration>

其属性读取顺序为:

  1. 在properties元素体内定义的属性会首先被读取
  2. 然后会读取properties元素中的resource或url加载的属性,它会覆盖已经读取的同名属性。

3、typeAliases(类型别名)

1、mybatis支持别名:

别名 映射的类型
_byte byte
_long long
_short short
_int int
_integer int
_double double
_float float
_boolean boolean
string String
byte Byte
long Long
short Short
int Integer
integer Integer
double Double
float Float
boolean Boolean
date Date
decimal BigDecimal
bigdecimal BigDecimal
map Map

2、自定义别名:在SqlMapConfig.xml中配置:

1
2
3
4
5
6
7
<!--别名-->
<typeAliases>
<!--定义单个pojo类别名 type 类的全路径名, alias别名-->
<typeAlias type="cn.itheima.pojo.User" alias="user"/>
<!--使用包扫描的方式数量定义别名,定以后别名等于类名,不区分大小写,建议按照Java命名规则来-->
<package name="cn.itheima.pojo"/>
</typeAliases>

4、Mappers(映射器)

  1. : 使用相对于类路径的资源,如:

  2. :使用mapper接口类路径,如:

注意:此种方法要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。

  1. :注册指定包下的所有mapper接口,如:

注意:此种方法要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中。

5、传递pojo包装类内容(输入类型的一种)

开发中通过pojo传递查询条件,查询条件是综合的查询条件,不仅包括用户查询条件,还包括其它的查询条件(比如将用户购买商品信息也作为查询条件),这时可以使用包装对象传递输入参数。Pojo类中包含pojo。

需求:根据用户名查询用户信息,查询条件放到QueryVo的user属性中。

  1. QueryVo:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package cn.itheima.pojo;
    import java.util.List;
    public class QueryVo {
    private User user;
    private List<Integer> ids;
    public List<Integer> getIds() {
    return ids;
    }
    public void setIds(List<Integer> ids) {
    this.ids = ids;
    }
    public User getUser() {
    return user;
    }
    public void setUser(User user) {
    this.user = user;
    }
    }
  2. sql语句

    select * from user where username like '%王%'

  3. Mapper文件

    1
    2
    3
    <select id="findUserByVo" parameterType="cn.itheima.pojo.QueryVo" resultType="cn.itheima.pojo.User">
    select * from user where username LIKE '%${user.username}%' AND sex=#{user.sex}
    </select>
  4. 接口

    1
    public List<User> findUserByVo(QueryVo queryVo);
  5. 测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Test
    public void findUserByVo() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    QueryVo queryVo = new QueryVo();
    User user = new User();
    user.setUsername("王");
    user.setSex("1");
    queryVo.setUser(user);
    List<User> users = userMapper.findUserByVo(queryVo);
    System.out.println(users);
    }

6、resultType(输出类型)

1、输出简单类型

1
2
3
<select id="findUserCount" resultType="java.lang.Long">
select count(*) from user
</select>
1
public Long findUserCount();
1
2
3
4
5
6
@Test
public void testFindUserCount() throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
System.out.println(userMapper.findUserCount());
}

2、输出pojo对象或者pojo对象列表

1
2
3
4
5
6
7
<select id="findUserById" parameterType="java.lang.Integer" resultType="user">
select * from user where id = #{id}
</select>
<select id="findUserByUserName" parameterType="java.lang.String" resultType="User">
select * from user where username like '%${value}%'
</select>
1
2
3
public User findUserById(Integer id);
// 动态代理模式中,如果返回结果集WieList,mybatis会在生成实现类的时候自动调用selectList方法
public List<User> findUserByUserName(String username);

7、resultMap

resultType可以指定pojo将查询结果映射为pojo,但需要pojo的属性名和sql查询的列名一致方可映射成功。如果sql查询字段名和pojo的属性名不一致,可以通过resultMap将字段名和属性名作一个对应关系 ,resultMap实质上还需要将查询结果映射到pojo对象中。resultMap可以实现将查询结果映射为复杂类型的pojo,比如在查询结果映射对象中包括pojo和list实现一对一查询和一对多查询。

  1. Mapper.xml的定义:使用resultMap来指定输出内容映射

    1
    2
    3
    4
    5
    <select id="findOrdersAndUser2" resultMap="orderAndUserResultMap">
    select a.*, b.id uid, username, birthday, sex, address
    from orders a, user b
    where a.user_id = b.id
    </select>
  2. 定义resultMap

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <!--一对一,手动映射-->
    <!--id resultMap的唯一标识,type为将查询出的数据放入这个指定的对象
    手动映射需要制定数据库中表的字段名与java中Pojo类的属性名称的对应关系
    -->
    <resultMap id="orderAndUserResultMap" type="cn.itheima.pojo.Orders">
    <!--id标签制定主键字段对应关系, column为数据库中的字段名,property为pojo类的属性名-->
    <id column="id" property="id"/>
    <!--result标签制定非主键列的对应关系-->
    <result column="user_id" property="userId"/>
    <result column="number" property="number"/>
    <result column="createtime" property="createtime"/>
    <result column="note" property="note"/>
    <!--user对象的映射关系的指定
    property:指定将数据放入Orders中的user属性中,
    type为user的类型
    -->
    <association property="user" javaType="cn.itheima.pojo.User">
    <id column="uid" property="id"/>
    <result column="username" property="username"/>
    <result column="birthday" property="birthday"/>
    <result column="sex" property="sex"/>
    <result column="address" property="address"/>
    </association>
    </resultMap>
  3. Mapper接口定义

    1
    public List<Orders> findOrdersAndUser2();

五、动态sql

通过mybatis提供的各种标签方法实现动态拼接SQL。

1、if

1
2
3
4
5
6
7
8
9
10
<select id="findUserByUserNameAndSex" parameterType="cn.itheima.pojo.User" resultType="cn.itheima.pojo.User">
select * from user
where 1=1
<if test="id!=null">
and id=#{id}
</if>
<if test="username!=null and username!=''">
and username like '%${username}%'
</if>
</select>

空字符串校验

2、where

1
2
3
4
5
6
7
8
9
10
11
<select id="findUserByUserNameAndSex" parameterType="cn.itheima.pojo.User" resultType="cn.itheima.pojo.User">
select * from user
<where>
<if test="id!=null and id!=''">
and id=#{id}
</if>
<if test="username!=null and username!=''">
and username like '%${username}%'
</if>
</where>
</select>

会自动向SQL语句中添加where关键字,会去掉第一个条件的and关键字

3、foreach

向SQL传递数组或List,mybatis使用foreach解析,需求在传入多个id查询用户信息时需要。

  • 需要在Vo中定义list属性ids存储多个用户id,并添加getter、setter方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    private List<Integer> ids;
    public List<Integer> getIds() {
    return ids;
    }
    public void setIds(List<Integer> ids) {
    this.ids = ids;
    }
  • mapper.xml中的编写:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <select id="findUserByIds" parameterType="cn.itheima.pojo.QueryVo" resultType="user">
    select * from user
    <where>
    <if test="ids != null and ids.size > 0">
    <!--foreach:循环传入的集合参数
    collection:传入的集合的变量名称
    item:每次循环将循环出的数据放入这个变量中
    open:循环开始拼接的字符串
    close:循环结束拼接的字符串
    seperator:循环中拼接的字符串
    -->
    <foreach collection="ids" item="id" open="and id in (" close=")" separator=",">
    #{id}
    </foreach>
    </if>
    </where>
    </select>
  • 测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Test
    public void testFindUserByIds() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    QueryVo vo = new QueryVo();
    List<Integer> ids = new ArrayList<>();
    ids.add(1);
    ids.add(10);
    ids.add(16);
    ids.add(28);
    ids.add(22);
    vo.setIds(ids);
    List<User> users = userMapper.findUserByIds(vo);
    for (User user : users) {
    System.out.println("===============" + user + "===============");
    }
    }

4、sql片段

sql中可将重复的SQL提取出来,使用时用include引用即可,从而达到SQL重用的目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--封装SQL条件,封装后可以重用,id为SQL的唯一标示-->
<sql id="user_Where">
<!--会自动向SQL语句中添加where关键字,
会去掉第一个条件的and关键字-->
<where>
<if test="username != null and username != ''">
and username like '%${username}%'
</if>
<if test="sex != null and sex != ''">
and sex=#{sex}
</if>
</where>
</sql>
......
<select id="findUserByUserNameAndSex" parameterType="cn.itheima.pojo.User" resultType="cn.itheima.pojo.User">
select * from user
<!--调用SQL条件-->
<include refid="user_Where"/>
</select>

六、关联查询

1、商品订单数据模型

Alt text

2、一对一关联

需求:查询所有订单信息,关联查询下单用户信息。由于一个订单信息只能由一个用户下单,所以从查询订单信息出发关联查询用户信息为一对一查询。如果从用户信息出发查询用户下的订单为一对多查询。

方法一:使用resultType,定义订单信息po类,此po类中包括了订单信息和用户信息。

  1. SQL语句

    1
    2
    3
    select a.*, b.id uid, username, birthday, sex, address
    from orders a, user b
    where a.user_id = b.id
  2. 定义po类:po类应该包含sql查询的所有字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package cn.itheima.pojo;
    import java.util.Date;
    public class CustomOrders extends Orders{
    private int uid;
    private String username;// 用户姓名
    private String sex;// 性别
    private Date birthday;// 生日
    private String address;// 地址
    //omit...

    OrdersCustomer类继承Orders类后包括了Orders类的所有字段,只需要定义用户的信息字段即可。

  3. Mapper.xml中

    1
    2
    3
    4
    5
    6
    <!--一对一,自动映射 -->
    <select id="findOrdersAndUser1" resultType="cn.itheima.pojo.CustomOrders">
    select a.*, b.id uid, username, birthday, sex, address
    from orders a, user b
    where a.user_id = b.id
    </select>
  4. Mapper接口

    1
    public List<CustomOrders> findOrdersAndUser1();
  5. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Test
    public void testFindOrdersAndUser1() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    List<CustomOrders> customOrders = userMapper.findOrdersAndUser1();
    for (CustomOrders customOrders1 : customOrders) {
    System.out.println("===============" + customOrders1 + "===============");
    }
    }
  6. 小结:
    定义专门的po类作为输出类型,其中定义了SQL查询结果集的所有字段,此方法比较简单,企业中使用普遍。

方法二:使用resultMap,定义专门的resultMap用于映射一对一查询

  1. SQL语句

    1
    2
    3
    select a.*, b.id uid, username, birthday, sex, address
    from orders a, user b
    where a.user_id = b.id
  2. 定义po类:在Orders类中加入User属性,User属性中用于存储关联查询的用户信息,因为订单关联查询是一对一关系,所以这里使用单个User对象存储关联查询的用户信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package cn.itheima.pojo;
    import java.util.Date;
    import java.util.List;
    public class Orders {
    private Integer id;
    private Integer userId;
    private String number;
    private Date createtime;
    private String note;
    private User user;
    //omit...
    }
  3. Mapper.xml中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <resultMap id="orderAndUserResultMap" type="cn.itheima.pojo.Orders">
    <!--id标签制定主键字段对应关系, column为数据库中的字段名,property为pojo类的属性名-->
    <id column="id" property="id"/>
    <!--result标签制定非主键列的对应关系-->
    <result column="user_id" property="userId"/>
    <result column="number" property="number"/>
    <result column="createtime" property="createtime"/>
    <result column="note" property="note"/>
    <!--user对象的映射关系的指定
    property:指定将数据放入Orders中的user属性中,
    type为user的类型
    -->
    <association property="user" javaType="cn.itheima.pojo.User">
    <id column="uid" property="id"/>
    <result column="username" property="username"/>
    <result column="birthday" property="birthday"/>
    <result column="sex" property="sex"/>
    <result column="address" property="address"/>
    </association>
    </resultMap>
    <select id="findOrdersAndUser2" resultMap="orderAndUserResultMap">
    select a.*, b.id uid, username, birthday, sex, address
    from orders a, user b
    where a.user_id = b.id
    </select>
    • association:表示进行关联查询单条记录
    • property:表示关联查询的结果存储在cn.itcast.mybatis.po.Orders的user属性中
    • javaType:表示关联查询的结果类型
    • :查询结果的user_id列对应关联对象的id属性,这里是表示user_id是关联查询对象的唯一标识。
    • :查询结果的username列对应关联对象的username属性
  1. Mapper接口

    1
    public List<Orders> findOrdersAndUser2();
  2. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    public void testFindOrdersAndUser2() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    List<Orders> orders = userMapper.findOrdersAndUser2();
    System.out.println(orders);
    }
  3. 小结:使用association完成关联查询,将关联查询信息映射到pojo对象中。

3、一对多查询

需求:查询所有用户信息及用户关联的订单信息,用户信息和订单信息为一对多关系。使用resultMap实现:

  1. SQL语句

    1
    2
    select a.*, b.id oid, user_id, number, createtime
    from user a, orders b where a.id = b.user_id
  2. 定义po类:在User类中加入List orders属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package cn.itheima.pojo;
    import java.util.Date;
    import java.util.List;
    public class User {
    private int id;
    private String username;// 用户姓名
    private String sex;// 性别
    private Date birthday;// 生日
    private String address;// 地址
    private List<Orders> ordersList;
    //omit....
    }
  3. Mapper.xml中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <resultMap id="userAndOrdersResultMap" type="cn.itheima.pojo.User">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="sex" property="sex"/>
    <result column="birthday" property="birthday"/>
    <result column="address" property="address"/>
    <!--订单集合-->
    <!--collection指定对应的集合关系映射
    property:将数据放入指定的User对象的OrdersList集合属性中
    ofType: 指定OrderList中的泛型类型
    -->
    <collection property="ordersList" ofType="cn.itheima.pojo.Orders">
    <id column="oid" property="id"/>
    <result column="user_id" property="userId"/>
    <result column="number" property="number"/>
    <result column="createtime" property="createtime"/>
    </collection>
    </resultMap>
    <select id="findUserAndOrders" resultMap="userAndOrdersResultMap">
    select a.*, b.id oid, user_id, number, createtime
    from user a, orders b where a.id = b.user_id
    </select>
    • collections:定义了用户关联的订单信息,表示关联查询结果集
    • property:关联查询的结果集存储在User对象上的那个属性
    • ofType:指定关联查询的结果集中的对象类型及List中的对象类型。此处可以使用别名也可以使用全限定名
    • :查询结果的oid列对应关联对象的id属性,这里是表示oid是关联查询对象的唯一标识。
    • :查询结果的userId列对应关联对象的user_id属性
  1. Mapper接口

    1
    public List<User> findUserAndOrders();
  2. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    public void testFindUserAndOrders() throws Exception {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    List<User> users = userMapper.findUserAndOrders();
    System.out.println(users);
    }

七、Spring整合mybatis

1、整合思路

  1. SqlSessionFactory对象应该放入Spring容器中作为单例存在。

  2. 传统dao的开发方式中,应该从Spring容器中获取SqlSession对象。

  3. Mapper代理形式中,应该从Spring容器中直接获得mapper对象。

  4. 数据库的链接记忆数据库连接池事务都交给spring容器来完成。

2、整合需要的jar包

  1. Spring的jar包

  2. mybatis的jar包

  3. spring+mybatis的整合包

  4. MySQL数据库驱动包

  5. 数据库连接池的jar包

    Alt text

3、整合的步骤

  1. 创建一个java工程

  2. 导入jar包

  3. mybatis配置文件SQLMapConfig.xml

  4. 编写Spring配置文件,包括数据库连接及连接池、事务管理、SqlSessionFactory对象,配置到Spring容器中、,mapper代理对象或者dao实现类配置到Spring容器中。

  5. 编写dao或者mapper文件

  6. 测试

1、SqlMapConfig.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--别名-->
<typeAliases>
<!--定义单个pojo类别名 type 类的全路径名, alias别名-->
<typeAlias type="cn.itheima.pojo.User" alias="user"/>
<!--使用包扫描的方式数量定义别名,定以后别名等于类名,不区分大小写,建议按照Java命名规则来-->
<package name="cn.itheima.pojo"/>
</typeAliases>
<mappers>
<mapper resource="User.xml"/>
<!--使用class属性引入接口的全路径名称,使用规则
1、接口名称和映射文件名称除了扩展名之外完全相同,
2、接口和映射文件要放在同一个目录下
-->
<!--<mapper class="UserMapper"/>-->
<!--使用包扫描的方式批量引入Mapper接口
1、接口名称和映射文件名称除了扩展名之外完全相同,
2、接口和映射文件要放在同一个目录下-->
<package name="cn.itheima.mapper"/>
</mappers>
</configuration>

2、applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 加载配置文件 -->
<context:property-placeholder location="classpath:db.properties" />
<!-- 数据库连接池 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="maxActive" value="10" />
<property name="maxIdle" value="5" />
</bean>
<!-- mapper配置 -->
<!-- 让spring管理sqlsessionfactory 使用mybatis和spring整合包中的 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 数据库连接池 -->
<property name="dataSource" ref="dataSource" />
<!-- 加载mybatis的全局配置文件 -->
<property name="configLocation" value="classpath:SqlMapConfig.xml" />
</bean>
<!--配置原生DAO实现-->
<!--<bean id="userDao" class="cn.itheima.dao.UserDaoImpl">-->
<!--<property name="sqlSessionFactory" ref="sqlSessionFactory"/>-->
<!--</bean>-->
<!--Mapper接口代理的实现-->
<!--<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">-->
<!--&lt;!&ndash;接口路径名&ndash;&gt;-->
<!--<property name="mapperInterface" value="UserMapper"/>-->
<!--<property name="sqlSessionFactory" ref="sqlSessionFactory"/>-->
<!--</bean>-->
<!--使用包扫描的方式引入Mapper-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定要扫描的包的全路径名称,如果有多个包,用英文逗号分隔-->
<!--扫描后引用的时候使用类名首字母小写-->
<property name="basePackage" value="cn.itheima.mapper"/>
</bean>
</beans>

3、db.properties

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8
jdbc.username=root
jdbc.password=linux

4、DAO的开发

三种dao的实现方式:

  1. 传统dao的开发方式

  2. 使用mapper代理形式开发方式

  3. 使用扫描包配置mapper代理

1、使用传统dao的开发方式

接口+实现类来完成。dao实现类需要继承SqlSessionDaoSupport.

  1. 实现类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package cn.itheima.dao;
    import cn.itheima.pojo.User;
    import org.apache.ibatis.session.SqlSession;
    import org.mybatis.spring.support.SqlSessionDaoSupport;
    import java.util.List;
    public class UserDaoImpl extends SqlSessionDaoSupport implements UserDao{
    @Override
    public User findUserById(Integer id) {
    //sqlSession是线程不安全的,所以他是最佳使用范围在方法体内
    SqlSession openSession = this.getSqlSession();
    User user = openSession.selectOne("test.findUserById", id);
    return user;
    }
    @Override
    public List<User> findUserByUserName(String username) {
    SqlSession sqlSession = this.getSqlSession();
    List<User> users = sqlSession.selectList("test.findUserByUserName", username);
    return users;
    }
    }
  2. 配置DAO:

    1
    2
    3
    4
    <!--配置原生DAO实现-->
    <bean id="userDao" class="cn.itheima.dao.UserDaoImpl">
    <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
    </bean>
  3. 测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package cn.itheima.test;
    import cn.itheima.dao.UserDao;
    import cn.itheima.pojo.User;
    import org.junit.Before;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    public class UserDaoTest {
    private ApplicationContext applicationContext;
    @Before
    public void setUp() throws Exception {
    String configLocation = "applicationContext.xml";
    applicationContext = new ClassPathXmlApplicationContext(configLocation);
    }
    @Test
    public void testFindUserById() throws Exception {
    //获取DAO对象,getBean中的字符串为配置文件中声明的
    UserDao userDao = (UserDao) applicationContext.getBean("userDao");
    User user = userDao.findUserById(1);
    System.out.println(user);
    }
    }

2、Mapper代理形式开发dao

按照代理的要求,编写映射文件和接口,放置在同一个路径下。

  1. 配置

    1
    2
    3
    4
    5
    6
    <!--Mapper接口代理的实现-->
    <bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
    &lt;!&ndash;接口路径名&ndash;&gt;
    <property name="mapperInterface" value="UserMapper"/>
    <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
    </bean>
  2. 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package cn.itheima.test;
import cn.itheima.mapper.UserMapper;
import cn.itheima.pojo.User;
import cn.itheima.pojo.UserExample;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.List;
public class UserMapperTest {
private ApplicationContext applicationContext;
@Before
public void setUp() throws Exception {
String configLocation = "applicationContext.xml";
applicationContext = new ClassPathXmlApplicationContext(configLocation);
}
// @Test
// public void testFindUserById() throws Exception {
// //获取DAO对象,getBean中的字符串为配置文件中声明的
// UserMapper userMapper = (UserMapper) applicationContext.getBean("userMapper");
//
// User user = userMapper.findUserById(1);
//
// System.out.println(user);
// }
@Test
public void testFindUserById() throws Exception {
//获取DAO对象,getBean中的字符串为配置文件中声明的
UserMapper userMapper = (UserMapper) applicationContext.getBean("userMapper");
User user = userMapper.selectByPrimaryKey(1);
System.out.println(user);
}
@Test
public void testFindUserAndSex() throws Exception{
//获取DAO对象,getBean中的字符串为配置文件中声明的
UserMapper userMapper = (UserMapper) applicationContext.getBean("userMapper");
//创建UserExample对象
UserExample userExample = new UserExample();
//创建查询条件封装对象
UserExample.Criteria criteria = userExample.createCriteria();
//输入条件
criteria.andUsernameLike("%王%");
criteria.andSexEqualTo("1");
List<User> user = userMapper.selectByExample(userExample);
System.out.println(user);
}
}

3、使用包扫描的方式配置mapper

1
2
3
4
5
6
<!--使用包扫描的方式引入Mapper-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定要扫描的包的全路径名称,如果有多个包,用英文逗号分隔-->
<!--扫描后引用的时候使用类名首字母小写-->
<property name="basePackage" value="cn.itheima.mapper"/>
</bean>

每个mapper代理对象的id就是类名首字母小写

八、mybatis逆向工程

使用官方网站的mapper自动生成工具mybatis-generator-core-1.3.2来生成po类和mapper映射文件。

作用:mybatis官方提供逆向工程,可以使用它通过数据库中的表来自动生成Mapper接口和映射文件(单表增删改查)和Po类.

需要导入的jar包包括:

Alt text

1、mapper生成配置文件

在generatorConfig.xml中配置mapper生成的详细信息,需要注意:

  1. 添加要生成的数据库表

  2. po文件所在的包路径

  3. mapper文件所在的包路径

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="testTables" targetRuntime="MyBatis3">
<commentGenerator>
<!-- 是否去除自动生成的注释 true:是 : false:否 -->
<property name="suppressAllComments" value="true" />
</commentGenerator>
<!--数据库连接的信息:驱动类、连接地址、用户名、密码 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/springmvc" userId="root"
password="linux">
</jdbcConnection>
<!-- <jdbcConnection driverClass="oracle.jdbc.OracleDriver"
connectionURL="jdbc:oracle:thin:@127.0.0.1:1521:yycg"
userId="yycg"
password="yycg">
</jdbcConnection> -->
<!-- 默认false,把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer,为 true时把JDBC DECIMAL 和
NUMERIC 类型解析为java.math.BigDecimal -->
<javaTypeResolver>
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>
<!-- targetProject:生成PO类的位置 -->
<javaModelGenerator targetPackage="cn.itheima.pojo"
targetProject=".\src">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false" />
<!-- 从数据库返回的值被清理前后的空格 -->
<property name="trimStrings" value="true" />
</javaModelGenerator>
<!-- targetProject:mapper映射文件生成的位置 -->
<sqlMapGenerator targetPackage="cn.itheima.dao"
targetProject=".\src">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false" />
</sqlMapGenerator>
<!-- targetPackage:mapper接口生成的位置 -->
<javaClientGenerator type="XMLMAPPER"
targetPackage="cn.itheima.dao"
targetProject=".\src">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false" />
</javaClientGenerator>
<!-- 指定数据库表 -->
<table tableName="items"></table>
<!--<table tableName="orders"></table>-->
<!--<table tableName="orderdetail"></table>-->
<table tableName="user"></table>
<!-- <table schema="" tableName="sys_user"></table>
<table schema="" tableName="sys_role"></table>
<table schema="" tableName="sys_permission"></table>
<table schema="" tableName="sys_user_role"></table>
<table schema="" tableName="sys_role_permission"></table> -->
<!-- 有些表的字段需要指定java类型
<table schema="" tableName="">
<columnOverride column="" javaType="" />
</table> -->
</context>
</generatorConfiguration>

2、使用java类生成mapper文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class StartServer {
public void generator() throws Exception{
List<String> warnings = new ArrayList<String>();
boolean overwrite = true;
File configFile = new File("generator.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
callback, warnings);
myBatisGenerator.generate(null);
}
public static void main(String[] args) throws Exception {
try {
StartServer startServer = new StartServer();
startServer.generator();
} catch (Exception e) {
e.printStackTrace();
}
}
}

3、将生成的mapper文件拷贝到指定目录

注意:mapper xml文件和mapper.java文件在一个目录内且文件名相同。

4、测试mapper接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package cn.itheima.dao;
import cn.itheima.pojo.Items;
import cn.itheima.pojo.ItemsExample;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface ItemsMapper {
int countByExample(ItemsExample example);
int deleteByExample(ItemsExample example);
int deleteByPrimaryKey(Integer id);
int insert(Items record);
int insertSelective(Items record);
List<Items> selectByExampleWithBLOBs(ItemsExample example);
List<Items> selectByExample(ItemsExample example);
Items selectByPrimaryKey(Integer id);
int updateByExampleSelective(@Param("record") Items record, @Param("example") ItemsExample example);
int updateByExampleWithBLOBs(@Param("record") Items record, @Param("example") ItemsExample example);
int updateByExample(@Param("record") Items record, @Param("example") ItemsExample example);
int updateByPrimaryKeySelective(Items record);
int updateByPrimaryKeyWithBLOBs(Items record);
int updateByPrimaryKey(Items record);
}

5、注意事项

  1. Mapper文件内容时追加不是覆盖。所以XXXMapper.xml文件已经存在时,如果进行重新生成则mapper.xml文件内容不被覆盖而是进行内容追加,结果导致mybatis解析失败。
    解决方法:删除原来已经生成的mapper xml文件再进行生成。Mybatis自动生成的po及mapper.java文件不是内容而是直接覆盖没有此问题。

  2. Table Schema问题:

下边是关于针对oracle数据库表生成代码的schema问题:

Schma即数据库模式,oracle中一个用户对应一个schema,可以理解为用户就是schema。
当Oralce数据库存在多个schema可以访问相同的表名时,使用mybatis生成该表的mapper.xml将会出现mapper.xml内容重复的问题,结果导致mybatis解析错误。
解决方法:在table中填写schema,如下:


XXXX即为一个schema的名称,生成后将mapper.xml的schema前缀批量去掉,如果不去掉当oracle用户变更了sql语句将查询失败。
快捷操作方式:mapper.xml文件中批量替换:“from XXXX.”为空

Oracle查询对象的schema可从dba_objects中查询,如下:
select * from dba_objects

九、总结

  1. mybatis是一个持久层框架, 作用是跟数据库交互完成增删改查

  2. 原生Dao实现(需要接口和实现类)

  3. 动态代理方式(只需要接口)
    mapper接口代理实现编写规则:

    1. 映射文件中namespace要等于接口的全路径名称
    2. 映射文件中sql语句id要等于接口的方法名称
    3. 映射文件中传入参数类型要等于接口方法的传入参数类型
    4. 映射文件中返回结果集类型要等于接口方法的返回值类型
  4. #{}占位符:占位
    如果传入的是基本类型,那么#{}中的变量名称可以随意写
    如果传入的参数是pojo类型,那么#{}中的变量名称必须是pojo中的属性.属性.属性…

  5. ${}拼接符:字符串原样拼接
    如果传入的是基本类型,那么${}中的变量名必须是value
    如果传入的参数是pojo类型,那么${}中的变量名称必须是pojo中的属性.属性.属性…
    注意:使用拼接符有可能造成sql注入,在页面输入的时候可以加入校验,不可输入sql关键字,不可输入空格

  6. 映射文件:

    1. 传入参数类型通过parameterType属性指定
    2. 返回结果集类型通过resultType属性指定
  7. hibernate和mybatis区别:

    • hibernate:它是一个标准的orm框架,比较重量级,学习成本高.
      优点:高度封装,使用起来不用写sql,开发的时候,会减低开发周期.
      缺点:sql语句无法优化
      应用场景:oa(办公自动化系统), erp(企业的流程系统)等,还有一些政府项目,
      总的来说,在用于量不大,并发量小的时候使用.
      
    • mybatis:它不是一个orm框架, 它是对jdbc的轻量级封装, 学习成本低,比较简单
      有点:学习成本低, sql语句可以优化, 执行效率高,速度快
      缺点:编码量较大,会拖慢开发周期
      应用场景: 互联网项目,比如电商,P2p等
      总的来说是用户量较大,并发高的项目.
      
  8. 输入映射(就是映射文件中可以传入哪些参数类型)

    • 1)基本类型
      • 2)pojo类型
      • 3)Vo类型
  9. 输出映射(返回的结果集可以有哪些类型)

    • 1)基本类型
    • 2)pojo类型
    • 3)List类型
  10. 动态sql:动态的拼接sql语句,因为sql中where条件有可能多也有可能少

    • 1)where:可以自动添加where关键字,还可以去掉第一个条件的and关键字
    • 2)if:判断传入的参数是否为空
    • 3)foreach:循环遍历传入的集合参数
    • 4)sql:封装查询条件,以达到重用的目的
  11. 对单个对象的映射关系:

    • 1)自动关联(偷懒的办法):可以自定义一个大而全的pojo类,然后自动映射其实是根据数据库总的字段名称和pojo中的属性名称对应.
    • 2)手动关联: 需要指定数据库中表的字段名称和java的pojo类中的属性名称的对应关系.使用association标签
  12. 对集合对象的映射关系
    只能使用手动映射:指定表中字段名称和pojo中属性名称的对应关系使用collection标签

  13. spring和mybatis整合
    整合后会话工厂都归spring管理

    *     1)原生Dao实现:
             需要在spring配置文件中指定dao实现类
             dao实现类需要继承SqlSessionDaoSupport超类
             在dao实现类中不要手动关闭会话,不要自己提交事务.
    *     2)Mapper接口代理实现:
             在spring配置文件中可以使用包扫描的方式,一次性的将所有mapper加载
    
  14. 逆向工程:自动生成Pojo类,还可以自动生成Mapper接口和映射文件

    注意:生成的方式是追加而不是覆盖,所以不可以重复生成,重复生成的文件有问题.
        如果想重复生成将原来生成的文件删除
    

概念区分:

  1. pojo:不按mvc分层,只是java bean有一些属性,还有get set方法
  2. domain:不按mvc分层,只是java bean有一些属性,还有get set方法
  3. po:用在持久层,还可以再增加或者修改的时候,从页面直接传入action中,它里面的java bean 类名等于表名,
    属性名等于表的字段名,还有对应的get set方法
  4. vo: view object表现层对象,主要用于在高级查询中从页面接收传过来的各种参数.好处是扩展性强
  5. bo: 用在servie层,现在企业基本不用.

这些po,vo, bo,pojo可以用在各种层面吗?

可以,也就是po用在表现层,vo用在持久层不报错,因为都是普通的java bean没有语法错误.
但是在企业最好不要混着用,因为这些都是设计的原则,混着用比较乱.不利于代码维护.

1…678…11
cdx

cdx

Be a better man!

110 日志
36 分类
31 标签
GitHub E-Mail
© 2020 cdx
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.2