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{
    @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') &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、方便修改

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
以下是一个Java银行存取款操作系统的简单实现,包括用户注册、登录、修改密码、余额查询、充值、转账和取款等功能。该系统使用MySQL数据库存储用户信息和交易记录,并采用正则表达式对用户输入进行规范化处理。 ```java import java.sql.*; import java.util.Scanner; import java.util.regex.Matcher; import java.util.regex.Pattern; public class BankSystem { private static final String DB_URL = "jdbc:mysql://localhost:3306/bank"; private static final String DB_USER = "root"; private static final String DB_PASSWORD = "123456"; private static Connection conn = null; private static Statement stmt = null; private static ResultSet rs = null; private static Scanner scanner = new Scanner(System.in); public static void main(String[] args) { try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); stmt = conn.createStatement(); System.out.println("欢迎使用银行存取款管理系统!"); while (true) { System.out.println("请选择操作:"); System.out.println("1.用户注册 2.用户登录 3.退出系统"); int choice = scanner.nextInt(); scanner.nextLine(); switch (choice) { case 1: register(); break; case 2: login(); break; case 3: System.out.println("感谢使用银行存取款管理系统!"); System.exit(0); default: System.out.println("无效的选择,请重新输入!"); } } } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } finally { try { if (rs != null) rs.close(); if (stmt != null) stmt.close(); if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } private static void register() throws SQLException { System.out.println("请输入用户名:"); String username = scanner.nextLine(); if (usernameExists(username)) { System.out.println("该用户名已存在,请重新输入!"); return; } System.out.println("请输入密码:"); String password = scanner.nextLine(); if (!isValidPassword(password)) { System.out.println("密码不符合规范,请重新输入!"); return; } System.out.println("请输入初始余额:"); double balance = scanner.nextDouble(); scanner.nextLine(); String sql = "INSERT INTO users (username, password, balance) VALUES ('" + username + "', '" + password + "', " + balance + ")"; stmt.executeUpdate(sql); System.out.println("注册成功!"); } private static void login() throws SQLException { System.out.println("请输入用户名:"); String username = scanner.nextLine(); if (!usernameExists(username)) { System.out.println("该用户名不存在,请重新输入!"); return; } System.out.println("请输入密码:"); String password = scanner.nextLine(); String sql = "SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'"; rs = stmt.executeQuery(sql); if (rs.next()) { System.out.println("登录成功!"); while (true) { System.out.println("请选择操作:"); System.out.println("1.修改密码 2.余额查询 3.充值 4.转账 5.取款 6.返回上一级"); int choice = scanner.nextInt(); scanner.nextLine(); switch (choice) { case 1: changePassword(username); break; case 2: checkBalance(username); break; case 3: deposit(username); break; case 4: transfer(username); break; case 5: withdraw(username); break; case 6: return; default: System.out.println("无效的选择,请重新输入!"); } } } else { System.out.println("密码错误,请重新输入!"); } } private static void changePassword(String username) throws SQLException { System.out.println("请输入旧密码:"); String oldPassword = scanner.nextLine(); String sql = "SELECT * FROM users WHERE username='" + username + "' AND password='" + oldPassword + "'"; rs = stmt.executeQuery(sql); if (rs.next()) { System.out.println("请输入新密码:"); String newPassword = scanner.nextLine(); if (!isValidPassword(newPassword)) { System.out.println("密码不符合规范,请重新输入!"); return; } sql = "UPDATE users SET password='" + newPassword + "' WHERE username='" + username + "'"; stmt.executeUpdate(sql); System.out.println("密码修改成功!"); } else { System.out.println("旧密码错误,请重新输入!"); } } private static void checkBalance(String username) throws SQLException { String sql = "SELECT balance FROM users WHERE username='" + username + "'"; rs = stmt.executeQuery(sql); if (rs.next()) { double balance = rs.getDouble("balance"); System.out.println("当前余额为:" + balance); } } private static void deposit(String username) throws SQLException { System.out.println("请输入充值金额:"); double amount = scanner.nextDouble(); scanner.nextLine(); String sql = "UPDATE users SET balance=balance+" + amount + " WHERE username='" + username + "'"; stmt.executeUpdate(sql); sql = "INSERT INTO transactions (username, type, amount) VALUES ('" + username + "', '充值', " + amount + ")"; stmt.executeUpdate(sql); System.out.println("充值成功!"); } private static void transfer(String username) throws SQLException { System.out.println("请输入对方用户名:"); String toUsername = scanner.nextLine(); if (!usernameExists(toUsername)) { System.out.println("该用户名不存在,请重新输入!"); return; } System.out.println("请输入转账金额:"); double amount = scanner.nextDouble(); scanner.nextLine(); String sql = "SELECT balance FROM users WHERE username='" + username + "'"; rs = stmt.executeQuery(sql); if (rs.next()) { double balance = rs.getDouble("balance"); if (balance < amount) { System.out.println("余额不足,转账失败!"); return; } sql = "UPDATE users SET balance=balance-" + amount + " WHERE username='" + username + "'"; stmt.executeUpdate(sql); sql = "UPDATE users SET balance=balance+" + amount + " WHERE username='" + toUsername + "'"; stmt.executeUpdate(sql); sql = "INSERT INTO transactions (username, type, amount) VALUES ('" + username + "', '转账', -" + amount + ")"; stmt.executeUpdate(sql); sql = "INSERT INTO transactions (username, type, amount) VALUES ('" + toUsername + "', '转账', " + amount + ")"; stmt.executeUpdate(sql); System.out.println("转账成功!"); } } private static void withdraw(String username) throws SQLException { System.out.println("请输入取款金额:"); double amount = scanner.nextDouble(); scanner.nextLine(); String sql = "SELECT balance FROM users WHERE username='" + username + "'"; rs = stmt.executeQuery(sql); if (rs.next()) { double balance = rs.getDouble("balance"); if (balance < amount) { System.out.println("余额不足,取款失败!"); return; } sql = "UPDATE users SET balance=balance-" + amount + " WHERE username='" + username + "'"; stmt.executeUpdate(sql); sql = "INSERT INTO transactions (username, type, amount) VALUES ('" + username + "', '取款', -" + amount + ")"; stmt.executeUpdate(sql); System.out.println("取款成功!"); } } private static boolean usernameExists(String username) throws SQLException { String sql = "SELECT * FROM users WHERE username='" + username + "'"; rs = stmt.executeQuery(sql); return rs.next(); } private static boolean isValidPassword(String password) { String regex = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$"; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(password); return matcher.matches(); } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值