db系统设计规范

阅读: 评论:0

db系统设计规范

db系统设计规范

目录

前言

正文

第一步:分析实体与关系,建立数据表

第二步:dao层sql规范

第三步:服务层规范--服务分层

        3.1 基础服务层,xxBaseService

        3.2 服务层,xxService

        3.3 综合服务层

第四步,controller层规范

技巧一:对象转换

技巧二:使用查询对象进行查询

结尾


前言

        欲先设计一个系统,必先了解其组成。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{@Autowiredprivate 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 {@AutowiredStudentInfoService studentInfoService;@AutowiredStudentClassService studentClassService;public void save(SaveStudentRequest request) {studentInfoService.vertToStudentInfo());studentClassService.save(StudentClass.ClassId(), vertToStudentInfo()));}}

        在这段代码中,StudentService作为综合服务,在调用save方法的时候同时保存了学生信息(StudentInfo)和学生班级关联信息(StudentClass),同时操作了多个表。

        综合服务通过调用其他服务来实现组合服务的效果,在许多应用场景中,我们需要的是往往是综合服务而不是单一的服务,如录入学生的信息可能有多种方式,如手工录入,扫码录入,刷卡录入等等,每种录入方式都要保存学生及学生和班级的关系,这时候我们只需调用综合服务的save方法即可,而不需要在三种录入方式里面分别调用单一的服务来实现。

第四步,controller层规范

        controller层的作用只有两个,一是让开发人员能够迅速理解所有的业务,二是生成swagger文档,因而,controller层的核心是注释、注解,而代码量应该尽可能少,如下是两者的对比。

错误示范:

    @PostMapping("/addEntEmailAccount")@ApiOperation("开通企业邮箱用户")public BaseResponse addEntEmailAccount(@RequestBody BaseRequest<AddEntEmailAccountRequest> baseRequest) {BaseHead baseHead = Head();AddEntEmailAccountRequest request = Body();if (StringUtils.AccountPrefix())) {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.Head(), Body()));}

        在错误示范之中,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.A());courseQuery.B());courseQuery.C());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.ansferCourseQuery());
}

        上述的场景是,单个源对象包含目标对象中全部需要的要素,因而可以在源对象中写对象转换方法。而有些场景中,目标对象的要素来源于多个源对象,此时可以在目标对象中写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.A());instance.B());instance.D());instance.F());return instance;}
}//只包含业务代码的业务函数
public Response query(Request request){Course course = courseService.ansferCourseQuery());Student student = studentService.ansferStudentQuery());return Response.buildBy(course,student);
}

        总结如下:

当目标对象由单个源对象决定时,在源对象中书写covertToXXX()方法;当目标对象由多个源对象组合决定时,在目标对象中书写buildBy()方法。

         使用对象转换的方法,可以将对象转换的代码从业务层挪到相应的对象中去,一方面能够使业务代码更加清晰,另一方面,一些经常使用的对象,需要经常转换成其他的对象,将转换的代码写在对象中,可以方便复用。

技巧二:使用查询对象进行查询

        使用对象而非Map进行查询,能够使代码更清晰。比如对于Course实体,我们可以写一个继承于Course的查询对象CourseQuery。

@Data
class Course{//基本属性Aprivate String A;//基本属性Bprivate String B;//基本属性Cprivate 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') &gt;= #{createTimeS,jdbcType=VARCHAR}</if><if test="createTimeE != null and createTimeE != ''">and TO_CHAR(CREATETIME,'yyyy-MM-dd hh:mm:ss') &lt;= #{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、方便修改

        所谓规范,只是提高工作效率的其中一种手段,不断改进自己的工作方法,节约时间学习新的事物,形成:提高效率--节约时间学习--进一步提高效率的良性循环,才是王道。 雄关漫道真如铁,而今漫步从头越,加油吧诸位!

本文发布于:2024-01-29 05:44:29,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170647827213117.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:设计规范   系统   db
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23