开发记录

本文详细描述了数据库设计中的关键原则、用户密码安全存储策略,以及MySQL配置与数据库建立过程。重点讲解了API接口设计、JPA和Spring-data-jpa的应用,涉及节点存储、无环判断、活动状态管理。项目进度功能实现部分涵盖了功能描述、接口设计和事务管理。
摘要由CSDN通过智能技术生成

开发记录

1. 数据库设计

1.1 总体设计原则

本次数据库设计不同于以往的由个人完成的小项目,需要考虑实际应用过程中的各种问题,例如性能问题、安全性、可拓展性等,因此本次数据库设计注重一下方面:
(1)数据的一致性和完整性:通过对数据的约束和审核,保证数据合乎规范和保持一致
(2)安全性:保护数据不被非法访问、敏感内容加密存储等
(3)可拓展性:充分考虑发展的需要、移植的需要,具有良好的扩展性、伸缩性和适度冗余
(4)提高性能:通过合理设计表的结构、适当使用索引等提高性能

1.2 用户名密码的存储

用户名和密码是非常关键的数据,一旦泄露会造成巨大的损失。在本次设计中我们准备进行如下设计:
(1)每个用户名唯一对应一个UID,后端处理时使用UID而非用户名
(2)每个用户对应一个口令,用户进行登录时需要提供口令,口令经过加密后存储在数据库中
(3)密码加密存储,通过哈希加盐法对密码进行加密。用户在注册时提供口令,之后后端为该用户分配盐值,该盐值之后插入到口令中进行hash,最后将hash后的口令存入数据库。用户进行身份验证时用户提供口令,在数据库中查找盐值,进行同样操作与数据库中已经存入的加密的口令进行比对判断是否验证成功。

1.3 数据库与文件

通过数据库进行文件的存取是之前没有接触过的,需要解决的问题不仅仅包含把文件存下来,在我们的项目中还要记录文件在网盘中的位置(即要想办法生成文件在网盘中的结构),进行历史版本管理等。
(1)小文件存储
典型的例子就是用户的头像,对于这样的小文件,可以直接转成二进制存入数据库中,在MySQL中对应的数据类型为blob
(2)网盘文件管理
存入网盘的文件有大有小,并且既有文件也有文件夹。
采用的方案为对于每一个团队,在服务器下都会建立一个单独的文件夹,并且路径会保存在数据库中(或者根据团队ID计算生成)。对于上传的文件,通过两个表进行维护:在文件表中主要存储FID,文件名称,服务器存储名称(为了便于管理,上传之后会将文件改名,通过这个表记录对应关系);在文件目录表中,根据文件目录的树状结构,可以通过把这颗树存储的方式保存文件目录(采取的方案为每一列对应一个节点,每个节点有一个parent属性)

2.MySQL配置与数据库建立

2.1 数据库外部连接

在MySQL8.0中允许外部连接的步骤为:
(1)配置MySQL后重新修改密码
(2)创建外部访问用户user

create user 'root'@'%' IDENTIFIED WITH root By 'root';

(3)允许任何主机通过该用户访问

update user set host='%' where user ='root';
FLUSH PRIVILEGES;

(4)授予该用户操作权限

GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'WITH GRANT OPTION;

(5)修改服务器3306端口的入站规则

2.2 外码

通过Navicat设置外码时出现以下选项
在这里插入图片描述
这4个选项为父表和子表的约束关系
CASCADE:父表UPDATE/DELETE时子表会同步更新
NO ACTION:如果子表中有匹配的记录,则不允许对父表对应候选键进行update/delete操作
RESTRICT:同NO ACTION
SET NULL:父表UPDATE/DELETE时子表对应值设为NULL(要求该列允许为NULL)
这4种约束关系是由数据库保证,当然也可以自己写规则

2.3 数据库存储对象

MySQL中Blob(binary large object)类型,可以存储二进制文件,可以据此把对象转化为二进制流存入数据库以实现对象的存储
在Java可以通过对象操作流(ObjectOutputStream,ObjectInputStream)实现对象和二进制流的转化

3.API文档和接口设计

3.1 API文档

3.2接口设计

3.2.1 JPA

JPA即Java持久化API,是Sun引入的新的ORM规范
在springboot中整合了JPA即Spring-data-jpa

JPA的查询语言是面向对象而非面向数据库的,它以面向对象的自然语法构造查询语句,可以看成是Hibernate HQL的等价物。JPA定义了独特的JPQL(Java Persistence Query Language),JPQL是EJB QL的一种扩展,它是针对实体的一种查询语言,操作对象是实体,而不是关系数据库的表,而且能够支持批量更新和修改、JOIN、GROUP BY、HAVING 等通常只有 SQL 才能够提供的高级查询特性,甚至还能够支持子查询。

