Fms-Java版开发实录:19 业务模块设计

Fms-Java版开发实录:19 业务模块设计

终于要开始写业务功能模块了,现在唯一的业务模块就是项目管理,还是先来点设计工作......

在上一篇文章里我为了编写印花税功能,实际上又新开了一个stamp分支,现在可以把这个分支也合并进主分支,然后来开始正式编写业务了。

整体数据结构

这个由于有了之前的Django项目作为基础,还是比较容易的设计出来了:
fms-data-structure
其实本质上一切网站都是内容管理系统,我们也不例外,就是要建立一个这样的体系。
这里相比我原来的Django项目的设计变更点,就是在合同与预算的对应关系上,采取了更灵活的多对多关系,而不是一对一关系,这是为了后边如果出现合同对应多个预算条目的问题,所以先在基础结构上做好准备。

这里的箭头方向都是从主控表指向被控表,即Hibernate的映射关系所在的表。
经过考虑,我觉得系统比较简单,还不用太多考虑性能,还是决定采取外键设置,暂时不采取业务代码来控制。只不过在创建详细表的时候,还是要合理的分离每个表最关键的数据和可以暂时不取的数据。

设计数据表 - PO类的映射关系

借这个机会我就要重新思考一下映射关系。
在之前的User-Role-UserInfo中,我是把外键和控制关系都放在了User表中。这里的特殊之处在于,由于项目和预算是一对多关系,一般来说外键都在多的那一方,但是我又想通过项目查询所有预算,所以很显然,也要进行一个反向映射,其他也是类似,都要进行双向映射。

Project类基础信息

现在所写的功能是项目管理,我们就把这个功能命名为pms,也是所有子包的名称。我们逐渐把所有数据表的骨架搭起来,。首先是Project类,放在cc.conyli.fms.dataobject.pms.entity包下:

@Entity
@Table(name = "project")
public class Project implements Serializable {

    private static final long serialVersionUID = 7L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @NotEmpty(message = "项目名称不能为空")
    @Length(max = 255, message = "长度不能超过255个字符")
    @Column(name = "name", nullable = false)
    private String projectName;

    @NotEmpty(message = "地址不能为空")
    @Length(max = 255, message = "长度不能超过255个字符")
    @Column(name = "address", nullable = false)
    private String projectAddress;

    @NotEmpty(message = "项目经理名称不能为空")
    @Length(max = 10, message = "长度不能超过255个字符")
    @Column(name = "manager", nullable = false, length = 10)
    private String projectManager;

    @NotEmpty(message = "项目描述不能为空")
    @Length(max = 1000, message = "长度不能超过1000个字符")
    @Column(name = "description", nullable = false, length = 1000)
    private String projectDescription;

    @Temporal(TemporalType.DATE)
    @Column(name = "start_time")
    private Date startTime;

    @Temporal(TemporalType.DATE)
    @Column(name = "complete_time")
    private Date completeTime;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "create_time", updatable = false)
    @CreationTimestamp
    private Date createTime;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "update_time")
    @UpdateTimestamp
    protected Date updateTime;
}

省略了其中的其他方法。项目由于数据不多,而且经常要展示,就不再分另外一个表来映射了。

接下来是预算。

Budget类基础信息

这里也先把预算类的骨架搭好,根据我最新的预算表来搭建:

@Entity
@Table(name = "budget")
public class Budget implements Serializable {

    private static final long serialVersionUID = 8L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @NotEmpty(message = "预算条目名称不能为空")
    @Length(max = 255, message = "长度不能超过255个字符")
    @Column(name = "name", nullable = false)
    private String budgetName;

    @NotNull(message = "含税收入不能为空")
    @Digits(integer = 14, fraction = 2)
    private BigDecimal grossIncome;

    @NotNull(message = "不含税收入不能为空")
    @Digits(integer = 14, fraction = 2)
    private BigDecimal netIncome;

    @NotNull(message = "含税成本不能为空")
    @Digits(integer = 14, fraction = 2)
    private BigDecimal grossCost;

