前后端交互初探
一、基本事实
后端职责可以粗浅的理解为处理各种数据,那么处理数据就可以从下面几个方面考虑:
- 数据的来源
根据不同的数据来源,我们探究两个方面的内容:
- 数据的形式
- 数据的操作
当然,一通操作以后,各个来源的数据需要通信,所以还有一个:
- 数据的交互
二、数据来源
想要持久化存储数据,需要将数据存入数据库。
想要对数据进行各种丰富的处理,需要将数据存放进 Java 的各种数据结构中(List、Map...)。
还有一个来源就是用户在前端的输入。
1. 数据库
数据形式: 关系型数据库里面的存储可以见到的组织形式就是表。
数据操作: SQL 语言
2. Java 后端
数据形式: 类,数据结构
数据操作: 方法调用
3. 前端
数据形式: 字符串,JSON
数据操作: JavaScript
这里暂时默认大家独立的这三大件都有所了解了。
三、数据的交互
数据的来源有三个,一般都是以 Java 后端作为中间桥梁。我们一对一对的看。数据交互要达到的效果就是统一形式、互相操作。
1. 数据库与 Java 后端
数据库一般只是用来提供数据,所以交互的任务就落到了 Java 后端的身上。
数据库里面数据组织的形式是表,Java 里面是类,因为我们要把数据拿到 Java 里面去操作。一个很自然的想法就是,将表直接映射成 Java 里面的类,由于类只是一个结构,我们具体操作的是类的对象。所以我们更多地会说成将表映射成对象。由于这里的表特指关系型数据库的表,所以这种映射被称为 Object–relational mapping,即对象关系映射,简称 ORM.
那么表能变成对象吗?我们发现是可以的。因为表里的各个属性可以映射到对象的属性上来,每一条表的记录都可以映射成一个对象的各个属性。
理论上是可行的,那实际操作呢?Java 要想将表的记录转变成对象,那就只能通过一系列方法的执行来做到了。Java 不就那么点东西嘛,不是属性就是方法。
但是方法的实现要怎么写呢?让我们写一套和数据库交互的方法吗?反正是我就直接劝退了hhhh……
所以为了方便 Java 程序员操作数据库,JDK 和数据库厂商就一起提供了一套 API,供大家使用。既然是一套 API,那么就有规定的一套流程。由于数据库厂商的不同,细节可能不一样,但是流程是大体相同的。以下流程来自 Oracle 官网:
In general, to process any SQL statement with JDBC, you follow these steps:
- Establishing a connection. 创建连接,加载驱动
- Create a statement. 声明一个用于执行数据操作的对象 statement
- Execute the query. 通过 SQL 执行查询等操作
- Process the
ResultSetobject. 处理得到的结果对象 ResultSet- Close the connection. 关闭连接
当我们的数据库不发生变化的时候,我们的1、2、5等操作可能是不变的,我们在写代码时,不希望总关注这些机械的操作。我们可能更关心的是 3 和 4,3 是和数据库相关的查询等操作,4 是对表记录对应对象的操作,这就刚好对应了我们的关注点,将表记录转换成对象。甚至说,在我的数据库表是规范的域模型的情况下(稍后解释),我都不要写 SQL,直接根据对应的方法定义“猜”出我要执行的 SQL 语句,那么这样 Java 程序员就要轻松很多了。于是一大堆的 ORM 框架就应运而生了,帮我们简化了很多操作,像1,2,5这些操作只用配置一次,也提高了操作的安全性。
我们先来看这种不用写 SQL 语句的,我们使用 Spring Data JPA 作为一个例子,那么什么是 JPA 呢,这里就不展开了,反正咱不用写 SQL 语句了。
这里我们把官网的例子拿过来,代码看不懂没事,实现细节不是我们现在要讨论的重点内容。首先是咱们要操作的对象 Customer:
package com.example.accessingdatajpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
protected Customer() {}
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format(
"Customer[id=%d, firstName='%s', lastName='%s']",
id, firstName, lastName);
}
public Long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
对象有了,那么还有对象的一系列操作,只需要定义这些方法的接口:
package com.example.accessingdatajpa;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
Customer findById(long id);
}
接下来就是调用这些接口了:
package com.example.accessingdatajpa;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class AccessingDataJpaApplication {
private static final Logger log = LoggerFactory.getLogger(AccessingDataJpaApplication.class);
public static void main(String[] args) {
SpringApplication.run(AccessingDataJpaApplication.class);
}
@Bean
public CommandLineRunner demo(CustomerRepository repository) {
return (args) -> {
// save a few customers
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));
// fetch all customers
log.info("Customers found with findAll():");
log.info("-------------------------------");
for (Customer customer : repository.findAll()) {
log.info(customer.toString());
}
log.info("");
// fetch an individual customer by ID
Customer customer = repository.findById(1L);
log.info("Customer found with findById(1L):");
log.info("--------------------------------");
log.info(customer.toString());
log.info("");
// fetch customers by last name
log.info("Customer found with findByLastName('Bauer'):");
log.info("--------------------------------------------");
repository.findByLastName("Bauer").forEach(bauer -> {
log.info(bauer.toString());
});
// for (Customer bauer : repository.findByLastName("Bauer")) {
// log.info(bauer.toString());
// }
log.info("");
};
}
}
看起来代码很多,那是因为为了方便调试,定义了一个日志记录器,我们一般都喜欢用 System.out.println() 对不对(说实话这样真的显得不专业hhh),然后还使用了一些 Java 8 的 Lambda 表达式,所以看起来似乎有点费劲。但实际上,核心方法就是 saveXXX(),findXXX() 这些方法,这些方法就完成了对数据库的操作,在执行 saveXXX() 方法的时候,数据库的记录也会随之增加。剩下的就是业务逻辑,只调用几个方法就完成操作了,简直不要太爽好不好。
当然,这个程序要跑起来还有一个通常的启动类,由于代码实现不是本文的重点,感兴趣的同学可以自行去官网查看详细解释和原始源码。
等等,说好的表与对象的映射呢?表呢?映射逻辑呢?我这个接口里面的方法还没有实现怎么就可以调用了?
看到那一大堆你目前可能看不懂的注解了吧,看到继承的接口了吧,这些都为你的问号做了实现。所以在这种情况下,Java 程序员甚至不需要懂 SQL,就完成了和数据库的交互。可以概括如下:
- 定义实体类
- 写接口
- 调用接口
但是默认的这些方法所能够拥有的功能是有限的,对于国内的互联网公司来说,业务可能会十分复杂,数据访问量也会比较大,这时候良好的 SQL 可能就是必要的啦。虽然说 Spring Data JPA 支持自定义 SQL,但是这不是它的长处,在某些情境下是要受到限制的。于是虽然世界上整体流行使用 JPA,但是我们国内用的不多。
于是我们需要一种对 SQL 支持良好的框架,程序员的注意力可以从写 Java 代码转到了写 SQL 上。这款框架就是 MyBatis.
MyBatis 根据你所写的 SQL 语句去生成对应的对象,并且支持动态 SQL,这是它的主要功能。
那么 MyBatis 的步骤就是多了几步:
- 定义实体类
- 写接口
- 写 SQL
- 接口与 SQL 绑定
- 调用接口
在 MyBatis 官方文档里有这样几句话:
MyBatis 创建时的一个思想是:数据库不可能永远是你所想或所需的那个样子。 我们希望每个数据库都具备良好的第三范式或 BCNF 范式,可惜它们并不都是那样。 如果能有一种数据库映射模式,完美适配所有的应用程序,那就太好了,但可惜也没有。 而 ResultMap 就是 MyBatis 对这个问题的答案。
比如,我们如何映射下面这个语句?
<!-- 非常复杂的语句 -->
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
select
B.id as blog_id,
B.title as blog_title,
B.author_id as blog_author_id,
A.id as author_id,
A.username as author_username,
A.password as author_password,
A.email as author_email,
A.bio as author_bio,
A.favourite_section as author_favourite_section,
P.id as post_id,
P.blog_id as post_blog_id,
P.author_id as post_author_id,
P.created_on as post_created_on,
P.section as post_section,
P.subject as post_subject,
P.draft as draft,
P.body as post_body,
C.id as comment_id,
C.post_id as comment_post_id,
C.name as comment_name,
C.comment as comment_text,
T.id as tag_id,
T.name as tag_name
from Blog B
left outer join Author A on B.author_id = A.id
left outer join Post P on B.id = P.blog_id
left outer join Comment C on P.id = C.post_id
left outer join Post_Tag PT on PT.post_id = P.id
left outer join Tag T on PT.tag_id = T.id
where B.id = #{id}
</select>
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。
不幸的是,Java 注解的表达能力和灵活性十分有限。尽管我们花了很多时间在调查、设计和试验上,但最强大的 MyBatis 映射并不能用注解来构建——我们真没开玩笑。而 C# 属性就没有这些限制,因此 MyBatis.NET 的配置会比 XML 有更大的选择余地。虽说如此,基于 Java 注解的配置还是有它的好处的。
所以啊,想偷懒还是不行滴。那有没有一种框架,在简单时,直接调用方法,复杂时,可以自定义 SQL 呢?有啊,它就是 MyBatis Plus. 它在 MyBatis 的基础上增加了一些常用的接口和功能。并且还有一个神奇的代码生成器,只需要在图形界面点两下,代码和映射文件都会帮你生成好,连代码都不需要写了。但是我个人不喜欢,每个人的业务不一样,生成的代码很是机械,需要自己修改很多。还不如自己写呢。
MyBatis 和 MyBatis Plus 的具体运用,在弄明白基本流程后,可以去看官方文档。
2. 后端对数据的处理
不管你是用什么方式从数据库拿到的数据,在 Java 里面就是一个个对象,这时候就可以拿 Java 语言对数据进行各种操作咯。那么处理完成以后,你还需要返回给前端。
3. 前后端数据交互
既然要返回给前端,那么数据形式仍然要统一。前端可以认识的形式就是 JSON,所以我们需要把 Java 对象转换成 JSON,数据格式如果统一的话,那么我们还要想的就是前后端如何通信。
前端每一个请求都对应一个URL,我们将 URL 和 Java 的方法建立映射,一旦在前端访问到 URL,我们就执行相应的后端方法处理相应的请求。
而这些工作在 Spring Boot 里面都有了很好的支持,我们通常使用注解来进行实现。
当然前端也可以发送 JSON 格式的数据给后端,后端转换为 Java 对象,再通过 ORM 框架完成存入数据库的操作。
四、后端分层的依据
我们可以看到,Java 后端承载着太多的使命:
- 数据库访问
- 数据处理
- 前后端交互
那么将每一项使命放在不同的包里,就对应的变成了 dao 层、service 层和 controller 层。
后端处理数据只是最基本的职责,还有很多东西我们需要去学习和探索。