3.2.2 Spring-data-jpa的使用

springboot后端一般分为3层结构
1.DAO层

全称Data Access Object。Dao层比较底层,负责与数据库打交道具体到对某个表、某个实体的增删改查

2.SERVICE层

又叫服务层或业务层,封装Dao层的操作,使一个方法对外表现为实现一种功能,例如:网购生成订单时,不仅要插入订单信息记录,还要查询商品库存是否充足,购买是否超过限制等等。

3.CONTROLLER层

业务控制层,负责接收数据和请求,并且调用Service层实现这个业务逻辑。

JPA主要负责DAO层,通过实体类(entity)建立与表及表的各个属性的映射,repository类实现对属性的操作(增删改查)
目录结构如下:
在这里插入图片描述

entity实体类
@Entity
@Table(name = "activity_info")
public class activity_info {
    @Id
    @Column(name = "ACTID")
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "activity_info_AID")
    private String ACTID;

    @Column(name = "adescriptioin")
    private String adescriptioin;

    @Column(name = "aleader")
    private String aleader;

    @Column(name = "aexpected")
    private int aexpected;

    @Column(name = "anumber")
    private int anumber;

    @Column(name = "APID")
    private String APID;
}

注解:
@Entity 表明该类是实体类
@Table(name) 表示类与表的对应关系,name为表名称
@Column(name) 表示类变量与表属性的对应关系
@Id 表明该字段是主键
@GeneratedValue 表明主键的自增方式
另外还有许多其他注解,如:
@Transient 表明该字段不是与表属性的映射
@EntityListeners 对实体类进行监听
在有@EntityListeners注解后,可以用如下注解自动在增删改查时进行记录
@CreatedBy
@CreatedDate
@LastModifiedBy
@LastModifiedDate
@Version

repository 数据访问层
public interface studentOnline_repository extends JpaRepository<student_online,String> {
    List<student_online> findByUID(String UID);
    List<student_online> findByUName(String UName);
}

(1)JPA的强大之处在于JPA的查询语言是面向对象而非面向数据库的,无需sql语句即可完成查询
JPA预先实现了一些简单的CURD方法,例如:findAll(),save()等,若要自定义查询,只需在类中使用关键字定义方法即可,例如想要通过UName查询,只需定义
List<student_online> findByUName(String UName)这一个方法即可,无需实现,JPA会自动帮你执行
(2)Update操作
通过对方法添加@Query可以执行sql语句
例如:

	@Modifying
    @Query(value = "update student_online set UOLcode = :UOLcode where UID = :UID",nativeQuery = true)
    public int updateUOLcodeByUID(@Param("UID")String UID, @Param("UOLcode")String UOLcode);

@Query注解中,value即为执行的sql语句,:UOLcode相当于占位符,通过@Param注解传参
注意可以讲方法的返回值类型设为int,返回受影响的行数以判断是否执行成功
(3)Delete操作
有两种方法,一种是类似于update,通过@Query注解实现
另一种方法类似于查询,通过deletexxxByxxx()实现(注意还要加上@TransAction注解)

	@Modifying
    @Query(value = "delete from activity_relation where ACTID = :ACTID AND ParentID = :ParentID",nativeQuery = true)
    int deleteByACTIDAndParentID(@Param("ACTID") String ACTID,@Param("ParentID") String ParentID);
repository 数据访问层的使用

通过@Resource注解使用

@Resource
studentPassword_repository spr;

通过spr即可执行增删改查操作
例如:

spr.findAll();  //JpaRepository预先实现的方法
spr.findByUID("1"); //自定义查询
spr.save(null); //insert

4.项目进度功能实现

4.1 功能描述

在这里插入图片描述
实现上图中功能需要完成:
(1)节点信息的保存
(2)各活动的预计开始、完成时间(由“根”节点到每个节点的路径长度(即每个节点的预计完成时间))计算
(3)项目预计完成时间,与(3)类似
(4)每个活动目前的状态,由当前时间与(2)中计算出的时间进行比较
(5)无环判断

4.2 实现描述

4.2.1 节点存储

本质上要存一张有向无环图,以及图中各节点的信息,采用如下设计:
存储节点的信息以及节点的先驱,由此可以还原成图
在这里插入图片描述
activity_info表存储节点信息
activity_relation表存储节点的前驱(多值属性)
按如下规则还原:
对于节点a,遍历其所有的前驱b,添加一条边(a,b),其权值为节点a的预期完成时间

