目录
前言
欲先设计一个系统,必先了解其组成。db系统的组成结构有且只有两种:实体、关系。因而,db系统的设计,实际上就是对实体及其之间的关系的设计。
正文
下面,我们从一个简化的学校教务系统展开,说明db系统设计的过程。
第一步:分析实体与关系,建立数据表
教务系统的实体有:老师、学生、课程、班级。关系有:老师-课程,课程-学生,课程-班级,班级-学生。所以一共需要建立 4(实体)+ 4(关系)=8张数据表。每个表的字段这里不作说明,因为那是属于数据库设计的范畴。
第二步:dao层sql规范
接下来我们为每个表建立dao层,每个表的dao层只包含对该表的数据进行操作,不能有关联查询操作,每个dao中,理论上只应该包含五条sql语句,它们分别是:
- 单条查询
- 批量查询
- 单条删除
- 单条更新
- 单条插入
在springboot + mybatis + mybatis-generator框架下,分别对应五条sql语句:
- selectByPrimaryKey
- selectSelective
- deleteByPrimaryKey
- updateByPrimaryKeySelective
- insertSelective
理论上来说,这五条sql语句包含了对该表的全部操作,不应该再增加额外的sql语句,sql语句应该越少越好,原因如下:
1、sql容易出错。相对于代码而言,sql没有语义校验;
2、更容易维护。对表字段进行修改时需要修改所有的sql,sql越少维护越简单;
3、避免重复造轮子。多余的sql本质上是重复造轮子,因为这五条sql已经包含了对该表的全部操作,是最小逻辑完备sql集合。
一个良好的dao层的代码如下图所示:
@Repository
public interface CourseDao {
int insertSelective(Course course);
int deleteByPrimaryKey(String courseId);
int updateByPrimaryKeySelective(Course course);
Course selectByPrimaryKey(String courseId);
List<Course> selectSelective(CourseQuery query);
}
上图是"课程"数据表对应的dao层,其他7个数据表的dao层与之类似。
第三步:服务层规范--服务分层
service层是db系统的灵魂,db系统好不好扩展,容不容易维护,方不方便重用,几乎完全取决于service层的设计。因而,我们会花较大的篇幅说明如何设计service层。
3.1 基础服务层,xxBaseService
基础服务层只包含对实体的增删改查服务,是对dao层的增删改查的扩展。前面说到,dao层只包含五条sql语句,然而在实际项目之中,这五条sql不能满足需要,如"课程"的查询,有时候我们希望按"课程学分"来查,希望在调用的时候只需要传"学分"即可,而不希望先生成一个包含"学分"的CourseQuery的对象,再进行查询,这个时候,我们就可以直接在CourseBaseService中编写该查询语句,如下:
@Service
public class CourseBaseService{
@Autowired
private CourseDao courseDao;
public List<Course> selectByCredit(Integer credit){
return courseDao.selectSelective(CourseQuery.builder().credit(credit).build());
}
}
这样,外部只需要调用CourseBaseService里面的selectByCredit()方法,而不需要调用dao层的最原始的selectSelective()方法了。
对于每一个dao,我们都给它一个baseSerivce。这样一共8张数据表,就分别对应8个dao,8个baseService,其他服务在需要保存数据的时候,应该调用baseService里面的方法,而不应该直接引用dao层中的方法,服务应该依赖于服务,而不应该依赖于dao。
这样做有以下好处:
1、方便新增。当有新的查询需求时,可直接在baseService中增加,而不需要改动dao层的代码,可以最大限度地减少sql出错的可能性。
2、方便变更。当数据表的删除查询由物理删除改为逻辑删除时,可统一修改baseService中的删除和查询,使之变成逻辑删除和逻辑查询,仍然不需要改动sql。
一句话,方便维护。
3.2 服务层,xxService
服务层是基础服务层的扩展,它是整个系统的核心,它依赖于其他服务,同时只提供在自己视野范围内的服务。
一个服务首先依赖于它的基础服务。如CourseService首先依赖于基础服务CourseBaseService,CourseService不实现数据的增删改查,而是依赖于其baseService来实现对数据表的增删改查。
其次依赖于它的扩展服务,如:在保存课程的时候需要同步至其他系统,则需要依赖于同步服务;需要根据每个字典值决定是否要校验某些信息,则需要依赖于字典服务;需要。。。,总而言之,当一个服务层要实现某个功能,应该尽可能去引用其他类已经实现的功能,而不应该自己一手包办。
一个服务应该只提供自己视野范围内的服务。如课程服务可以提供课程的保存服务,判断课程是否存在的服务,但决不应该提供判断班级是否存在的服务。超出视野的服务一概不提供。
总而言之,xxService,如CourseService,只提供对Course实体的相关操作,它的操作范围被限制在单个表。
3.3 综合服务层
不同于基础服务层(xxBaseService)与服务层(xxService),综合服务层没有唯一对应的数据表,综合服务层可能在一个函数内对很多个数据表进行操作,这是综合服务层和基础服务层及服务层的最大区别。如学生,学生这个实体除了包含学生本身的信息,如身高、年龄、体重,还包含其他的信息,如学习经历、工作经历,家庭关系等等。这么多的信息,一个数据表肯定是不行的,需要用多个数据表分别保存不同的信息。这样一来,在保存学生这个实体的时候,需要同时保存学生基本信息表、经历表、家庭关系表等,那么就需要调用多个数据表的xxService进行保存操作,在删除学生实体的同时,也需要同时删除这三个表中相应的数据,对于多表的操作,统一在综合服务层执行。综合服务层综合了多个数据表的统一管理,因而被称之为综合服务层。
综合服务层是面向用户的,用户对于db系统的操作无非是三种:1、操作实体,2、操作关系,3、操作实体&操作关系。什么意思呢?如录入课程信息,则只包含操作实体(保存课程实体),不包含操作关系;学生选课,则只包含操作关系(关联课程和学生),不包含实体;录入学生信息(含班级信息),则同时包含操作实体(创建学生)和操作关系(关联学生和班级)。
对于只操作实体和只操作关系的操作,我们一般直接在服务层编写对应的函数即可。而对于既操作实体又操作关系的操作,我们需要将其写在综合服务。如:
@Service
public class StudentService {
@Autowired
StudentInfoService studentInfoService;
@Autowired
StudentClassService studentClassService;
public void save(SaveStudentRequest request) {
studentInfoService.save(request.covertToStudentInfo());
studentClassService.save(StudentClass.buildBy(request.getClassId(), request.covertToStudentInfo()));
}
}
在这段代码中,StudentService作为综合服务,在调用save方法的时候同时保存了学生信息(StudentInfo)和学生班级关联信息(StudentClass),同时操作了多个表。
综合服务通过调用其他服务来实现组合服务的效果,在许多应用场景中,我们需要的是往往是综合服务而不是单一的服务,如录入学生的信息可能有多种方式,如手工录入,扫码录入,刷卡录入等等,每种录入方式都要保存学生及学生和班级的关系,这时候我们只需调用综合服务的save方法即可,而不需要在三种录入方式里面分别调用单一的服务来实现。
第四步,controller层规范
controller层的作用只有两个,一是让开发人员能够迅速理解所有的业务,二是生成swagger文档,因而,controller层的核心是注释、注解,而代码量应该尽可能少,如下是两者的对比。
错误示范:
@PostMapping("/addEntEmailAccount")
@ApiOperation("开通企业邮箱用户")
public BaseResponse addEntEmailAccount(@RequestBody BaseRequest<AddEntEmailAccountRequest> baseRequest) {
BaseHead baseHead = baseRequest.getHead();
AddEntEmailAccountRequest request = baseRequest.getBody();
if (StringUtils.isEmpty(request.getAccountPrefix())) {
throw new BaseException(ResponseEnum.ICEM000);
}
return BaseResponse.build(emailService.addEntEmailAccount(request, baseHead));
}
正确示范:
@PostMapping("/addEntEmailAccount")
@ApiOperation("开通企业邮箱用户")
public BaseResponse addEntEmailAccount(@RequestBody BaseRequest<AddEntEmailAccountRequest> baseRequest) {
return BaseResponse.build(emailService.addEntEmailAccount(baseRequest.getHead(), baseRequest.getBody()));
}
在错误示范之中,controller层里面包含了没有必要的数据转换代码及参数校验代码,这些代码完全可以放到服务层中去处理。正确的做法是用一行代码说清楚其调用的服务,其他的内容全部用来书写swagger文档相关以及一些注释信息,用于说明该接口的作用。
------------------------------------------------
下面说明在开发过程中的一些设计小技巧,其目的是尽可能地减少重复性代码,并将代码归到合适的位置。
技巧一:对象转换
对于前端传给后端的参数,往往需要用这些参数构建相应的对象进行进一步的操作,假设有以下参数:
//前端的参数类型
@Data
class Request{
private String A;
private String B;
private String C;
private String D;
}
//查询需要的参数类型
@Data
class CourseQuery{
private String A;
private String B;
private String C;
}
上图中,前端传的参数有A、B、C、D,后台查询所用的对象只需要A、B、C,一般的写法是:
@Autowired
CourseService courseService;
public Object query(Request request){
CourseQuery courseQuery = new CourseQuery();
courseQuery.setA(request.getA());
courseQuery.setB(request.getB());
courseQuery.setC(request.getC());
courseService.query(courseQeury);
}
上述代码尽管逻辑没有问题,但是作为业务函数的query(),却包含了大量的非业务代码,这很不利于后续的维护。观察到courseQuery对象完全是由request对象转换而来的,故而可以将转换的逻辑写在request对象里,业务函数query0相应的则只包含业务代码了。如下
//前端的参数类型
@Data
class Request{
private String A;
private String B;
private String C;
private String D;
public CourseQuery transferCourseQuery(){
CourseQuery instance = new CourseQuery();
instance.setA(A);
instance.setB(B);
instance.setC(C);
return instance;
}
}
//只包含业务代码的业务函数
public Object query(Request request){
courseService.query(request.transferCourseQuery());
}
上述的场景是,单个源对象包含目标对象中全部需要的要素,因而可以在源对象中写对象转换方法。而有些场景中,目标对象的要素来源于多个源对象,此时可以在目标对象中写buildBy方法来生成对象,如下:
@Data
class Course{
private String A;
private String B;
private String C;
}
@Data
class Student{
private String D;
private String E;
private String F;
}
@Data
class Response{
private String A;
private String B;
private String D;
private String F;
public static Response buildBy(Course course, Student student){
Response instance = new Response();
instance.setA(course.getA());
instance.setB(course.getB());
instance.setD(student.getD());
instance.setF(student.getF());
return instance;
}
}
//只包含业务代码的业务函数
public Response query(Request request){
Course course = courseService.query(request.transferCourseQuery());
Student student = studentService.query(request.transferStudentQuery());
return Response.buildBy(course,student);
}
总结如下:
当目标对象由单个源对象决定时,在源对象中书写covertToXXX()方法;当目标对象由多个源对象组合决定时,在目标对象中书写buildBy()方法。
使用对象转换的方法,可以将对象转换的代码从业务层挪到相应的对象中去,一方面能够使业务代码更加清晰,另一方面,一些经常使用的对象,需要经常转换成其他的对象,将转换的代码写在对象中,可以方便复用。
技巧二:使用查询对象进行查询
使用对象而非Map进行查询,能够使代码更清晰。比如对于Course实体,我们可以写一个继承于Course的查询对象CourseQuery。
@Data
class Course{
//基本属性A
private String A;
//基本属性B
private String B;
//基本属性C
private String C;
}
@Data
class CourseQuery extends Course{
//课程名称列表
private List<String> courseNames;
//创建时间-开始
private String createTimeS;
//创建时间-结束
private String createTiemE;
}
在查询时,既可以根据Course的基本属性精确查询,也可以根据CourseQuery中的扩展属性查询,相对于Map而言,对象查询具有结构清晰,方便注释、方便对象转换的特点,相应的,Mapper中的sql代码也变为:
<sql id="Base_Filter">
<if test="A != null and A != ''">
and A = #{A,jdbcType=VARCHAR}
</if>
<if test="B != null and B != ''">
and B = #{B,jdbcType=VARCHAR}
</if>
<if test="C != null and C != ''">
and C = #{C,jdbcType=VARCHAR}
</if>
</sql>
<sql id="Extend_Filter">
<if test="courseNames != null and courseNames.size() > 0">
and courseName in
<foreach collection="courseNames " index="index" item="id" open="(" separator="," close=")">
#{courseName}
</foreach>
</if>
<if test="createTimeS != null and createTimeS != ''">
and TO_CHAR(CREATETIME,'yyyy-MM-dd hh:mm:ss') >= #{createTimeS,jdbcType=VARCHAR}
</if>
<if test="createTimeE != null and createTimeE != ''">
and TO_CHAR(CREATETIME,'yyyy-MM-dd hh:mm:ss') <= #{createTimeE,jdbcType=VARCHAR}
</if>
</sql>
<select id="selectSelective" resultType="Course">
select
<include refid="Base_Column_List"/>
from Course
<where>
<include refid="Base_Filter"/>
<include refid="Extend_Filter"/>
</where>
</select>
在上述代码中,将查询条件分为Base_Filter和Extend_Filter,分别对应Course中的基本属性和CourseQuery中的扩展属性。通过这种分类的方式,当需要修改时可以帮助我们迅速定位到对应的代码,减少查找代码所需要的时间。
结尾
这里的代码规范主要针对于db系统设计而言,但代码规范不是唯一的,不同的场景规范不同,但无论哪种规范,都必须满足以下特点:
1、可读性好
2、方便查找
3、方便重用
4、方便修改
所谓规范,只是提高工作效率的其中一种手段,不断改进自己的工作方法,节约时间学习新的事物,形成:提高效率--节约时间学习--进一步提高效率的良性循环,才是王道。 雄关漫道真如铁,而今漫步从头越,加油吧诸位!