Note/青空笔记/SpringBoot笔记/SpringBoot笔记(四).md
2023-08-31 11:30:31 +08:00

1007 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SpringBoot其他框架
通过了解其他的SpringBoot框架我们就可以在我们自己的Web服务器上实现更多更高级的功能。
## 邮件发送Mail
我们在注册很多的网站时,都会遇到邮件或是手机号验证,也就是通过你的邮箱或是手机短信去接受网站发给你的注册验证信息,填写验证码之后,就可以完成注册了,同时,网站也会绑定你的手机号或是邮箱。
那么像这样的功能我们如何实现呢SpringBoot已经给我们提供了封装好的邮件模块使用
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
```
### 邮件发送
在学习邮件发送之前,我们需要先了解一下什么是电子邮件。
电子邮件也是一种通信方式,是互联网应用最广的服务。通过网络的电子邮件系统,用户可以以非常低廉的价格(不管发送到哪里,都只需负担网费,实际上就是把信息发送到对方服务器而已)、非常快速的方式,与世界上任何一个地方的电子邮箱用户联系。
虽说方便倒是方便虽然是曾经的霸主不过现在这个时代QQ微信横行手机短信和电子邮箱貌似就只剩收验证码这一个功能了。
要在Internet上提供电子邮件功能必须有专门的电子邮件服务器。例如现在Internet很多提供邮件服务的厂商新浪、搜狐、163、QQ邮箱等他们都有自己的邮件服务器。这些服务器类似于现实生活中的邮局它主要负责接收用户投递过来的邮件并把邮件投递到邮件接收者的电子邮箱中。
所有的用户都可以在电子邮件服务器上申请一个账号用于邮件发送和接收那么邮件是以什么样的格式发送的呢实际上和Http一样邮件发送也有自己的协议也就是约定邮件数据长啥样以及如何通信。
![img](https://images2015.cnblogs.com/blog/851491/201612/851491-20161202143243756-1715308358.png)
比较常用的协议有两种:
1. SMTP协议主要用于发送邮件 Simple Mail Transfer Protocol
2. POP3协议主要用于接收邮件 Post Office Protocol 3
整个发送/接收流程大致如下:
![img](https://img2.baidu.com/it/u=3675146129,445744702&fm=253&fmt=auto&app=138&f=JPG?w=812&h=309)
实际上每个邮箱服务器都有一个smtp发送服务器和pop3接收服务器比如要从QQ邮箱发送邮件到163邮箱那么我们只需要通过QQ邮箱客户端告知QQ邮箱的smtp服务器我们需要发送邮件以及邮件的相关信息然后QQ邮箱的smtp服务器就会帮助我们发送到163邮箱的pop3服务器上163邮箱会通过163邮箱客户端告知对应用户收到一封新邮件。
而我们如果想要实现给别人发送邮件那么就需要连接到对应电子邮箱的smtp服务器上并告知其我们要发送邮件。而SpringBoot已经帮助我们将最基本的底层通信全部实现了我们只需要关心smtp服务器的地址以及我们要发送的邮件长啥样即可。
这里以163邮箱 https://mail.163.com 为例我们需要在配置文件中告诉SpringBootMail我们的smtp服务器的地址以及你的邮箱账号和密码首先我们要去设置中开启smtp/pop3服务才可以开启后会得到一个随机生成的密钥这个就是我们的密码。
```yaml
spring:
mail:
# 163邮箱的地址为smtp.163.com直接填写即可
host: smtp.163.com
# 你申请的163邮箱
username: javastudy111@163.com
# 注意密码是在开启smtp/pop3时自动生成的记得保存一下不然就找不到了
password: AZJTOAWZESLMHTNI
```
配置完成后,接着我们来进行一下测试:
```java
@SpringBootTest
class SpringBootTestApplicationTests {
//JavaMailSender是专门用于发送邮件的对象自动配置类已经提供了Bean
@Autowired
JavaMailSender sender;
@Test
void contextLoads() {
//SimpleMailMessage是一个比较简易的邮件封装支持设置一些比较简单内容
SimpleMailMessage message = new SimpleMailMessage();
//设置邮件标题
message.setSubject("【电子科技大学教务处】关于近期学校对您的处分决定");
//设置邮件内容
message.setText("XXX同学您好经监控和教务巡查发现您近期存在旷课、迟到、早退、上课刷抖音行为" +
"现已通知相关辅导员请手写5000字书面检讨并在2022年4月1日17点前交到辅导员办公室。");
//设置邮件发送给谁可以多个这里就发给你的QQ邮箱
message.setTo("你的QQ号@qq.com");
//邮件发送者,这里要与配置文件中的保持一致
message.setFrom("javastudy111@163.com");
//OK万事俱备只欠发送
sender.send(message);
}
}
```
如果需要添加附件等更多功能可以使用MimeMessageHelper来帮助我们完成
```java
@Test
void contextLoads() throws MessagingException {
//创建一个MimeMessage
MimeMessage message = sender.createMimeMessage();
//使用MimeMessageHelper来帮我们修改MimeMessage中的信息
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setSubject("Test");
helper.setText("lbwnb");
helper.setTo("你的QQ号@qq.com");
helper.setFrom("javastudy111@163.com");
//发送修改好的MimeMessage
sender.send(message);
}
```
### 邮件注册
既然我们已经了解了邮件发送,那么我们接着来看如何在我们的项目中实现邮件验证。
首先明确验证流程:请求验证码 -> 生成验证码(临时有效,注意设定过期时间) -> 用户输入验证码并填写注册信息 -> 验证通过注册成功!
***
![点击查看图片来源](https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic1.zhimg.com%2Fv2-6f0b9bb234b2534ec295ff195bad183a_1440w.jpg%3Fsource%3D172ae18b&refer=http%3A%2F%2Fpic1.zhimg.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1645108924&t=d40aa5dc6be398725b4ff21ef5895454)
## 持久层框架JPA
* 用了Mybatis之后你看那个JDBC真是太逊了。
* 这么说,你的项目很勇哦?
* 开玩笑,我的写代码超勇的好不好。
* 阿伟你可曾幻想过有一天你的项目里不再有SQL语句
* 不再有SQL语句那我怎么和数据库交互啊
* 我看你是完全不懂哦
* 懂,懂什么啊?
* 你想懂?来,到我项目里来,我给你看点好康的。
* 好康?是什么新框架哦?
* 什么新框架比新框架还刺激还可以让你的项目登duang郎哦。
*杰哥你项目里面都没SQL语句诶这是用的什么框架啊
在我们之前编写的项目中我们不难发现实际上大部分的数据库交互操作到最后都只会做一个事情那就是把数据库中的数据映射为Java中的对象。比如我们要通过用户名去查找对应的用户或是通过ID查找对应的学生信息在使用Mybatis时我们只需要编写正确的SQL语句就可以直接将获取的数据映射为对应的Java对象通过调用Mapper中的方法就能直接获得实体类这样就方便我们在Java中数据库表中的相关信息了。
但是以上这些操作都有一个共性那就是它们都是通过某种条件去进行查询而最后的查询结果都是一个实体类所以你会发现你写的很多SQL语句都是一个套路`select * from xxx where xxx=xxx`那么能否有一种框架帮我们把这些相同的套路给封装起来直接把这类相似的SQL语句给屏蔽掉不再由我们编写而是让框架自己去组合拼接。
### 认识SpringDataJPA
首先我们来看一个国外的统计:
![image-20220119140326867](/Users/nagocoler/Library/Application Support/typora-user-images/image-20220119140326867.png)
不对吧为什么Mybatis这么好用这么强大却只有10%的人喜欢呢然而事实就是在国外JPA几乎占据了主导地位而Mybatis并不像国内那样受待见所以你会发现JPA都有SpringBoot的官方直接提供的starter而Mybatis没有。
至于为啥SSM阶段不讲这个而是放到现在来讲也是因为在微服务场景下它的优势才能更多的发挥出来。
那么什么是JPA
JPAJava Persistence API和JDBC类似也是官方定义的一组接口但是它相比传统的JDBC它是为了实现ORM而生的即Object-Relationl Mapping它的作用是在关系型数据库和对象之间形成一个映射这样我们在具体的操作数据库的时候就不需要再去和复杂的SQL语句打交道只要像平时操作对象一样操作它就可以了。
在之前我们使用JDBC或是Mybatis来操作数据通过直接编写对应的SQL语句来实现数据访问但是我们发现实际上我们在Java中大部分操作数据库的情况都是读取数据并封装为一个实体类因此为什么不直接将实体类直接对应到一个数据库表呢也就是说一张表里面有什么属性那么我们的对象就有什么属性所有属性跟数据库里面的字段一一对应而读取数据时只需要读取一行的数据并封装为我们定义好的实体类既可以而具体的SQL语句执行完全可以交给框架根据我们定义的映射关系去生成不再由我们去编写因为这些SQL实际上都是千篇一律的。
而实现JPA规范的框架一般最常用的就是`Hibernate`它是一个重量级框架学习难度相比Mybatis也更高一些而SpringDataJPA也是采用Hibernate框架作为底层实现并对其加以封装。
官网https://spring.io/projects/spring-data-jpa
### 使用JPA
同样的我们只需要导入stater依赖即可
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
```
接着我们可以直接创建一个类,比如账户类,我们只需要把一个账号对应的属性全部定义好即可:
```java
@Data
public class Account {
int id;
String username;
String password;
}
```
接着我们可以通过注解形式在属性上添加数据库映射关系这样就能够让JPA知道我们的实体类对应的数据库表长啥样。
```java
@Data
@Entity //表示这个类是一个实体类
@Table(name = "users") //对应的数据库中表名称
public class Account {
@GeneratedValue(strategy = GenerationType.IDENTITY) //生成策略,这里配置为自增
@Column(name = "id") //对应表中id这一列
@Id //此属性为主键
int id;
@Column(name = "username") //对应表中username这一列
String username;
@Column(name = "password") //对应表中password这一列
String password;
}
```
接着我们来修改一下配置文件:
```yaml
spring:
jpa:
#开启SQL语句执行日志信息
show-sql: true
hibernate:
#配置为自动创建
ddl-auto: create
```
`ddl-auto`属性用于设置自动表定义可以实现自动在数据库中为我们创建一个表表的结构会根据我们定义的实体类决定它有4种
* create 启动时删数据库中的表,然后创建,退出时不删除数据表
* create-drop 启动时删数据库中的表,然后创建,退出时删除数据表 如果表不存在报错
* update 如果启动时表格式不一致则更新表,原有数据保留
* validate 项目启动表结构进行校验 如果不一致则报错
我们可以在日志中发现在启动时执行了如下SQL语句
```
Hibernate: create table users (id integer not null auto_increment, password varchar(255), username varchar(255), primary key (id)) engine=InnoDB
```
而我们的数据库中对应的表已经创建好了。
我们接着来看如何访问我们的表我们需要创建一个Repository实现类
```java
@Repository
public interface AccountRepository extends JpaRepository<Account, Integer> {
}
```
注意JpaRepository有两个泛型前者是具体操作的对象实体也就是对应的表后者是ID的类型接口中已经定义了比较常用的数据库操作。编写接口继承即可我们可以直接注入此接口获得实现类
```java
@SpringBootTest
class JpaTestApplicationTests {
@Resource
AccountRepository repository;
@Test
void contextLoads() {
//直接根据ID查找
repository.findById(1).ifPresent(System.out::println);
}
}
```
运行后,成功得到查询结果。我们接着来测试增删操作:
```java
@Test
void addAccount(){
Account account = new Account();
account.setUsername("Admin");
account.setPassword("123456");
account = repository.save(account); //返回的结果会包含自动生成的主键值
System.out.println("插入时自动生成的主键ID为"+account.getId());
}
```
```java
@Test
void deleteAccount(){
repository.deleteById(2); //根据ID删除对应记录
}
```
```java
@Test
void pageAccount() {
repository.findAll(PageRequest.of(0, 1)).forEach(System.out::println); //直接分页
}
```
我们发现使用了JPA之后整个项目的代码中没有出现任何的SQL语句可以说是非常方便了JPA依靠我们提供的注解信息自动完成了所有信息的映射和关联。
相比MybatisJPA几乎就是一个全自动的ORM框架而Mybatis则顶多算是半自动ORM框架。
### 方法名称拼接自定义SQL
虽然接口预置的方法使用起来非常方便但是如果我们需要进行条件查询等操作或是一些判断就需要自定义一些方法来实现同样的我们不需要编写SQL语句而是通过方法名称的拼接来实现条件判断这里列出了所有支持的条件判断名称
| `Distinct` | `findDistinctByLastnameAndFirstname` | `select distinct … where x.lastname = ?1 and x.firstname = ?2` |
| ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `And` | `findByLastnameAndFirstname` | `… where x.lastname = ?1 and x.firstname = ?2` |
| `Or` | `findByLastnameOrFirstname` | `… where x.lastname = ?1 or x.firstname = ?2` |
| `Is``Equals` | `findByFirstname`,`findByFirstnameIs`,`findByFirstnameEquals` | `… where x.firstname = ?1` |
| `Between` | `findByStartDateBetween` | `… where x.startDate between ?1 and ?2` |
| `LessThan` | `findByAgeLessThan` | `… where x.age < ?1` |
| `LessThanEqual` | `findByAgeLessThanEqual` | `… where x.age <= ?1` |
| `GreaterThan` | `findByAgeGreaterThan` | `… where x.age > ?1` |
| `GreaterThanEqual` | `findByAgeGreaterThanEqual` | `… where x.age >= ?1` |
| `After` | `findByStartDateAfter` | `… where x.startDate > ?1` |
| `Before` | `findByStartDateBefore` | `… where x.startDate < ?1` |
| `IsNull``Null` | `findByAge(Is)Null` | `… where x.age is null` |
| `IsNotNull``NotNull` | `findByAge(Is)NotNull` | `… where x.age not null` |
| `Like` | `findByFirstnameLike` | `… where x.firstname like ?1` |
| `NotLike` | `findByFirstnameNotLike` | `… where x.firstname not like ?1` |
| `StartingWith` | `findByFirstnameStartingWith` | `… where x.firstname like ?1`(参数与附加`%`绑定) |
| `EndingWith` | `findByFirstnameEndingWith` | `… where x.firstname like ?1`(参数与前缀`%`绑定) |
| `Containing` | `findByFirstnameContaining` | `… where x.firstname like ?1`(参数绑定以`%`包装) |
| `OrderBy` | `findByAgeOrderByLastnameDesc` | `… where x.age = ?1 order by x.lastname desc` |
| `Not` | `findByLastnameNot` | `… where x.lastname <> ?1` |
| `In` | `findByAgeIn(Collection<Age> ages)` | `… where x.age in ?1` |
| `NotIn` | `findByAgeNotIn(Collection<Age> ages)` | `… where x.age not in ?1` |
| `True` | `findByActiveTrue()` | `… where x.active = true` |
| `False` | `findByActiveFalse()` | `… where x.active = false` |
| `IgnoreCase` | `findByFirstnameIgnoreCase` | `… where UPPER(x.firstname) = UPPER(?1)` |
比如我们想要实现根据用户名模糊匹配查找用户:
```java
@Repository
public interface AccountRepository extends JpaRepository<Account, Integer> {
//按照表中的规则进行名称拼接不用刻意去记IDEA会有提示
List<Account> findAllByUsernameLike(String str);
}
```
```java
@Test
void test() {
repository.findAllByUsernameLike("%T%").forEach(System.out::println);
}
```
又比如我们想同时根据用户名和ID一起查询
```java
@Repository
public interface AccountRepository extends JpaRepository<Account, Integer> {
Account findByIdAndUsername(int id, String username);
//可以使用Optional类进行包装Optional<Account> findByIdAndUsername(int id, String username);
List<Account> findAllByUsernameLike(String str);
}
```
```java
@Test
void test() {
System.out.println(repository.findByIdAndUsername(1, "Test"));
}
```
比如我们想判断数据库中是否存在某个ID的用户
```java
@Repository
public interface AccountRepository extends JpaRepository<Account, Integer> {
Account findByIdAndUsername(int id, String username);
List<Account> findAllByUsernameLike(String str);
boolean existsAccountById(int id);
}
```
```java
@Test
void test() {
System.out.println(repository.existsAccountByUsername("Test"));
}
```
注意自定义条件操作的方法名称一定要遵循规则,不然会出现异常:
```
Caused by: org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract ...
```
### 关联查询
在实际开发中,比较常见的场景还有关联查询,也就是我们会在表中添加一个外键字段,而此外键字段又指向了另一个表中的数据,当我们查询数据时,可能会需要将关联数据也一并获取,比如我们想要查询某个用户的详细信息,一般用户简略信息会单独存放一个表,而用户详细信息会单独存放在另一个表中。当然,除了用户详细信息之外,可能在某些电商平台还会有用户的购买记录、用户的购物车,交流社区中的用户帖子、用户评论等,这些都是需要根据用户信息进行关联查询的内容。
![img](https://img1.baidu.com/it/u=292198351,4011695440&fm=253&fmt=auto&app=138&f=JPEG?w=404&h=436)
我们知道在JPA中每张表实际上就是一个实体类的映射而表之间的关联关系也可以看作对象之间的依赖关系比如用户表中包含了用户详细信息的ID字段作为外键那么实际上就是用户表实体中包括了用户详细信息实体对象
```java
@Data
@Entity
@Table(name = "users_detail")
public class AccountDetail {
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
int id;
@Column(name = "address")
String address;
@Column(name = "email")
String email;
@Column(name = "phone")
String phone;
@Column(name = "real_name")
String realName;
}
```
而用户信息和用户详细信息之间形成了一对一的关系,那么这时我们就可以直接在类中指定这种关系:
```java
@Data
@Entity
@Table(name = "users")
public class Account {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
@Id
int id;
@Column(name = "username")
String username;
@Column(name = "password")
String password;
@JoinColumn(name = "detail_id") //指定存储外键的字段名称
@OneToOne //声明为一对一关系
AccountDetail detail;
}
```
在修改实体类信息后,我们发现在启动时也进行了更新,日志如下:
```
Hibernate: alter table users add column detail_id integer
Hibernate: create table users_detail (id integer not null auto_increment, address varchar(255), email varchar(255), phone varchar(255), real_name varchar(255), primary key (id)) engine=InnoDB
Hibernate: alter table users add constraint FK7gb021edkxf3mdv5bs75ni6jd foreign key (detail_id) references users_detail (id)
```
是不是感觉非常方便!都懒得去手动改表结构了。
接着我们往用户详细信息中添加一些数据,一会我们可以直接进行查询:
```java
@Test
void pageAccount() {
repository.findById(1).ifPresent(System.out::println);
}
```
查询后,可以发现,得到如下结果:
```
Hibernate: select account0_.id as id1_0_0_, account0_.detail_id as detail_i4_0_0_, account0_.password as password2_0_0_, account0_.username as username3_0_0_, accountdet1_.id as id1_1_1_, accountdet1_.address as address2_1_1_, accountdet1_.email as email3_1_1_, accountdet1_.phone as phone4_1_1_, accountdet1_.real_name as real_nam5_1_1_ from users account0_ left outer join users_detail accountdet1_ on account0_.detail_id=accountdet1_.id where account0_.id=?
Account(id=1, username=Test, password=123456, detail=AccountDetail(id=1, address=四川省成都市青羊区, email=8371289@qq.com, phone=1234567890, realName=本伟))
```
也就是在建立关系之后我们查询Account对象时会自动将关联数据的结果也一并进行查询。
那要是我们只想要Account的数据不想要用户详细信息数据怎么办呢我希望在我要用的时候再获取详细信息这样可以节省一些网络开销我们可以设置懒加载这样只有在需要时才会向数据库获取
```java
@JoinColumn(name = "detail_id")
@OneToOne(fetch = FetchType.LAZY) //将获取类型改为LAZY
AccountDetail detail;
```
接着我们测试一下:
```java
@Transactional //懒加载属性需要在事务环境下获取因为repository方法调用完后Session会立即关闭
@Test
void pageAccount() {
repository.findById(1).ifPresent(account -> {
System.out.println(account.getUsername()); //获取用户名
System.out.println(account.getDetail()); //获取详细信息(懒加载)
});
}
```
接着我们来看看控制台输出了什么:
```
Hibernate: select account0_.id as id1_0_0_, account0_.detail_id as detail_i4_0_0_, account0_.password as password2_0_0_, account0_.username as username3_0_0_ from users account0_ where account0_.id=?
Test
Hibernate: select accountdet0_.id as id1_1_0_, accountdet0_.address as address2_1_0_, accountdet0_.email as email3_1_0_, accountdet0_.phone as phone4_1_0_, accountdet0_.real_name as real_nam5_1_0_ from users_detail accountdet0_ where accountdet0_.id=?
AccountDetail(id=1, address=四川省成都市青羊区, email=8371289@qq.com, phone=1234567890, realName=卢本)
```
可以看到获取用户名之前并没有去查询用户的详细信息而是当我们获取详细信息时才进行查询并返回AccountDetail对象。
那么我们是否也可以在添加数据时,利用实体类之间的关联信息,一次性添加两张表的数据呢?可以,但是我们需要稍微修改一下级联关联操作设定:
```java
@JoinColumn(name = "detail_id")
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) //设置关联操作为ALL
AccountDetail detail;
```
* ALL所有操作都进行关联操作
* PERSIST插入操作时才进行关联操作
* REMOVE删除操作时才进行关联操作
* MERGE修改操作时才进行关联操作
可以多个并存,接着我们来进行一下测试:
```java
@Test
void addAccount(){
Account account = new Account();
account.setUsername("Nike");
account.setPassword("123456");
AccountDetail detail = new AccountDetail();
detail.setAddress("重庆市渝中区解放碑");
detail.setPhone("1234567890");
detail.setEmail("73281937@qq.com");
detail.setRealName("张三");
account.setDetail(detail);
account = repository.save(account);
System.out.println("插入时自动生成的主键ID为"+account.getId()+"外键ID为"+account.getDetail().getId());
}
```
可以看到日志结果:
```
Hibernate: insert into users_detail (address, email, phone, real_name) values (?, ?, ?, ?)
Hibernate: insert into users (detail_id, password, username) values (?, ?, ?)
插入时自动生成的主键ID为6外键ID为3
```
结束后会发现数据库中两张表都同时存在数据。
接着我们来看一对多关联,比如每个用户的成绩信息:
```java
@JoinColumn(name = "uid") //注意这里的name指的是Score表中的uid字段对应的就是当前的主键会将uid外键设置为当前的主键
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) //在移除Account时一并移除所有的成绩信息依然使用懒加载
List<Score> scoreList;
```
```java
@Data
@Entity
@Table(name = "users_score") //成绩表注意只存成绩不存学科信息学科信息id做外键
public class Score {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
@Id
int id;
@OneToOne //一对一对应到学科上
@JoinColumn(name = "cid")
Subject subject;
@Column(name = "socre")
double score;
@Column(name = "uid")
int uid;
}
```
```java
@Data
@Entity
@Table(name = "subjects") //学科信息表
public class Subject {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cid")
@Id
int cid;
@Column(name = "name")
String name;
@Column(name = "teacher")
String teacher;
@Column(name = "time")
int time;
}
```
在数据库中填写相应数据,接着我们就可以查询用户的成绩信息了:
```java
@Transactional
@Test
void test() {
repository.findById(1).ifPresent(account -> {
account.getScoreList().forEach(System.out::println);
});
}
```
成功得到用户所有的成绩信息,包括得分和学科信息。
同样的,我们还可以将对应成绩中的教师信息单独分出一张表存储,并建立多对一的关系,因为多门课程可能由同一个老师教授(千万别搞晕了,一定要理清楚关联关系,同时也是考验你的基础扎不扎实):
```java
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tid") //存储教师ID的字段和一对一是一样的也会当前表中创个外键
Teacher teacher;
```
接着就是教师实体类了:
```java
@Data
@Entity
@Table(name = "teachers")
public class Teacher {
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
int id;
@Column(name = "name")
String name;
@Column(name = "sex")
String sex;
}
```
最后我们再进行一下测试:
```java
@Transactional
@Test
void test() {
repository.findById(3).ifPresent(account -> {
account.getScoreList().forEach(score -> {
System.out.println("课程名称:"+score.getSubject().getName());
System.out.println("得分:"+score.getScore());
System.out.println("任课教师:"+score.getSubject().getTeacher().getName());
});
});
}
```
成功得到多对一的教师信息。
最后我们再来看最复杂的情况,现在我们一门课程可以由多个老师教授,而一个老师也可以教授多个课程,那么这种情况就是很明显的多对多场景,现在又该如何定义呢?我们可以像之前一样,插入一张中间表表示教授关系,这个表中专门存储哪个老师教哪个科目:
```java
@ManyToMany(fetch = FetchType.LAZY) //多对多场景
@JoinTable(name = "teach_relation", //多对多中间关联表
joinColumns = @JoinColumn(name = "cid"), //当前实体主键在关联表中的字段名称
inverseJoinColumns = @JoinColumn(name = "tid") //教师实体主键在关联表中的字段名称
)
List<Teacher> teacher;
```
接着JPA会自动创建一张中间表并自动设置外键我们就可以将多对多关联信息编写在其中了。
### JPQL自定义SQL语句
虽然SpringDataJPA能够简化大部分数据获取场景但是难免会有一些特殊的场景需要使用复杂查询才能够去完成这时你又会发现如果要实现只能用回Mybatis了因为我们需要自己手动编写SQL语句过度依赖SpringDataJPA会使得SQL语句不可控。
使用JPA我们也可以像Mybatis那样直接编写SQL语句不过它是JPQL语言与原生SQL语句很类似但是它是面向对象的当然我们也可以编写原生SQL语句。
比如我们要更新用户表中指定ID用户的密码
```java
@Repository
public interface AccountRepository extends JpaRepository<Account, Integer> {
@Transactional //DML操作需要事务环境可以不在这里声明但是调用时一定要处于事务环境下
@Modifying //表示这是一个DML操作
@Query("update Account set password = ?2 where id = ?1") //这里操作的是一个实体类对应的表,参数使用?代表后面接第n个参数
int updatePasswordById(int id, String newPassword);
}
```
```java
@Test
void updateAccount(){
repository.updatePasswordById(1, "654321");
}
```
现在我想使用原生SQL来实现根据用户名称修改密码
```java
@Transactional
@Modifying
@Query(value = "update users set password = :pwd where username = :name", nativeQuery = true) //使用原生SQL和Mybatis一样这里使用 :名称 表示参数,当然也可以继续用上面那种方式。
int updatePasswordByUsername(@Param("name") String username, //我们可以使用@Param指定名称
@Param("pwd") String newPassword);
```
```java
@Test
void updateAccount(){
repository.updatePasswordByUsername("Admin", "654321");
}
```
通过编写原生SQL在一定程度上弥补了SQL不可控的问题。
虽然JPA能够为我们带来非常便捷的开发体验但是正式因为太便捷了保姆级的体验有时也会适得其反可能项目开发到后期特别庞大时就只能从底层SQL语句开始进行优化而由于JPA尽可能地在屏蔽我们对SQL语句的编写所以后期优化是个大问题并且Hibernate相对于Mybatis来说更加重量级。不过在微服务的时代单体项目一般不会太大而JPA的劣势并没有太明显地体现出来。
有关Mybatis和JPA的对比可以参考https://blog.csdn.net/u010253246/article/details/105731204
***
## Extra. 前后端分离跨域处理
我们的项目已经处于前后端分离状态了,那么前后端分离状态和我们之前的状态有什么区别的呢?
* **不分离:**前端页面看到的都是由后端控制由后端渲染页面或重定向后端需要控制前端的展示前端与后端的耦合度很高。比如我们之前都是使用后端来执行重定向操作或是使用Thymeleaf来填充数据而最终返回的是整个渲染好的页。
![img](https://img2018.cnblogs.com/blog/1394466/201809/1394466-20180916231510365-285933655.png)
* **分离:**后端仅返回前端所需的数据不再渲染HTML页面不再控制前端的效果。至于前端用户看到什么效果从后端请求的数据如何加载到前端中都由前端通过JS等进行动态数据填充和渲染。这样后端只返回JSON数据前端处理JSON数据并展示这样前后端的职责就非常明确了。
![img](https://img2018.cnblogs.com/blog/1394466/201809/1394466-20180916231716242-1862208927.png)
实现前后端分离有两种方案一种是直接放入SpringBoot的资源文件夹下但是这样实际上还是在依靠SpringBoot内嵌的Tomcat服务器进行页面和静态资源的发送我们现在就是这种方案。
另一种方案就是直接将所有的页面和静态资源单独放到代理服务器上如Nginx这样我们后端服务器就不必再处理静态资源和页面了专心返回数据即可而前端页面就需要访问另一个服务器来获取虽然逻辑和明确但是这样会出现跨域问题实际上就是我们之前所说的跨站请求伪造为了防止这种不安全的行为发生所以对异步请求会进行一定的限制。
这里,我们将前端页面和后端页面直接分离进行测试,在登陆时得到如下错误:
```
Access to XMLHttpRequest at 'http://localhost:8080/api/auth/login' from origin 'http://localhost:63342' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
```
可以很清楚地看到在Ajax发送异步请求时我们的请求被阻止原因是在响应头中没有包含`Access-Control-Allow-Origin`,也就表示,如果服务端允许跨域请求,那么会在响应头中添加一个`Access-Control-Allow-Origin`字段,如果不允许跨域,就像现在这样。那么,什么才算是跨域呢:
1. 请求协议`如http、https`不同
2. 请求的地址/域名不同
3. 端口不同
因为我们现在相当于前端页面访问的是静态资源服务器而后端数据是由我们的SpringBoot项目提供它们是两个不同的服务器所以在垮服务器请求资源时会被判断为存在安全风险。
但是现在,由于我们前后端是分离状态,我们希望的是能够实现跨域请求,这时我们就需要添加一个过滤器来处理跨域问题:
```java
@Bean
public CorsFilter corsFilter() {
//创建CorsConfiguration对象后添加配置
CorsConfiguration config = new CorsConfiguration();
//设置放行哪些原始域,这里直接设置为所有
config.addAllowedOriginPattern("*");
//你可以单独设置放行哪些原始域 config.addAllowedOrigin("http://localhost:2222");
//放行哪些原始请求头部信息
config.addAllowedHeader("*");
//放行哪些请求方式,*代表所有
config.addAllowedMethod("*");
//是否允许发送Cookie必须要开启因为我们的JSESSIONID需要在Cookie中携带
config.setAllowCredentials(true);
//映射路径
UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**", config);
//返回CorsFilter
return new CorsFilter(corsConfigurationSource);
}
```
这样我们的SpringBoot项目就支持跨域访问了接着我们再来尝试进行登陆可以发现已经能够正常访问了并且响应头中包含了以下信息
```
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Allow-Origin: http://localhost:63342
Access-Control-Expose-Headers: *
Access-Control-Allow-Credentials: true
```
可以看到我们当前访问的原始域已经被放行了。
但是还有一个问题我们的Ajax请求中没有携带Cookie信息这个按理说属于前端知识了这里我们稍微改一下不然我们的请求无法确认身份
```js
function get(url, success){
$.ajax({
type: "get",
url: url,
async: true,
dataType: 'json',
xhrFields: {
withCredentials: true
},
success: success
});
}
function post(url, data, success){
$.ajax({
type: "post",
url: url,
async: true,
data: data,
dataType: 'json',
xhrFields: {
withCredentials: true
},
success: success
});
}
```
添加两个封装好的方法,并且将`withCredentials`开启这样在发送异步请求时就会携带Cookie信息了。
在学习完成Linux之后我们会讲解如何在Linux服务器上部署Nginx反向代理服务器。
***
## 接口管理Swagger
在前后端分离项目中前端人员需要知道我们后端会提供什么数据根据后端提供的数据来进行前端页面渲染在之前我们也演示过这个时候我们就需要编写一个API文档以便前端人员随时查阅。
但是这样的一个文档,我们也不可能单独写一个项目去进行维护,并且随着我们的后端项目不断更新,文档也需要跟随更新,这显然是很麻烦的一件事情,那么有没有一种比较好的解决方案呢?
当然有那就是丝袜哥Swagger
### 走进Swagger
Swagger的主要功能如下
- 支持 API 自动生成同步的在线文档:使用 Swagger 后可以直接通过代码生成文档,不再需要自己手动编写接口文档了,对程序员来说非常方便,可以节约写文档的时间去学习新技术。
- 提供 Web 页面在线测试 API光有文档还不够Swagger 生成的文档还支持在线测试。参数和格式都定好了,直接在界面上输入参数对应的值即可在线测试接口。
结合Spring框架Spring-foxSwagger可以很轻松地利用注解以及扫描机制来快速生成在线文档以实现当我们项目启动之后前端开发人员就可以打开Swagger提供的前端页面查看和测试接口。依赖如下
```xml
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
```
SpringBoot 2.6以上版本修改了路径匹配规则但是Swagger3还不支持这里换回之前的不然启动直接报错
```yaml
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
```
项目启动后我们可以直接打开http://localhost:8080/swagger-ui/index.html这个页面要是觉得丑UI是可以换的支持第三方会显示所有的API文档包括接口的路径、支持的方法、接口的描述等并且我们可以直接对API接口进行测试非常方便。
我们可以创建一个配置类去配置页面的相关信息:
```java
@Configuration
public class SwaggerConfiguration {
@Bean
public Docket docket() {
return new Docket(DocumentationType.OAS_30).apiInfo(
new ApiInfoBuilder()
.contact(new Contact("你的名字", "https://www.bilibili.com", "javastudy111*@163.com"))
.title("图书管理系统 - 在线API接口文档")
.build()
);
}
}
```
### 接口信息配置
虽然Swagger的UI界面已经可以很好地展示后端提供的接口信息了但是非常的混乱我们来看看如何配置接口的一些描述信息。
首先我们的页面中完全不需要显示ErrorController相关的API所以我们配置一下选择哪些Controller才会生成API信息
```java
@Bean
public Docket docket() {
ApiInfo info = new ApiInfoBuilder()
.contact(new Contact("你的名字", "https://www.bilibili.com", "javastudy111@163.com"))
.title("图书管理系统 - 在线API接口文档")
.description("这是一个图书管理系统的后端API文档欢迎前端人员查阅")
.build();
return new Docket(DocumentationType.OAS_30)
.apiInfo(info)
.select() //对项目中的所有API接口进行选择
.apis(RequestHandlerSelectors.basePackage("com.example.controller"))
.build();
}
```
接着我们来看看如何为一个Controller编写API描述信息
```java
@Api(tags = "账户验证接口", description = "包括用户登录、注册、验证码请求等操作。")
@RestController
@RequestMapping("/api/auth")
public class AuthApiController {
```
我们可以直接在类名称上面添加`@Api`注解并填写相关信息来为当前的Controller设置描述信息。
接着我们可以为所有的请求映射配置描述信息:
```java
@ApiResponses({
@ApiResponse(code = 200, message = "邮件发送成功"),
@ApiResponse(code = 500, message = "邮件发送失败") //不同返回状态码描述
})
@ApiOperation("请求邮件验证码") //接口描述
@GetMapping("/verify-code")
public RestBean<Void> verifyCode(@ApiParam("邮箱地址") //请求参数的描述
@RequestParam("email") String email){
```
```java
@ApiIgnore //忽略此请求映射
@PostMapping("/login-success")
public RestBean<Void> loginSuccess(){
return new RestBean<>(200, "登陆成功");
}
```
我们也可以为实体类配置相关的描述信息:
```java
@Data
@ApiModel(description = "响应实体封装类")
@AllArgsConstructor
public class RestBean<T> {
@ApiModelProperty("状态码")
int code;
@ApiModelProperty("状态码描述")
String reason;
@ApiModelProperty("数据实体")
T data;
public RestBean(int code, String reason) {
this.code = code;
this.reason = reason;
}
}
```
这样,我们就可以在文档中查看实体类简介以及各个属性的介绍了。
最后我们再配置一下多环境:
```xml
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<environment>dev</environment>
</properties>
</profile>
<profile>
<id>prod</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<properties>
<environment>prod</environment>
</properties>
</profile>
</profiles>
```
```xml
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>application*.yaml</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>application.yaml</include>
<include>application-${environment}.yaml</include>
</includes>
</resource>
</resources>
```
首先在Maven中添加两个环境接着我们配置一下不同环境的配置文件
```yaml
jpa:
show-sql: false
hibernate:
ddl-auto: update
springfox:
documentation:
enabled: false
```
在生产环境下我们选择不开启Swagger文档以及JPA的数据库操作日志这样我们就可以根据情况选择两套环境了。