4.2.2 无环判断

有多种种方法,例如通过BFS,DFS,拓扑排序等实现,最终选择拓扑排序
首先对图进行拓扑排序:
1、选择一个入度为0的顶点并输出之;
2、从网中删除此顶点及所有出边。
算法结束后,比较输出的节点数和有向图的节点数,若输出的节点数<有向图的节点,说明有环

4.2.3 预计完成时间

进行拓扑排序,求出关键路径长度

4.2.4 单个活动的预计开始时间

进行拓扑排序,计算关键路径上到该节点的路径长度

4.2.5 活动状态

当前端拉取是进行计算看当前时间与预计开始时间和预计完成时间的关系

4.3 接口设计

public interface Graph {
    final static int MAX_PRIORITY = 365;   //最大权值(预计用时)
    
    //增加一条边
    public int addEdge(int x,int y,int priority);
    
    //是否成环
    public Boolean isLooped();
    
    //对原图进行拓扑排序,返回一个拓扑序
    public ArrayList<activity_node> TopologicalSort();
    
    //节点v1到节点v2的最短路径长度(v1,v2为节点的序号)
    public int minPath(int v1,int v2);
    
    //计算各个节点的开始时间
    public int[] calStartTime();
    
    //计算计算整个项目结束预计花费时间
    public int calFinishedTime();
}

4.4 实现

通过邻接矩阵存储有向图,并由一个单独的节点数组存储节点信息(经过排序),通过节点数组的下标建立节点序号与其在邻接矩阵中标号的对应关系

5.后端接口实现

5.1 接口描述

本次我负责实现ProjectController全部接口和UserController的部分接口,主要涉及用户信息,项目管理,项目成员管理以及邀请信息管理,接口列表如下:

UserController:
在这里插入图片描述
ProjectController:
在这里插入图片描述

5.2 接口结构

5.2.1 参数接受

通过cookie获得发起请求者的Email,请求参数以form-data形式传入

5.2.1 返回值

后端一律返回一个RetResult对象
在这里插入图片描述
code:状态码
在这里插入图片描述
msg:附带信息
data:要返回给前端的数据

5.2.2 总体结构

核心为service层,由transaction类实现对单表的操作,在service中调用实现各种逻辑
对于Exception的处理,在service中采用以下结构:

try{
	//执行各种逻辑,若出现逻辑错误或成功执行会对RetResult的信息进行设置
}catch(Exception e){
	//根据异常对要返回的RetResult的信息进行设置
}
//最后统一把RetResult返回
return result

关于对输入值的检查,在service类和transaction类中检查的参数类别不同。在transaction层,主要检查要进行insert或者update的数据是否正确且符合规范主要关注一个表,在service类中,主要检查发起请求者是否有权限等,可能会涉及到多个表,或者是有一定逻辑
对于方法的返回值,transaction层的单个方法返回Boolean值,代表操作是否成功(执行的具体信息在返回之前被写入到RetResult中)

5.2.3 事务transaction

为了保证系统的稳定性,在执行数据库操作是需要保证原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability),在出现错误后要考虑是否回滚以维持项目稳定。
在jpa中,可以通过@Transactional注解实现对事务的管理,并且可以通过修改参数实现对事务的管理
在实际开发中,主要用到了以下几个属性:
①notRollbackFor和rollbackFor
在加上@Transactional注解后,若遇到未检查的异常,spring会自动对事务进行回滚,但实际开发中可能还需要对已检查的异常进行回滚等,可以通过该注解进行实现
这两个属性分别控制对某类异常回滚和不会滚,接受的参数为class对象,如rollbackFor=Exception.class
②propagation
这个属性定义了事务的传播行为,默认值为PROPAGATION_REQUIRED,其机制为如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
在实际开发中,可能遇到事务嵌套的问题,在这种情况下需要根据逻辑设置propagation的值进行处理
在实际开发中对事务进行测试时出现了UnexpectedRollbackException异常:Transaction rolled back because it has been marked as rollback-only。
当时是对新建项目进行测试。新建一个项目需要进行3个操作:在project_info表中插入信息,在project_member表中插入信息,在对应路径下建立项目对应的文件夹。对于这3项操作,若后面的操作出错则前面的都要回滚。在测试时故意让第2步出错,以检测是否能回滚。结果发现事务确实被回滚但是确爆出了UnexpectedRollbackException异常。上网查阅发现是事务的传播机制出现问题。
以如下结构距离:

