前后端交互初探

369 阅读10分钟

前后端交互初探

一、基本事实

后端职责可以粗浅的理解为处理各种数据,那么处理数据就可以从下面几个方面考虑:

  • 数据的来源

根据不同的数据来源,我们探究两个方面的内容:

  • 数据的形式
  • 数据的操作

当然,一通操作以后,各个来源的数据需要通信,所以还有一个:

  • 数据的交互

二、数据来源

想要持久化存储数据,需要将数据存入数据库。

想要对数据进行各种丰富的处理,需要将数据存放进 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:

  1. Establishing a connection. 创建连接,加载驱动
  2. Create a statement. 声明一个用于执行数据操作的对象 statement
  3. Execute the query. 通过 SQL 执行查询等操作
  4. Process the ResultSet object. 处理得到的结果对象 ResultSet
  5. 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 层。

后端处理数据只是最基本的职责,还有很多东西我们需要去学习和探索。