设计共分以下几步:
①依赖导入(这个应该看需要导入)
②实体类设计(通过自动建表,减少数据库设计)
③数据操作设计(使用JpaRepository,减少大量麻烦)
④series层和其实现的设计
⑤一些测试的设计
①依赖导入(Gradle):
如果是maven也很简单,此处就不多做介绍了。
可以看出这里是2.3.4.RELEASE版本的springboot。
plugins {
id 'org.springframework.boot' version '2.3.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
compile group: 'com.alibaba', name: 'druid', version: '0.2.9'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
②实体类设计:
此部分一共有三个实体类:
Student、Clazz、Grade
注意:class容易出现误解、错误等问题,使用clazz代替班级class。
首先是Student实体类:
import lombok.*;
import org.hibernate.annotations.Type;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.UUID;
@Entity
@Table(name = "tb_student")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Student implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "suuid")
@Type(type = "uuid-char")
private UUID suuid;
@Column(columnDefinition = "int(10) not null UNIQUE auto_increment")
private Integer snumber;
@Column
private String sgender;
@Column
private String sname;
@Column
private LocalDate birthday;
@ManyToOne
@JoinColumn(name = "cname", referencedColumnName = "cname")
private Clazz clazz;
@Override
public String toString() {
return "Student:[uuid = " + getSuuid() +
" ,s_number = " + getSnumber() +
" ,s_name = " + getSname() +
" ,s_gender =" + getSgender() +
" ,s_birthday = " + getBirthday() +
" ,class_name = " + getClazz().getCname() +
"]";
}
}
然后是Clazz实体类:
import lombok.*;
import org.hibernate.annotations.Type;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;
import java.util.UUID;
@Entity
@Table(name = "tb_class")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Clazz implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "cuuid")
@Type(type = "uuid-char")
private UUID cuuid;
@Column
private String cname;
@ManyToOne
@JoinColumn(name = "gName", referencedColumnName = "gName")
private Grade grade;
@Column
@OneToMany(cascade = CascadeType.ALL, mappedBy = "clazz", fetch = FetchType.EAGER)
private Set<Student> studentList;
@Override
public String toString() {
return "Clazz:[uuid = " + getCuuid() +
" ,c_name = " + getCname() +
" ,Grade_name = " + getGrade().getGname() +
"]";
}
}
最后是Grade实体类:
import lombok.*;
import org.hibernate.annotations.Type;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;
import java.util.UUID;
@Entity
@Table(name = "tb_grade")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
public class Grade implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "guuid")
@Type(type = "uuid-char")
private UUID guuid;
@Column
private String gname;
@Column
@OneToMany(cascade = CascadeType.ALL,mappedBy = "grade",fetch = FetchType.EAGER)
private Set<Clazz> clazzList;
}
需要注意的是:我这里没有重写hashcode和equals,有需要的话记得重写;另外如果需要特殊getter、setter自己重写;我这里的Serializable接口在一开始的demo中并不需要,再后来添加了一些东西以后不添加就会报错。这个接口就类似于一个标签,告诉它我需要序列化或者反序列化。
还有一对多的时候我尝试将fetch改为lazy,但是会报错,另外如果是UUID类型保存(JpaRepository)进mysql(8.0.12)的时候会变成乱码,于是添加了@Type注解,还有就是想要一个int类型的自增键,当做学号,我用的是@Column(columnDefinition = "int(10) not null UNIQUE auto_increment")完成的,因为主键不是学号,是UUID,所以在主键是AUTO的时候,如果再去给学号添加@GeneratedValue(strategy = GenerationType.IDENTITY),虽然不会报错,但是并没有用,所以在经过尝试以后这种方式是可以实现非主键自增的。
【实体类属于dao层中的dbo,另外还有dto,不过在这个demo中没有写明,在另一个地方有,但是我嫌麻烦就懒得去找了,这里我贴上这个demo的git地址:https://github.com/Tezzxamo/demoStudent.git,有刚学的同学可以新建一个文件夹,然后在文件夹中打开gitbash,使用git命令clone下来,或者直接在idea中点击后在url中输入即可。】
【note:在postgres数据库中不能使用@Column(columnDefinition = "int(10) not null UNIQUE auto_increment")】
③数据操作设计:
每一个实体类对应一个repo:
import com.example.demo.dao.dbo.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Set;
import java.util.UUID;
public interface StudentRepo extends JpaRepository<Student, UUID> {
Set<Student> findByClazzCname(String cname);
}
import com.example.demo.dao.dbo.Clazz;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
public interface ClazzRepo extends JpaRepository<Clazz, UUID> {
Set<Clazz> findByGradeGname(String gname);
Optional<Clazz> findByCname(String cname);
Optional<Clazz> findByCnameAndGradeGname(String cname,String gname);
}
import com.example.demo.dao.dbo.Grade;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface GradeRepo extends JpaRepository<Grade, UUID> {
Optional<Grade> findByGname(String gname);
}
注意:我在这里写到的都是我写demo时候用到的,如果你不需要你也可以删除,你也可以新建,而且不需要写实现方法,因为Jpa会帮你做到,但是前提是你写的方法名符合jpa的要求,一般来说idea会有提示(但也不排除有些不会有提示,那你就自求多福吧,我也不知道解决办法,如果你知道的话记得评论告诉我,我遇到过没有的,但我自己用的时候有),有提示的,jpa一般都会帮你做到(不排除个别情况),有了jpa,就可以少些很多实现、sql等。
但这并不意味着impl不存在了,impl是很重要的,另外repo也不能代替service,因为很多业务逻辑仅靠jpa也是实现不了的,所以这个时候service和service.impl就有了它们的作用。
④series层和其实现的设计:
我在刚写这个demo的时候,是每个实体类对应了一个service,但是最后我将它们合在了一起,但是合在一起的那个demo现在(写博客的时候)不在我的手上,也不在git上,所以就当做是留一个练习题,刚学习的同学,可以考虑如何将三个service合在一起并实现。
import com.example.demo.dao.dbo.Student;
import java.util.Set;
public interface StudentService {
Set<Student> findByGradeGname(String gname) throws Exception;
}
import com.example.demo.dao.dbo.Clazz;
import java.util.Map;
import java.util.Set;
public interface ClazzService {
String sexRatioOne(String cname) throws Exception;
String sexRatioTwo(String cname) throws Exception;
Boolean updateClazzWithGrade(String cname,String gname) throws Exception;
Map<String, Set<Clazz>> classify();
Boolean existByName(String cname);
Boolean existByCnameAndGname(String cname,String gname);
}
import com.example.demo.dao.dbo.Clazz;
import java.util.Map;
import java.util.Set;
public interface GradeService {
Map<String, Set<Clazz>> classify();
Boolean existByName(String gname);
}
注意:我写的service中的方法不一定很好,有些甚至是冗余的,只不过一开始写的时候顺手就写上去了。
注意:import里面的内容要记得改,比如我的Student实体类的路径可能就和你不一样,除非你git上down的代码。
import com.example.demo.dao.dbo.Clazz;
import com.example.demo.dao.dbo.Student;
import com.example.demo.dao.repo.ClazzRepo;
import com.example.demo.dao.repo.GradeRepo;
import com.example.demo.dao.repo.StudentRepo;
import com.example.demo.service.GradeService;
import com.example.demo.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class StudentServiceImpl implements StudentService {
StudentRepo studentRepo;
ClazzRepo clazzRepo;
GradeRepo gradeRepo;
GradeService gradeService;
@Autowired
public StudentServiceImpl(StudentRepo studentRepo,
ClazzRepo clazzRepo,
GradeRepo gradeRepo,
GradeService gradeService) {
this.studentRepo = studentRepo;
this.clazzRepo = clazzRepo;
this.gradeRepo = gradeRepo;
this.gradeService = gradeService;
}
@Override
public Set<Student> findByGradeGname(String gname) throws Exception {
// 校验
if (!gradeService.existByName(gname)) {
throw new Exception("没有该年级");
}
Set<Student> studentSet = new HashSet<>();
//流的方式获取studentSet然后返回
clazzRepo.findByGradeGname(gname)
.stream()
.map(Clazz::getStudentList)
.collect(Collectors.toSet())
.forEach(studentSet::addAll);
return studentSet;
}
}
import com.example.demo.dao.dbo.Clazz;
import com.example.demo.dao.dbo.Grade;
import com.example.demo.dao.dbo.Student;
import com.example.demo.dao.repo.ClazzRepo;
import com.example.demo.dao.repo.GradeRepo;
import com.example.demo.dao.repo.StudentRepo;
import com.example.demo.service.ClazzService;
import com.example.demo.service.GradeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class ClazzServiceImpl implements ClazzService {
ClazzRepo clazzRepo;
StudentRepo studentRepo;
GradeRepo gradeRepo;
GradeService gradeService;
@Autowired
public ClazzServiceImpl(ClazzRepo clazzRepo,
StudentRepo studentRepo,
GradeService gradeService,
GradeRepo gradeRepo) {
this.clazzRepo = clazzRepo;
this.studentRepo = studentRepo;
this.gradeService = gradeService;
this.gradeRepo = gradeRepo;
}
public Map<String, Long> sexRatio(String cname) throws Exception {
Map<String, Long> map = new HashMap<>(10);
//校验
if (!existByName(cname)) {
throw new Exception("不存在该班级");
}
Set<Student> studentSet = studentRepo.findByClazzCname(cname);
long men = studentSet.stream()
.filter(student -> "male".equals(student.getSgender()))
.count();
long women = studentSet.size() - men;
map.put("men", men);
map.put("women", women);
return map;
}
@Override
public String sexRatioOne(String cname) throws Exception {
Map<String, Long> map = sexRatio(cname);
long men = map.get("men");
long women = map.get("women");
String re;
if (men >= women) {
BigDecimal bg = new BigDecimal((double) men / women).setScale(1, RoundingMode.UP);
re = bg + ":1";
} else {
BigDecimal bg = new BigDecimal((double) women / men).setScale(1, RoundingMode.UP);
re = "1:" + bg;
}
return re;
}
@Override
public String sexRatioTwo(String cname) throws Exception {
Map<String, Long> map = sexRatio(cname);
long men = map.get("men");
long women = map.get("women");
return men + ":" + women;
}
@Override
public Boolean updateClazzWithGrade(String cname, String gname) throws Exception {
// // 校验有没有这个班级
// if (StringUtils.isBlank(cname) || existByName(cname)) {
// throw new Exception("班级id输入有误或不存在");
// }
//检验班级
Clazz clazz = clazzRepo.findByCname(cname)
.orElseThrow(() -> new Exception("班级有误或不存在"));
// 校验有没有这个年级
// if (StringUtils.isBlank(gname) || !gradeService.existByName(gname)) {
// throw new Exception("年级id输入有误或不存在");
// }
Grade grade = gradeRepo.findByGname(gname)
.orElseThrow(() -> new Exception("年级有误或不存在"));
// 校验这种情况是否需要更新
// if (clazzRepo.findByGradeGname(gname)
// .stream()
// .map(Clazz::getCname)
// .anyMatch((e)->e.equals(cname))) { throw new Exception("不需要更新"); }
if (existByCnameAndGname(cname, gname)) {
throw new Exception("不需要更新");
}
//重置clazz的年级
clazz.setGrade(grade);
//
System.out.println(cname);
System.out.println(clazz.getGrade().getGname());
//
//更新
clazzRepo.save(clazz);
return true;
}
@Override
public Map<String, Set<Clazz>> classify() {
//所有的年级
List<Grade> gradeList = gradeRepo.findAll();
Map<String, Set<Clazz>> collect = gradeList.stream()
.collect(Collectors.toMap(Grade::getGname, Grade::getClazzList));
return collect;
}
@Override
public Boolean existByName(String cname) {
Optional<Clazz> optionalClazz = clazzRepo.findByCname(cname);
return optionalClazz.isPresent();
}
@Override
public Boolean existByCnameAndGname(String cname, String gname) {
Optional<Clazz> optionalClazz = clazzRepo.findByCnameAndGradeGname(cname, gname);
return optionalClazz.isPresent();
}
}
import com.example.demo.dao.dbo.Clazz;
import com.example.demo.dao.dbo.Grade;
import com.example.demo.dao.repo.GradeRepo;
import com.example.demo.service.GradeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class GradeServiceImpl implements GradeService {
GradeRepo gradeRepo;
@Autowired
public GradeServiceImpl(GradeRepo gradeRepo) {
this.gradeRepo = gradeRepo;
}
@Override
public Map<String, Set<Clazz>> classify() {
return gradeRepo.findAll()
.stream()
.collect(Collectors.toMap(Grade::getGname,Grade::getClazzList));
}
@Override
public Boolean existByName(String gname) {
Optional<Grade> optionalClazz = gradeRepo.findByGname(gname);
return optionalClazz.isPresent();
}
}
注意:java版本为8,
注意:多使用流的操作!流的操作在网上只有基础操作,但是有些需求可能是基础操作做不到的,但是实际上可以通过流做到的,那么这些就需要你下去多琢磨了。
另外:
推荐这样的写法,不要在一行显示,这样idea还会给你一些辅助,告诉你结束了这行以后它是什么样子的一个数据。
【忽然想到,在代码中有一个没用到的util,DateUtil,本来想的是存储的String类型的时间数据,后来改了,就没用到了,可以当做参考,另外,我是故意没有去操作生日,没有去想一些有关年龄和生日的业务,所以如果有需要,请自己整吧,哈哈哈】
application.properties想了想还是写出来吧,毕竟我自己也在这里花过不少时间:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=password
# 指定数据源类型
spring.jpa.database=MYSQL
# 指定是否在控制台输出SQL语句
spring.jpa.show-sql=true
# 支持自动建表 Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto=update
# 命名策略(Naming strategy):用于指定实体与表映射的命名规范
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
#spring.servlet.multipart.resolve-lazily=false
spring.datasource.platform=mysql
它里面有些东西用处不大,反正同学们自己改吧。
⑤一些测试的设计:
本来觉得这东西没什么说的,但是想了想,测试数据什么的,你可以自己在数据库根据实体类建表然后手动添加,(累死你)也可以在运行测试的时候自动添加(乐死了),这里建议你先添加年级,再添加班级,最后再添加学生(可惜的是我在测试代码里先写的学生,再写的班级,最后写的年级),但是不是绝对,只是为了方便起见。
具体的添加实体的方法都在代码中的Test1里面:
有需要的自己down吧。
还有一些有关impl的测试在Test2中,自己看吧。不多说了,该睡觉了。
晚安,打工人!