class A{
	@Transactional
	void doA(){
		try{
			B.doB();
		}catch(Exception e){
			//处理
		}
	}
}
class B{
	@Transactional
	void doB(){
		throw new Exception();
	}
}

执行这段代码,若B抛出异常,最后也会抛出UnexpectedRollbackException,原因是事务的默认传播机制为PROPAGATION_REQUIRED,执行doA()时,当前没有事务,所以创建一个新事物,之后执行doB()时会加入到这个事务,若doB()中抛出异常,那么这个事务会被标记为需要回滚,然而在doA()中这个异常被catch住了,因此即使出现了异常,doA()会继续执行后面的代码,执行到最后一行代码时认为方法执行完毕,不需要回滚,但是由于doB()也在这个事务里,这个事务早已经被标记为回滚,前后不一致,导致了出现UnexpectedRollbackException。
由上例可以看出,主要是propagation属性没有设置好的原因。但是从结果来看,逻辑是对的(数据库确实回滚了),可以直接把该异常捕获,并不会影响逻辑,但是毕竟出现了前后不一致,为了可能会出现的问题需要对此进行解决。
出现问题的结构如下:

class Controller{
    @Transactional
    void test(){
        try{
            doService();
        }catch(Exception e){
            //服务器异常
        }
    }
}
class Service{
    @Transactional
    void doService(){
        try{
            doTransaction1();
            doTransaction2();
        }catch(Exception e){
            //处理
        }
    }
}
class Transaction{
    @Transactional
    void doTransaction1(){}
    @Transactional
    void doTransaction2(){}
}

可以看到propagation均为同一个值,因此从test()到doTransaction()全部在同一个事务中,而由于内部的异常会被catch住,所以会出现前后不一致的问题。而实际上要进行回滚的主要是doTransaction1()和doTransaction2(),这两个方法应该放进同一个事务里,因此解决方法为设置doTransaction1()的propagation=PROPAGATION_NESTED(如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED)
这样执行到doTransaction1()时会创建一个新的事务,而执行到doTransaction2()时会加入到doTransaction1()的事务中,这样当doTransaction2()抛出异常时被标记的是一个新创建的事务,而外面那层事务没有被标记,这样就解决了前后不一致的问题。
经过测试,修改后事务被正确的回滚,并且没有爆出UnexpectedRollbackException异常
③timeout
控制事务执行的最长时间,如timeout=30代表一个事务最长执行30s,若超过30s则自动回滚

5.2.4 项目文件夹建立

项目的文件夹负责存储对应项目的文件信息,该文件夹是在项目创建时建立的,因此要保证这个文件夹被正确建立和使用。

File dir = new File(FileSpace.PATH + PID);
if (!dir.exists()){
	dir.mkdir();
}

在创建项目和拉取项目信息时都会对文件夹是都存在做出判断,如果拉取时不存在作为补救措施会新建对应文件夹,同时会作为日志记录

5.2.4 项目日志

系统在运行中会对对数据库中对项目进行INSERT、UPDATE和DELETE的操作进行记录,并保存在数据库中,主要通过ChangeLogService类实现:
在这里插入图片描述
在相关操作执行成功后会向数据库插入相关信息

5.2 接口实现

5.2.1 主键ID的生成

采用两位字母+19为数字的形式

public static synchronized String randomCode() {

        Random random = new Random();
        Integer number = random.nextInt(900000) + 100000;

        return System.currentTimeMillis() + String.valueOf(number);
}
public static synchronized String randomProjectIdCode() {
        return "PD" + randomCode();
}
5.2.2 数据检查

除了格式是否正确外,还要检查逻辑上的问题,例如可能要插入的UID根本就不存在,对此在Repository中通过countBy对满足的数据进行计数

@Query(value = "select count(*) from project_info where project_pid = :pid and project_director = :director",nativeQuery = true)
int countByPidAndProjectDirector(@Param("pid")String pid,@Param("director")String director);
5.2.3 多表查询

在jpa中,多表查询可以由Repository实现,或者是自定义查询,后者需要自己提供接受结果的接口

public interface workProjection {
    @Value("#{target.earliest}")
    public String getEarliest();
    @Value("#{target.latest}")
    public String getLatest();
    @Value("#{target.project_create_time}")
    String getCreateTime();
    @Value("#{target.project_name}")
    String getName();
    @Value("#{target.total}")
    int getTotal();
    @Value("#{target.rest}")
    int getRest();
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值