    @NotNull(message = "不含税成本不能为空")
    @Digits(integer = 14, fraction = 2)
    private BigDecimal netCost;

    @NotNull(message = "费用不能为空")
    @Digits(integer = 14, fraction = 2)
    private BigDecimal expenditure;

    @Column(name = "virtual")
    @ColumnDefault("false")
    private boolean virtual;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "create_time", updatable = false)
    @CreationTimestamp
    private Date createTime;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "update_time")
    @UpdateTimestamp
    protected Date updateTime;
}

这里有一个布尔字段用来设置这个预算条目是不是虚拟条目,可以把一些纯结转的凭证和合同,都对应在这个虚拟条目中,合同也会有对应的虚拟合同。

ProjectBudget的映射关系

我需要在使用类似于project.budgets()的方法来根据Project查询对应的预算,也需要类似于budget.project()来获取一个预算条目对应的项目,这就需要在两方都做好映射。

由于项目和预算是一对多的关系,因此我把外键放在预算这边,做一个多对一映射,然后还需要在项目那边做一个一对多映射

先来配置Budget中的外键映射,这里可以参考我之前的博客:Hibernate关系映射

@Entity
@Table(name = "budget")
public class Budget {

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    @JoinColumn(name = "project_id")
    private Project project;

}

这实际上就在budget表中创建了一个名称为project_id的外键,并且不允许为null,这实际上表示不允许有脱离项目存在的预算,这本来也是我们的目的。这也意味在代码层面,需要先有一个已经持久化的项目,才能持久化一个对应的预算,顺序不能颠倒。

现在来做另外一个映射,就是从项目中获取对应的所有预算条目:

@Entity
@Table(name = "project")
public class Project implements Serializable {
    @OneToMany(mappedBy = "project", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Budget> budgets;
}

这个映射不会在project表中添加任何新外键,Hibernate会依照这个对应关系进行查询。既然那边是多对一映射,这里就是采用一对多映射,其中的mappedBy = "project"中字符串一定要与Budget类中映射多对一关系的变量名一致,此外,还需要设置cascade,为什么不在多对一的多那边设置呢。这是因为级联操作要先对当前对象进行操作,所以需要在先写入的那一边设置,才能级联保存,在多那边是无法先把Budget写入数据库,再去写项目的。

再添加上对应的gettersetter方法之后,我们的映射就做好了。接下来在编制整个业务流程的时候,其实也就是这样下去,先编制一层映射关系,再来编写对应的增删改查。

后续操作

当然到这里两个类远远没有结束,还有很多方法在等着我们编写,比如一个项目要获取完整的当前财务状态,一个预算条目需要获取对应的成本,支付情况等等,都需要进行计算,这里两边的关系都设置为了LAZY,就是想尽量减少查询次数。

URL路径设计

最基础的是项目,路径设计如下:

  1. pms/project/list,用于列出所有项目清单
  2. pms/project/detail/{id},用于单个项目详情
  3. pms/project/add,用于新增项目
  4. pms/project/edit/{id},用于编辑项目
  5. pms/project/delete,用于删除项目

由于我们的结构是一层一层的往下去的,这里顺便就把预算的一起设计了,其中的{pid}表示项目的id

  1. pms/budget/{pid}/list,用于列出某个项目对应的所有预算
  2. pms/budget/{pid}/detail/{id},用于列出某个预算条目的详情
  3. pms/budget/{pid}/add,用于新增预算
  4. pms/budget/{pid}/edit/{id},用于编辑某个预算条目
  5. pms/budget/delete,用于删除项目

由于预算不能脱离项目而存在,所以所有的查、删、改都要依托于项目。我这里设计URL主要是再做一层校验,而不是直接接受POST请求。

然后在实际操作的时候,会严格控制预算的新建,由于新创建一个预算条目一定要对应预算,所以在创建预算的时候直接就要指定对应的项目,这也是我们强关联关系的提现。下边就先来写项目的增删改查吧。

LICENSED UNDER CC BY-NC-SA 4.0
Comment