文章目录
Java后端基础内容
前言
Java/ DB/JDBC 1.BS/CS
HTML/CSS/JS 2. Tomcat
CS:容户端服务器架构模式 3.Servlet人门
优点:充分利用容户端机器的资源,减轻服务器的负荷
(一部分安全要求不高的计算任务存储任务放在客户端执行,不需要把所有的计算和
存储都在服务器端执行,从而能够减轻服务器的压力,也能够减轻网络负荷)
缺点:需要安装;升级维护成本较高
BS:浏览器眼务册架构模式
优点:喜户端不需要安装,维护成本较低
缺点:所有的计算和存储任务都是放在服务器端的,服务器的负荷较重;在服务端计算完成之后把结果再传输给客户端,因此春户端和服务器端会进行非常频繁的数据通信,从而网笔负荷较重
doPost 方法 和 doGet 方法 不能同时执行
作用域
1. 保存作用域
原始情况下,保存作用域我们可以认为有四个: page(页面级别,现在几乎不用) , request(一次请求响应范围) , session(一次会话范围) , application(整个应用程序范围)
1) request:一次请求响应范围
2) session:一次会话范围有效
3) application: 一次应用程序范围有效
servlet
1、设置编码:
tomcat8之前,设置编码:
1)get请求方式:
//get方式目前不需要设置编码(基于tomcat8)
//如果是get请求发送的中文数据,转码稍微有点麻烦(tomcat8之前)
String fname = request.getParameter(“fname”);
//1.将字符串打散成字节数组
byte[] bytes = fname.getBytes(“ISO-8859-1”);
//2.将字节数组按照设定的编码重新组装成字符串
fname = new String(bytes,“UTF-8”);
2)post请求方式:
request.setCharacterEncoding(“UTF-8”);
tomcat8开始,设置编码,只需要针对post方式
request.setCharacterEncoding(“UTF-8”);
注意:
需要注意的是,设置编码(post)这一句代码必须在所有的获取参数动作之前
2、Servlet的继承关系 - 重点查看的是服务方法(service())
1. #### 继承关系
javax.servlet.Servlet接口
javax.servlet.GenericServlet抽象类
javax.servlet.http.HttpServlet抽象子类
#### 2.相关方法
javax.servlet.Servlet接口:
void init(config) - 初始化方法
void service(request,response) - 服务方法
void destory() - 销毁方法
javax.servlet.GenericServlet抽象类:
void service(request,response) - 仍然是抽象的
javax.servlet.http.HttpServlet 抽象子类:
void service(request,response) - 不是抽象的
1.String method = req.getMethod(); 获取请求的方式
2. 各种if判断,根据请求方式不同,决定去调用不同的do方法
if (method.equals("GET")) {
this.doGet(req,resp);
} else if (method.equals("HEAD")) {
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
3. 在HttpServlet这个抽象类中,do方法都差不多:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
小结:
- 继承关系: HttpServlet -> GenericServlet -> Servlet
- Servlet中的核心方法: init() , service() , destroy()
- 服务方法: 当有请求过来时,service方法会自动响应(其实是tomcat容器调用的)
在HttpServlet中我们会去分析请求的方式:到底是get、post、head还是delete等等
然后再决定调用的是哪个do开头的方法
那么在HttpServlet中这些do方法默认都是405的实现风格-要我们子类去实现对应的方法,否则默认会报405错误 - 因此,我们在新建Servlet时,我们才会去考虑请求方法,从而决定重写哪个do方法
3、servlet生命周期
Servlet生命周期:实例化、初始化、服务、销毁
1) 生命周期:从出生到死亡的过程就是生命周期。对应Servlet中的三个方法:init(),service(),destroy()
2) 默认情况下:
第一次接收请求时,这个Servlet会进行实例化(调用构造方法)、初始化(调用init())、然后服务(调用service())
从第二次请求开始,每一次都是服务
当容器关闭时,其中的所有的servlet实例会被销毁,调用销毁方法
3) 通过案例我们发现:
-
Servlet实例tomcat只会创建一个,所有的请求都是这个实例去响应。
-
默认情况下,第一次请求时,tomcat才会去实例化,初始化,然后再服务.这样的好处是什么? 提高系统的启动速度 。 这样的缺点是什么? 第一次请求时,耗时较长。
-
因此得出结论: 如果需要提高系统的启动速度,当前默认情况就是这样。如果需要提高响应速度,我们应该设置Servlet的初始化时机。
4) Servlet的初始化时机: -
默认是第一次接收请求时,实例化,初始化
-
我们可以通过来设置servlet启动的先后顺序,数字越小,启动越靠前,最小值0
5) Servlet在容器中是:单例的、线程不安全的 -
单例:所有的请求都是同一个实例去响应
-
线程不安全:一个线程需要根据这个实例中的某个成员变量值去做逻辑判断。但是在中间某个时机,另一个线程改变了这个成员变量的值,从而导致第一个线程的执行路径发生了变化 在servlet内部不设有公共属性
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jPfAFu8q-1647596480630)(C:\Users\53026\AppData\Roaming\Typora\typora-user-images\image-20220317165834814.png)]
- 我们已经知道了servlet是线程不安全的,给我们的启发是: 尽量的不要在servlet中定义成员变量。如果不得不定义成员变量,那么不要去:①不要去修改成员变量的值 ②不要去根据成员变量的值做一些逻辑判断
4、配置xml文件 ,注册servlet
方法一:
<servlet>
<servlet-name>AddServlet</servlet-name>
<servlet-class>com.atguigu.servlets.AddServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>AddServlet</servlet-name>
<url-pattern>/add</url-pattern>
</servlet-mapping>
<!--
1. 用户发请求,action=add
2. 项目中,web.xml中找到url-pattern = /add -> 第12行
3. 找第11行的servlet-name = AddServlet
4. 找和servlet-mapping中servlet-name一致的servlet , 找到第7行
5. 找第8行的servlet-class -> com.atguigu.servlets.AddServlet
6. 用户发送的是post请求(method=post) , 因此 tomcat会执行AddServlet中的doPost方法
-->
5、服务器内部的转发与客户端的重定向
1) 服务器内部转发 : request.getRequestDispatcher("…").forward(request,response);
一次请求响应的过程,对于客户端而言,内部经过了多少次转发,客户端是不知道的
地址栏没有变化。 转发至另外的servlet
2) 客户端重定向: response.sendRedirect("…");
两次请求响应的过程。客户端肯定知道请求URL有变化
地址栏有变化
6、API
1)Servlet中的初始化方法有两个:init() , init(config)
其中带参数的方法代码如下:
public void init(ServletConfig config) throws ServletException {
this.config = config ;
init();
}
另外一个无参的init方法如下:
public void init() throws ServletException{
}
如果我们想要在Servlet初始化时做一些准备工作,那么我们可以重写init方法
我们可以通过如下步骤去获取初始化设置的数据
- 获取config对象:ServletConfig config = getServletConfig();
- 获取初始化参数值: config.getInitParameter(key);
2)配置servlet
方式一:在web.xml文件中
Demo01Servlet com.atguigu.servlet.Demo01Servlet hello world uname jim Demo01Servlet /demo01方式二:也可以通过注解的方式进行配置:
@WebServlet(urlPatterns = {"/demo01"} ,
initParams = {
@WebInitParam(name="hello",value="world"),
@WebInitParam(name="uname",value="jim")
})
3)ServletContext 和 context-param
-
获取ServletContext,有很多方法
在初始化方法中: ServletContxt servletContext = getServletContext();
在服务方法中也可以通过request对象获取,也可以通过session获取:
request.getServletContext(); session.getServletContext() -
获取初始化值:
servletContext.getInitParameter();
- 在web.xml文档中配置context-param
Http协议
1) Http是无状态的
- HTTP 无状态 :服务器无法判断这两次请求是同一个客户端发过来的,还是不同的客户端发过来的
- 无状态带来的现实问题:第一次请求是添加商品到购物车,第二次请求是结账;如果这两次请求服务器无法区分是同一个用户的,那么就会导致混乱
- 通过会话跟踪技术来解决无状态的问题。
2) 会话跟踪技术
客户端第一次发请求给服务器,服务器获取session,获取不到,则创建新的,然后响应给客户端
下次客户端给服务器发请求时,会把sessionID带给服务器,那么服务器就能获取到了,那么服务器就判断这一次请求和上次某次请求是同一个客户端,从而能够区分开客户端
常用的API:
request.getSession() -> 获取当前的会话,没有则创建一个新的会话
request.getSession(true) -> 效果和不带参数相同
request.getSession(false) -> 获取当前会话,没有则返回null,不会创建新的
session.getId() -> 获取sessionID
session.isNew() -> 判断当前session是否是新的
session.getMaxInactiveInterval() -> session的非激活间隔时长,默认1800秒
session.setMaxInactiveInterval()
session.invalidate() -> 强制性让会话立即失效
3) session保存作用域
session保存作用域是和具体的某一个session对应的
常用的API:
void session.setAttribute(k,v)
Object session.getAttribute(k)
void removeAttribute(k)
1、配置:
1) 添加thymeleaf的jar包
2) 新建一个Servlet类ViewBaseServlet 直接官网copy
public class ViewBaseServlet extends HttpServlet {
private TemplateEngine templateEngine;
@Override
public void init() throws ServletException {
// 1.获取ServletContext对象
ServletContext servletContext = this.getServletContext();
// 2.创建Thymeleaf解析器对象
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(servletContext);
// 3.给解析器对象设置参数
// ①HTML是默认模式,明确设置是为了代码更容易理解
templateResolver.setTemplateMode(TemplateMode.HTML);
// ②设置前缀
String viewPrefix = servletContext.getInitParameter("view-prefix");
templateResolver.setPrefix(viewPrefix);
// ③设置后缀
String viewSuffix = servletContext.getInitParameter("view-suffix");
templateResolver.setSuffix(viewSuffix);
// ④设置缓存过期时间(毫秒)
templateResolver.setCacheTTLMs(60000L);
// ⑤设置是否缓存
templateResolver.setCacheable(true);
// ⑥设置服务器端编码方式
templateResolver.setCharacterEncoding("utf-8");
// 4.创建模板引擎对象
templateEngine = new TemplateEngine();
// 5.给模板引擎对象设置模板解析器
templateEngine.setTemplateResolver(templateResolver);
}
protected void processTemplate(String templateName, HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1.设置响应体内容类型和字符集
resp.setContentType("text/html;charset=UTF-8");
// 2.创建WebContext对象
WebContext webContext = new WebContext(req, resp, getServletContext());
// 3.处理模板数据
templateEngine.process(templateName, webContext, resp.getWriter());
}
}
3) 在web.xml文件中添加配置
配置前缀 view-prefix
配置后缀 view-suffix
<!-- 配置上下文参数 -->
<context-param>
<param-name>view-prefix</param-name>
<param-value>/</param-value>
</context-param>
<context-param>
<param-name>view-suffix</param-name>
<param-value>.html</param-value>
</context-param>
4) Servlet继承ViewBaseServlet
5) 根据逻辑视图名称 得到 物理视图名称
//此处的视图名称是 index
//那么thymeleaf会将这个 逻辑视图名称 对应到 物理视图 名称上去
//逻辑视图名称 : index
//物理视图名称 : view-prefix + 逻辑视图名称 + view-suffix
//所以真实的视图名称是: / index .html
super.processTemplate(“index”,request,response);
6) 使用thymeleaf的标签
th:if , th:unless , th:each , th:text
2、HTML中的使用方法:
1)修改标签文本值
代码示例:
<p th:text="标签体新值">标签体原始值</p>
①th:text作用
- 不经过服务器解析,直接用浏览器打开HTML文件,看到的是『标签体原始值』
- 经过服务器解析,Thymeleaf引擎根据th:text属性指定的『标签体新值』去替换『标签体原始值』
②字面量
『字面量』是一个经常会遇到的概念,我们可以对照『变量』来理解它的含义。
// a是变量,100是字面量
int a = 100;
System.out.println("a = " + a);
- 变量:变量名字符串本身不是它的值,它指向的才是它的值
- 字面量:它就是字面上的含义,我们从『字面』上看到的直接就是它的值
现在我们在th:text属性中使用的就是『字面量』,它不指代任何其他值。
2)修改指定属性值
代码示例:
<input type="text" name="username" th:value="文本框新值" value="文本框旧值" />
语法:任何HTML标签原有的属性,前面加上『th:』就都可以通过Thymeleaf来设定新值。
3)解析URL地址
①基本语法
代码示例:
<p th:text="@{/aaa/bbb/ccc}">标签体原始值</p>
经过解析后得到:
/view/aaa/bbb/ccc
所以**@{}的作用是在字符串前附加『上下文路径』**
这个语法的好处是:实际开发过程中,项目在不同环境部署时,Web应用的名字有可能发生变化。所以上下文路径不能写死。而通过@{}动态获取上下文路径后,不管怎么变都不怕啦!
②首页使用URL地址解析
如果我们直接访问index.html本身,那么index.html是不需要通过Servlet,当然也不经过模板引擎,所以index.html上的Thymeleaf的任何表达式都不会被解析。
解决办法:通过Servlet访问index.html,这样就可以让模板引擎渲染页面了:
进一步的好处:
通过上面的例子我们看到,所有和业务功能相关的请求都能够确保它们通过Servlet来处理,这样就方便我们统一对这些请求进行特定规则的限定。
③给URL地址后面附加请求参数
参照官方文档说明:
4)、直接执行表达式
Servlet代码:
request.setAttribute("reqAttrName", "<span>hello-value</span>");
页面代码:
<p>有转义效果:[[${reqAttrName}]]</p>
<p>无转义效果:[(${reqAttrName})]</p>
执行效果:
<p>有转义效果:<span>hello-value</span></p>
<p>无转义效果:<span>hello-value</span></p>
5)访问域对象
1、域对象
①请求域
在请求转发的场景下,我们可以借助HttpServletRequest对象内部给我们提供的存储空间,帮助我们携带数据,把数据发送给转发的目标资源。
请求域:HttpServletRequest对象内部给我们提供的存储空间
②会话域
③应用域
PS:在我们使用的视图是JSP的时候,域对象有4个
- pageContext
- request:请求域
- session:会话域
- application:应用域
所以在JSP的使用背景下,我们可以说域对象有4个,现在使用Thymeleaf了,没有pageContext。
2、在Servlet中将数据存入属性域
①操作请求域
Servlet中代码:
String requestAttrName = "helloRequestAttr";
String requestAttrValue = "helloRequestAttr-VALUE";
request.setAttribute(requestAttrName, requestAttrValue);
Thymeleaf表达式:
②操作会话域
Servlet中代码:
// ①通过request对象获取session对象
HttpSession session = request.getSession();
// ②存入数据
session.setAttribute("helloSessionAttr", "helloSessionAttr-VALUE");
Thymeleaf表达式:
③操作应用域
存储在服务器中
Servlet中代码:
// ①通过调用父类的方法获取ServletContext对象
ServletContext servletContext = getServletContext();
// ②存入数据
servletContext.setAttribute("helloAppAttr", "helloAppAttr-VALUE");
Thymeleaf表达式:
<p th:text="${application.helloAppAttr}">这里显示应用域数据</p>
6)获取请求参数:
1、一个名字一个值
页面代码:
<p th:text="${param.username}">这里替换为请求参数的值</p>
2、一个名字多个值
页面代码:
<p th:text="${param.team}">这里替换为请求参数的值</p>
如果想要精确获取某一个值,可以使用数组下标。页面代码:
<p th:text="${param.team[0]}">这里替换为请求参数的值</p>
<p th:text="${param.team[1]}">这里替换为请求参数的值</p>
7)内置对象
1、概念
所谓内置对象其实就是在表达式中可以直接使用的对象。
2、基本内置对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qAut7Qkj-1647596480638)(https://heavy_code_industry.gitee.io/code_heavy_industry/assets/img/img021.98446d22.png)]
用法举例:
<h3>表达式的基本内置对象</h3>
<p th:text="${#request.getClass().getName()}">这里显示#request对象的全类名</p>
<p th:text="${#request.getContextPath()}">调用#request对象的getContextPath()方法</p>
<p th:text="${#request.getAttribute('helloRequestAttr')}">调用#request对象的getAttribute()方法,读取属性域</p>
基本思路:
- 如果不清楚这个对象有哪些方法可以使用,那么就通过getClass().getName()获取全类名,再回到Java环境查看这个对象有哪些方法
- 内置对象的方法可以直接调用
- 调用方法时需要传参的也可以直接传入参数
3、公共内置对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bwHjM4Y0-1647596480639)(https://heavy_code_industry.gitee.io/code_heavy_industry/assets/img/img022.b5d6690d.png)]
Servlet中将List集合数据存入请求域:
request.setAttribute("aNotEmptyList", Arrays.asList("aaa","bbb","ccc"));
request.setAttribute("anEmptyList", new ArrayList<>());
页面代码:
<p>#list对象isEmpty方法判断集合整体是否为空aNotEmptyList:<span th:text="${#lists.isEmpty(aNotEmptyList)}">测试#lists</span></p>
<p>#list对象isEmpty方法判断集合整体是否为空anEmptyList:<span th:text="${#lists.isEmpty(anEmptyList)}">测试#lists</span></p>
公共内置对象对应的源码位置:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LtoCBzLT-1647596480639)(https://heavy_code_industry.gitee.io/code_heavy_industry/assets/img/img023.b592213a.png)]
8)${}中的表达式本质是OGNL
1、OGNL
OGNL:Object-Graph Navigation Language对象-图 导航语言
2、对象图
从根对象触发,通过特定的语法,逐层访问对象的各种属性。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0KXcG93U-1647596480639)(https://heavy_code_industry.gitee.io/code_heavy_industry/assets/img/img024.76f91a6f.png)]
3、OGNL语法
#①起点
在Thymeleaf环境下,${}中的表达式可以从下列元素开始:
- 访问属性域的起点
- 请求域属性名
- session
- application
- param
- 内置对象
- #request
- #session
- #lists
- #strings
#②属性访问语法
- 访问对象属性:使用getXxx()、setXxx()方法定义的属性
- 对象.属性名
- 访问List集合或数组
- 集合或数组[下标]
- 访问Map集合
- Map集合.key
- Map集合[‘key’]
9)迭代与分支
#①if和unless
让标记了th:if、th:unless的标签根据条件决定是否显示。
示例的实体类:
public class Employee {
private Integer empId;
private String empName;
private Double empSalary;
示例的Servlet代码:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.创建ArrayList对象并填充
List<Employee> employeeList = new ArrayList<>();
employeeList.add(new Employee(1, "tom", 500.00));
employeeList.add(new Employee(2, "jerry", 600.00));
employeeList.add(new Employee(3, "harry", 700.00));
// 2.将集合数据存入请求域
request.setAttribute("employeeList", employeeList);
// 3.调用父类方法渲染视图
super.processTemplate("list", request, response);
}
示例的HTML代码:
<table>
<tr>
<th>员工编号</th>
<th>员工姓名</th>
<th>员工工资</th>
</tr>
<tr th:if="${#lists.isEmpty(employeeList)}">
<td colspan="3">抱歉!没有查询到你搜索的数据!</td>
</tr>
<tr th:if="${not #lists.isEmpty(employeeList)}">
<td colspan="3">有数据!</td>
</tr>
<tr th:unless="${#lists.isEmpty(employeeList)}">
<td colspan="3">有数据!</td>
</tr>
</table>
if配合not关键词和unless配合原表达式效果是一样的,看自己的喜好。
#②switch
<h3>测试switch</h3>
<div th:switch="${user.memberLevel}">
<p th:case="level-1">银牌会员</p>
<p th:case="level-2">金牌会员</p>
<p th:case="level-3">白金会员</p>
<p th:case="level-4">钻石会员</p>
</div>
迭代
<h3>测试each</h3>
<table>
<thead>
<tr>
<th>员工编号</th>
<th>员工姓名</th>
<th>员工工资</th>
</tr>
</thead>
<tbody th:if="${#lists.isEmpty(employeeList)}">
<tr>
<td colspan="3">抱歉!没有查询到你搜索的数据!</td>
</tr>
</tbody>
<tbody th:if="${not #lists.isEmpty(employeeList)}">
<!-- 遍历出来的每一个元素的名字 : ${要遍历的集合} -->
<tr th:each="employee : ${employeeList}">
<td th:text="${employee.empId}">empId</td>
<td th:text="${employee.empName}">empName</td>
<td th:text="${employee.empSalary}">empSalary</td>
</tr>
</tbody>
</table>
在迭代过程中,可以参考下面的说明使用迭代状态:
<h3>测试each</h3>
<table>
<thead>
<tr>
<th>员工编号</th>
<th>员工姓名</th>
<th>员工工资</th>
<th>迭代状态</th>
</tr>
</thead>
<tbody th:if="${#lists.isEmpty(employeeList)}">
<tr>
<td colspan="3">抱歉!没有查询到你搜索的数据!</td>
</tr>
</tbody>
<tbody th:if="${not #lists.isEmpty(employeeList)}">
<!-- 遍历出来的每一个元素的名字 : ${要遍历的集合} -->
<tr th:each="employee,empStatus : ${employeeList}">
<td th:text="${employee.empId}">empId</td>
<td th:text="${employee.empName}">empName</td>
<td th:text="${employee.empSalary}">empSalary</td>
<td th:text="${empStatus.count}">count</td>
</tr>
</tbody>
</table>
10)包含其他模板文件
#1、应用场景
抽取各个页面的公共部分:
#2、创建页面的代码片段
使用th:fragment来给这个片段命名:
<div th:fragment="header">
<p>被抽取出来的头部内容</p>
</div>
#3、包含到有需要的页面
语法 | 效果 |
---|---|
th:insert | 把目标的代码片段整个插入到当前标签内部 |
th:replace | 用目标的代码替换当前标签 |
th:include | 把目标的代码片段去除最外层标签,然后再插入到当前标签内部 |
页面代码举例:
<!-- 代码片段所在页面的逻辑视图 :: 代码片段的名称 -->
<div id="badBoy" th:insert="segment :: header">
div标签的原始内容
</div>
<div id="worseBoy" th:replace="segment :: header">
div标签的原始内容
</div>
<div id="worstBoy" th:include="segment :: header">
div标签的原始内容
</div>
3、HTML使用实例:
<tr th:if="${#lists.isEmpty(session.fruitList)}"> 遍历session保存的列表 & 条件判断是否为空
<td colspan="4">对不起,库存为空!</td>
</tr>
$取出fruitlist中每一个元素(引用为fruit)
<tr th:unless="${#lists.isEmpty(session.fruitList)}" th:each="fruit : ${session.fruitList}">
$取出元素的属性 @自动补全url的绝对路径
<!-- <td><a th:text="${fruit.fname}" th:href="@{'/edit.do?fid='+${fruit.fid}}">苹果</a></td> -->
<td><a th:text="${fruit.fname}" th:href="@{/edit.do(fid=${fruit.fid})}">苹果</a></td>
<td th:text="${fruit.price}">5</td>
<td th:text="${fruit.fcount}">20</td> 拼接th语句 |替代字符串内的''|
<!-- <td><img src="imgs/del.jpg" class="delImg" th:οnclick="'delFruit('+${fruit.fid}+')'"/></td> -->
<td><img src="imgs/del.jpg" class="delImg" th:onclick="|delFruit(${fruit.fid})|"/></td>
</tr>
小功能
1、翻页:
1)在sql查询中加入limit 限制查询条数
2)获取页面中的pageNo的值 并 servlet设置pageNo
Integer pageNo = 1 ; // 设置默认值为1
String pageNoStr = request.getParameter("pageNo");
if(StringUtil.isNotEmpty(pageNoStr)) {
pageNo = Integer.parseInt(pageNoStr);
}
HttpSession session = request.getSession() ;
session.setAttribute("pageNo",pageNo);
FruitDAO fruitDAO = new FruitDAOImpl();
List<Fruit> fruitList = fruitDAO.getFruitList(pageNo);
session.setAttribute("fruitList",fruitList);
//总记录条数
int fruitCount = fruitDAO.getFruitCount();
//总页数
int pageCount = (fruitCount+5-1)/5 ;
/*
总记录条数 总页数
1 1
5 1
6 2
10 2
11 3
fruitCount (fruitCount+5-1)/5
*/
session.setAttribute("pageCount",pageCount);
3)翻页 在html中通过js、thymeleaf实现
function page(pageNo){
window.location.href="index?pageNo="+pageNo;
}
<div style="width:60%;margin-left:20%;border:0px solid red;padding-top:4px;" class="center">
<input type="button" value="首 页1" class="btn" th:onclick="|page(1)|" th:disabled="${session.pageNo==1}"/>
<input type="button" value="上一页" class="btn" th:onclick="|page(${session.pageNo-1})|" th:disabled="${session.pageNo==1}"/>
<input type="button" value="下一页" class="btn" th:onclick="|page(${session.pageNo+1})|" th:disabled="${session.pageNo==session.pageCount}"/>
<input type="button" value="尾 页" class="btn" th:onclick="|page(${session.pageCount})|" th:disabled="${session.pageNo==session.pageCount}"/>
</div>
2、关键字搜索:
1)在sql语句中设置模糊查询
2)获取页面中的keyWord的值 并 将其保存在session作用域中
1、获取发送操作详情
String oper = request.getParameter("oper");
//如果oper!=null 说明 通过表单的查询按钮点击过来的
//如果oper是空的,说明 不是通过表单的查询按钮点击过来的
String keyword = null ;
2、获取keyword
if(StringUtil.isNotEmpty(oper) && "search".equals(oper)){
//说明是点击表单查询发送过来的请求
//此时,pageNo应该还原为1 , keyword应该从请求参数中获取
pageNo = 1 ;
keyword = request.getParameter("keyword");
if(StringUtil.isEmpty(keyword)){
keyword = "" ;
}
session.setAttribute("keyword",keyword);
}else{
//说明此处不是点击表单查询发送过来的请求(比如点击下面的上一页下一页或者直接在地址栏输入网址)
//此时keyword应该从session作用域获取
String pageNoStr = request.getParameter("pageNo");
if(StringUtil.isNotEmpty(pageNoStr)){
pageNo = Integer.parseInt(pageNoStr);
}
Object keywordObj = session.getAttribute("keyword");
if(keywordObj!=null){
keyword = (String)keywordObj ;
}else{
keyword = "" ;
}
3、查询keyword
FruitDAO fruitDAO = new FruitDAOImpl();
List<Fruit> fruitList = fruitDAO.getFruitList(keyword , pageNo);
session.setAttribute("fruitList",fruitList);
HTML中的查询:
隐藏文本框 ,设置操作方式
<form th:action="@{/index}" method="post" style="float:left;width:60%;margin-left:20%;">
<input type="hidden" name="oper" value="search"/>
请输入关键字:<input type="text" name="keyword" th:value="${session.keyword}"/>
<input type="submit" value="查询" class="btn"/>
</form>
3、Cookies
**目的:保存用户信息 **
设置位置:Despatchter
功能:1)时效内免登录 2) 记住用户名和密码(本地文件里) 设置时效
- 创建Cookie对象
- 在客户端保存Cookie
- 设置Cookie的有效时长
cookie.setMaxAge(60) , 设置cookie的有效时长是60秒
cookie.setDomain(pattern);
cookie.setPath(uri);
4、KaptchaServlet
目的:产生登录验证码
使用:
-
添加jar
-
在web.xml文件中注册KaptchaServlet,并设置验证码图片的相关属性
-
在html页面上编写一个img标签,然后设置src等于KaptchaServlet对应的url-pattern
kaptcha验证码图片的各个属性在常量接口:Constants中
KaptchaServlet在生成验证码图片时,会同时将验证码信息保存到session中
因此,我们在注册请求时,首先将用户文本框中输入的验证码值和session中保存的值进行比较,相等,则进行注册。
HttpSession session = request.getSession() ;
Object obj = session.getAttribute("KAPTCHA_SESSION_KEY");
System.out.println("obj = " + obj);
*重点:mvc
M是指业务模型,V是指用户界面,C则是控制器,使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。其中,View的定义比较清晰,就是用户界面。
-
Model1和Model2
MVC : Model(模型)、View(视图)、Controller(控制器)
视图层:用于做数据展示以及和用户交互的一个界面
控制层:能够接受客户端的请求,具体的业务功能还是需要借助于模型组件来完成
模型层:模型分为很多种:有比较简单的pojo/vo(value object),有业务模型组件,有数据访问层组件
1) pojo/vo : 值对象 2) DAO : 数据访问对象
3) BO : 业务对象- 区分业务对象和数据访问对象:
1) DAO中的方法都是单精度方法或者称之为细粒度方法。什么叫单精度?一个方法只考虑一个操作,比如添加,那就是insert操作、查询那就是select操作…
2) BO中的方法属于业务方法,也实际的业务是比较复杂的,因此业务方法的粒度是比较粗的
注册这个功能属于业务功能,也就是说注册这个方法属于业务方法。
那么这个业务方法中包含了多个DAO方法。也就是说注册这个业务功能需要通过多个DAO方法的组合调用,从而完成注册功能的开发。
注册:
1. 检查用户名是否已经被注册 - DAO中的select操作
2. 向用户表新增一条新用户记录 - DAO中的insert操作
3. 向用户积分表新增一条记录(新用户默认初始化积分100分) - DAO中的insert操作
4. 向系统消息表新增一条记录(某某某新用户注册了,需要根据通讯录信息向他的联系人推送消息) - DAO中的insert操作
5. 向系统日志表新增一条记录(某用户在某IP在某年某月某日某时某分某秒某毫秒注册) - DAO中的insert操作
6. … - 在库存系统中添加业务层组件
- 区分业务对象和数据访问对象:
目的:将业务层每层逻辑分离,每层负责自己的业务逻辑,对每层的功能进行抽象,实现低解耦。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LOhZTUwn-1647596480642)(C:\Users\53026\AppData\Roaming\Typora\typora-user-images\image-20220317191332252.png)]
**controller:**业务层组件实现某大一模块的功能,如 用户模块的登录、注册等
**属性:**service
**service:**实现某一功能(CRUD+组装),功能颗粒度低
**属性:**DAOImpl || service
**DAOImpl:**高精度CRUD 获得所需的内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dtDafaCZ-1647596480642)(D:\JAVA\JavaWeb\Day7-mvc-ioc-servlet\素材\01.MVC04.png)]
1、配置applicationContext.xml文档
**目的:**注册每一层所要操纵的类,以及属性的类。为在服务端开始时进行注册,实例化所有需要的类。
如:
<!-- 这个bean标签的作用是 将来servletpath中涉及的名字对应的是fruit,那么就要FruitController这个类来处理 -->
<!--
1.概念
HTML : 超文本标记语言
XML : 可扩展的标记语言
HTML是XML的一个子集
2.XML包含三个部分:
1) XML声明 , 而且声明这一行代码必须在XML文件的第一行
2) DTD 文档类型定义
3) XML正文
-->
2、实现DispatcherServlet
通过反射技术获得具体所要使用的模块、以及操作方法。也就是对应的controller以及controller中对应的方法(实现的模块)
@WebServlet("*.do")
public class DepacherServLet extends ViewBaseServlet {
private BeanFactory beanFactory; // 获得所有需要使用到的对应的实例类
// 通过ioc模块实现
@Override
public void init() throws ServletException {
super.init();
// 提前保存在servletContext中
ServletContext servletContext = getServletContext();
Object beanFactoryObj = servletContext.getAttribute("beanFactory");
if (beanFactoryObj != null) {
beanFactory = (BeanFactory) beanFactoryObj;
} else {
throw new RuntimeException("IOC error!");
}
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String servletPath = req.getServletPath();// 获得请求的servletPath 即对应的controller
int endStr = servletPath.lastIndexOf(".do");
// 字符串截取获得 具体的servlet 即 controller 的名字,再通过map获得实例化对象
String servlet = servletPath.substring(1, endStr);
Object controllerObj = beanFactory.getBean(servlet);
// 获取用户端发来的操作
String oper = req.getParameter("operate");
// 默认去到index 页面
if (oper == null) {
oper = "index";
}
// 获得对应的controller的所有方法并且遍历 ==> 并于operate 进行匹配
for (Method method : controllerObj.getClass().getDeclaredMethods()) {
if (method.getName().equals(oper)) {
// 这一步肯定是获取方法的所有参数类型,并且给他设置值
Parameter[] parameters = method.getParameters();
Object[] paraVals = new Object[parameters.length];// 参数 集合
for (int i = 0; i < parameters.length; i++) {
// 常见所需要的 http类的
if (parameters[i].getName().equals("req")) {
paraVals[i] = req;
} else if (parameters[i].getName().equals("resp")) {
paraVals[i] = resp;
} else if (parameters[i].getName().equals("session")) {
paraVals[i] = req.getSession();
} else {
// 需要用户传的参数通过request获取
String paraVal = req.getParameter(parameters[i].getName());
String typerName = parameters[i].getType().getName();
Object paraObj = paraVal;
// 参数类型
if (paraObj != null) {
if (typerName.equals("java.lang.Integer")) {
paraObj = Integer.parseInt(paraVal);
} else if (typerName.equals("java.lang.Double")) {
paraObj = Double.parseDouble(paraVal);
}
}
paraVals[i] = paraObj;
}
}
try {
// 部署用户界面
method.setAccessible(true);
// 调用对应的方法
Object returnObj = method.invoke(controllerObj, paraVals);
// 获得返回值,其中包含 服务器的转发或者重新发送之类的内容
String returnString = (String) returnObj;
if(returnString.isEmpty()){
return;
}
// 对返回值的内容进行判断
if (returnString.indexOf(".do") != -1) {
String redirectStr = returnString.substring("redirect:".length());
resp.sendRedirect(redirectStr);
} else if (returnString.startsWith("json:")) {
resp.setCharacterEncoding("utf-8");
resp.setContentType("application/json;charset=utf-8");
String jsonStr = returnString.substring("json:".length());
PrintWriter out = resp.getWriter();
out.print(jsonStr);
out.flush();
} else {
super.processTemplate(returnString, req, resp);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
}
3、controller 与 service、DAOImpl
他们都是一层一层抽象其所要实现的功能
controller:
service:
Impl:
*重点ioc
1.耦合/依赖
依赖指的是某某某离不开某某某
在软件系统中,层与层之间是存在依赖的。我们也称之为耦合。
我们系统架构或者是设计的一个原则是: 高内聚低耦合。
层内部的组成应该是高度聚合的,而层与层之间的关系应该是低耦合的,最理想的情况0耦合(就是没有耦合)
2.IOC - 控制反转 / DI - 依赖注入
控制反转:
- 之前在Servlet中,我们创建service对象 , FruitService fruitService = new FruitServiceImpl();
这句话如果出现在servlet中的某个方法内部,那么这个fruitService的作用域(生命周期)应该就是这个方法级别;
如果这句话出现在servlet的类中,也就是说fruitService是一个成员变量,那么这个fruitService的作用域(生命周期)应该就是这个servlet实例级别 - 之后我们在applicationContext.xml中定义了这个fruitService。然后通过解析XML,产生fruitService实例,存放在beanMap中,这个beanMap在一个BeanFactory中
因此,我们转移(改变)了之前的service实例、dao实例等等他们的生命周期。控制权从程序员转移到BeanFactory。这个现象我们称之为控制反转
依赖注入:
1) 之前我们在控制层出现代码:FruitService fruitService = new FruitServiceImpl();
那么,控制层和service层存在耦合。
1) 之后,我们将代码修改成FruitService fruitService = null ;
然后,在配置文件中配置:
<bean id="fruit" class="FruitController">
<property name="fruitService" ref="fruitService"/>
</bean>
3. 目的:解除Dispatcher与注册组件的耦合
4.ioc的实现
public class ClassPathXmlApplicationContext implements BeanFactory {
private Map<String,Object> beanMap = new HashMap<>();
public ClassPathXmlApplicationContext(){
try {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
//1.创建DocumentBuilderFactory
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
//2.创建DocumentBuilder对象
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
//3.创建Document对象
Document document = documentBuilder.parse(inputStream);
//4.获取所有的bean节点
NodeList beanNodeList = document.getElementsByTagName("bean");
for(int i = 0 ; i<beanNodeList.getLength() ; i++){
Node beanNode = beanNodeList.item(i);
if(beanNode.getNodeType() == Node.ELEMENT_NODE){
Element beanElement = (Element)beanNode ;
String beanId = beanElement.getAttribute("id");
String className = beanElement.getAttribute("class");
Class beanClass = Class.forName(className);
//创建bean实例
Object beanObj = beanClass.newInstance() ;
//将bean实例对象保存到map容器中
beanMap.put(beanId , beanObj) ;
//到目前为止,此处需要注意的是,bean和bean之间的依赖关系还没有设置
}
}
//5.组装bean之间的依赖关系
for(int i = 0 ; i<beanNodeList.getLength() ; i++){
Node beanNode = beanNodeList.item(i);
if(beanNode.getNodeType() == Node.ELEMENT_NODE) {
Element beanElement = (Element) beanNode;
String beanId = beanElement.getAttribute("id");
NodeList beanChildNodeList = beanElement.getChildNodes();
for (int j = 0; j < beanChildNodeList.getLength() ; j++) {
Node beanChildNode = beanChildNodeList.item(j);
if(beanChildNode.getNodeType()==Node.ELEMENT_NODE && "property".equals(beanChildNode.getNodeName())){
Element propertyElement = (Element) beanChildNode;
String propertyName = propertyElement.getAttribute("name");
String propertyRef = propertyElement.getAttribute("ref");
//1) 找到propertyRef对应的实例
Object refObj = beanMap.get(propertyRef);
//2) 将refObj设置到当前bean对应的实例的property属性上去
Object beanObj = beanMap.get(beanId);
Class beanClazz = beanObj.getClass();
Field propertyField = beanClazz.getDeclaredField(propertyName);
propertyField.setAccessible(true);
propertyField.set(beanObj,refObj);
}
}
}
}
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
@Override
public Object getBean(String id) {
return beanMap.get(id);
}
}
其他组件(过滤器等)
1、过滤器(Filter)
-
Filter也属于Servlet规范
-
Filter开发步骤:新建类实现Filter接口,然后实现其中的三个方法:init、doFilter、destroy配置Filter,可以用注解@WebFilter,也可以使用xml文件
-
Filter在配置时,和servlet一样,也可以配置通配符,例如 @WebFilter("*.do")表示拦截所有以.do结尾的请求
-
过滤器链
1)执行的顺序依次是: A B C demo03 C2 B2 A2
2)如果采取的是注解的方式进行配置,那么过滤器链的拦截顺序是按照全类名的先后顺序排序的
3)如果采取的是xml的方式进行配置,那么按照配置的先后顺序进行排序
1)、设置全局信息性过滤器:
@WebFilter(urlPatterns = {"*.do"},initParams = {@WebInitParam(name = "encoding",value = "UTF-8")})
public class CharacterEncodingFilter implements Filter {
private String encoding = "UTF-8";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String encodingStr = filterConfig.getInitParameter("encoding");
if(StringUtil.isNotEmpty(encodingStr)){
encoding = encodingStr ;
}
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
((HttpServletRequest)servletRequest).setCharacterEncoding(encoding);
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
2)、事务管理型过滤器:
@WebFilter("*.do")
public class OpenSessionInViewFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try{
TransactionManager.beginTrans();
System.out.println("开启事务....");
filterChain.doFilter(servletRequest, servletResponse);
TransactionManager.commit();
System.out.println("提交事务...");
}catch (Exception e){
e.printStackTrace();
try {
TransactionManager.rollback();
System.out.println("回滚事务....");
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
@Override
public void destroy() {
}
}
3)限制访问过滤器
目的:防止非法访问信息管理页面,造成信息泄露
@WebFilter(urlPatterns = {"*.do","*.html"},
initParams = {
@WebInitParam(name = "accessible",
value = "/bookstore/page.do?operate=page&page=user/login," +
"/bookstore/page.do?operate=page&page=index," +
"/bookstore/user.do?null,/bookstore/book.do?null," +
"/bookstore/page.do?operate=page&page=user/regist,")
})
public class SessionFilter implements Filter {
// 用户未登录时 可以访问的url白名单
List<String> accessibleList;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化白名单 , 从 配置文档或者注解中获取value
String[] list = filterConfig.getInitParameter("accessible").split(",");
accessibleList = Arrays.asList(list);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 获取访问的url
String uri = request.getRequestURI() ;
String queryString = request.getQueryString() ;
String str = uri + "?" + queryString ;
// 组装url
if(accessibleList.contains(str)){
filterChain.doFilter(request,response);
}else{
HttpSession session = request.getSession() ;
Object currUserObj = session.getAttribute("currUser");
// 对访问重定向
if(currUserObj==null){
response.sendRedirect("page.do?operate=page&page=user/login");
}else{
filterChain.doFilter(request,response);
}
}
}
@Override
public void destroy() {
}
}
2、事务管理:
1)涉及到的组件:
- OpenSessionInViewFilter
- TransactionManager
- ThreadLocal
- ConnUtil
- BaseDAO
2)ThreadLocal:
1)get() , set(obj)
2)ThreadLocal称之为本地线程 。 我们可以通过set方法在当前线程上存储数据、通过get方法在当前线程上获取数据
3)set方法源码分析:
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前的线程
ThreadLocalMap map = getMap(t); //每一个线程都维护各自的一个容器(ThreadLocalMap)
if (map != null)
map.set(this, value); //这里的key对应的是ThreadLocal,因为我们的组件中需要传输(共享)的对象可能会有多个(不止Connection)
else
createMap(t, value); //默认情况下map是没有初始化的,那么第一次往其中添加数据时,会去初始化
}
4)get方法源码分析:
public T get() {
Thread t = Thread.currentThread(); //获取当前的线程
ThreadLocalMap map = getMap(t); //获取和这个线程(企业)相关的ThreadLocalMap(也就是工作纽带的集合)
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //this指的是ThreadLocal对象,通过它才能知道是哪一个工作纽带
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value; //entry.value就可以获取到工具箱了
return result;
}
}
return setInitialValue();
}
3)Conn组件:
目的:获取与数据库的连接以及设置本地线程
public class ConnUtil {
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
public static final String DRIVER = "com.mysql.jdbc.Driver" ;
public static final String URL = "jdbc:mysql://localhost:3306/qqzonedb?useUnicode=true&characterEncoding=utf-8&useSSL=false";
public static final String USER = "root";
public static final String PWD = "123456" ;
private static Connection createConn(){
try {
//1.加载驱动
Class.forName(DRIVER);
//2.通过驱动管理器获取连接对象
return DriverManager.getConnection(URL, USER, PWD);
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return null ;
}
public static Connection getConn(){
Connection conn = threadLocal.get();
if(conn==null){
conn =createConn();
threadLocal.set(conn);
}
return threadLocal.get() ;
}
public static void closeConn() throws SQLException {
Connection conn = threadLocal.get();
if(conn==null){
return ;
}
if(!conn.isClosed()){
conn.close();
//threadLocal.set(null);
threadLocal.remove();
}
}
}
4)TranscManager:
管理整个事务:
public class TransactionManager {
//开启事务
public static void beginTrans() throws SQLException {
ConnUtil.getConn().setAutoCommit(false);
}
//提交事务
public static void commit() throws SQLException {
Connection conn = ConnUtil.getConn();
conn.commit();
ConnUtil.closeConn();
}
//回滚事务
public static void rollback() throws SQLException {
Connection conn = ConnUtil.getConn();
conn.rollback();
ConnUtil.closeConn();
}
}
5)OpenSessionFilter:
开启事务管理:
@WebFilter("*.do")
public class OpenSessionInViewFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try{
TransactionManager.beginTrans();
//System.out.println("开启事务....");
filterChain.doFilter(servletRequest, servletResponse);
TransactionManager.commit();
//System.out.println("提交事务...");
}catch (Exception e){
e.printStackTrace();
try {
TransactionManager.rollback();
//System.out.println("回滚事务....");
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
@Override
public void destroy() {
}
}
3、监听器(Listener)
-
ServletContextListener - 监听ServletContext对象的创建和销毁的过程
-
HttpSessionListener - 监听HttpSession对象的创建和销毁的过程
-
ServletRequestListener - 监听ServletRequest对象的创建和销毁的过程
-
ServletContextAttributeListener - 监听ServletContext的保存作用域的改动(add,remove,replace)
-
HttpSessionAttributeListener - 监听HttpSession的保存作用域的改动(add,remove,replace)
-
ServletRequestAttributeListener - 监听ServletRequest的保存作用域的改动(add,remove,replace)
-
HttpSessionBindingListener - 监听某个对象在Session域中的创建与移除
-
HttpSessionActivationListener - 监听某个对象在Session域中的序列化和反序列化
ContextLoaderListener:
监听上下文启动,在上下文启动的时候去创建IOC容器,然后将其保存到application作用域,中央控制器再从application作用域中去获取IOC容器
@WebListener
public class ContextLoaderListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
//1.获取ServletContext对象
ServletContext application = servletContextEvent.getServletContext();
//2.获取上下文的初始化参数
String path = application.getInitParameter("contextConfigLocation");
//3.创建IOC容器
BeanFactory beanFactory = new ClassPathXmlApplicationContext(path);
//4.将IOC容器保存到application作用域
application.setAttribute("beanFactory",beanFactory);
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
}
}
DB设计(后续补充):
数据库的范式:
1) 第一范式:列不可再分
2) 第二范式:一张表只表达一层含义(只描述一件事情)
3) 第三范式:表中的每一列和主键都是直接依赖关系,而不是间接依赖
数据库设计的范式和数据库的查询性能很多时候是相悖的,我们需要根据实际的业务情况做一个选择:
- 查询频次不高的情况下,我们更倾向于提高数据库的设计范式,从而提高存储效率
- 查询频次较高的情形,我们更倾向于牺牲数据库的规范度,降低数据库设计的范式,允许特定的冗余,从而提高查询的性能