learn.lianglianglee.com/专栏/领域驱动设计实践(完)/043 案例 培训管理系统.md.html
2022-05-11 19:04:14 +08:00

1379 lines
49 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>043 案例 培训管理系统.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/领域驱动设计实践(完)/001 「战略篇」访谈 DDD 和微服务是什么关系?.md.html">001 「战略篇」访谈 DDD 和微服务是什么关系?.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/002 「战略篇」开篇词:领域驱动设计,重焕青春的设计经典.md.html">002 「战略篇」开篇词:领域驱动设计,重焕青春的设计经典.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/003 领域驱动设计概览.md.html">003 领域驱动设计概览.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/004 深入分析软件的复杂度.md.html">004 深入分析软件的复杂度.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/005 控制软件复杂度的原则.md.html">005 控制软件复杂度的原则.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/006 领域驱动设计对软件复杂度的应对(上).md.html">006 领域驱动设计对软件复杂度的应对(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/007 领域驱动设计对软件复杂度的应对(下).md.html">007 领域驱动设计对软件复杂度的应对(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/008 软件开发团队的沟通与协作.md.html">008 软件开发团队的沟通与协作.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/009 运用领域场景分析提炼领域知识(上).md.html">009 运用领域场景分析提炼领域知识(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/010 运用领域场景分析提炼领域知识(下).md.html">010 运用领域场景分析提炼领域知识(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/011 建立统一语言.md.html">011 建立统一语言.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/012 理解限界上下文.md.html">012 理解限界上下文.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/013 限界上下文的控制力(上).md.html">013 限界上下文的控制力(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/014 限界上下文的控制力(下).md.html">014 限界上下文的控制力(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/015 识别限界上下文(上).md.html">015 识别限界上下文(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/016 识别限界上下文(下).md.html">016 识别限界上下文(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/017 理解上下文映射.md.html">017 理解上下文映射.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/018 上下文映射的团队协作模式.md.html">018 上下文映射的团队协作模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/019 上下文映射的通信集成模式.md.html">019 上下文映射的通信集成模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/020 辨别限界上下文的协作关系(上).md.html">020 辨别限界上下文的协作关系(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/021 辨别限界上下文的协作关系(下).md.html">021 辨别限界上下文的协作关系(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/022 认识分层架构.md.html">022 认识分层架构.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/023 分层架构的演化.md.html">023 分层架构的演化.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/024 领域驱动架构的演进.md.html">024 领域驱动架构的演进.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/025 案例 层次的职责与协作关系(图文篇).md.html">025 案例 层次的职责与协作关系(图文篇).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/026 限界上下文与架构.md.html">026 限界上下文与架构.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/027 限界上下文对架构的影响.md.html">027 限界上下文对架构的影响.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/028 领域驱动设计的代码模型.md.html">028 领域驱动设计的代码模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/029 代码模型的架构决策.md.html">029 代码模型的架构决策.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/030 实践 先启阶段的需求分析.md.html">030 实践 先启阶段的需求分析.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/031 实践 先启阶段的领域场景分析(上).md.html">031 实践 先启阶段的领域场景分析(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/032 实践 先启阶段的领域场景分析(下).md.html">032 实践 先启阶段的领域场景分析(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/033 实践 识别限界上下文.md.html">033 实践 识别限界上下文.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/034 实践 确定限界上下文的协作关系.md.html">034 实践 确定限界上下文的协作关系.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/035 实践 EAS 的整体架构.md.html">035 实践 EAS 的整体架构.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/036 「战术篇」访谈DDD 能帮开发团队提高设计水平吗?.md.html">036 「战术篇」访谈DDD 能帮开发团队提高设计水平吗?.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/037 「战术篇」开篇词:领域驱动设计的不确定性.md.html">037 「战术篇」开篇词:领域驱动设计的不确定性.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/038 什么是模型.md.html">038 什么是模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/039 数据分析模型.md.html">039 数据分析模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/040 数据设计模型.md.html">040 数据设计模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/041 数据模型与对象模型.md.html">041 数据模型与对象模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/042 数据实现模型.md.html">042 数据实现模型.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/领域驱动设计实践(完)/043 案例 培训管理系统.md.html">043 案例 培训管理系统.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/044 服务资源模型.md.html">044 服务资源模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/045 服务行为模型.md.html">045 服务行为模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/046 服务设计模型.md.html">046 服务设计模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/047 领域模型驱动设计.md.html">047 领域模型驱动设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/048 领域实现模型.md.html">048 领域实现模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/049 理解领域模型.md.html">049 理解领域模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/050 领域模型与结构范式.md.html">050 领域模型与结构范式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/051 领域模型与对象范式(上).md.html">051 领域模型与对象范式(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/052 领域模型与对象范式(中).md.html">052 领域模型与对象范式(中).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/053 领域模型与对象范式(下).md.html">053 领域模型与对象范式(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/054 领域模型与函数范式.md.html">054 领域模型与函数范式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/055 领域驱动分层架构与对象模型.md.html">055 领域驱动分层架构与对象模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/056 统一语言与领域分析模型.md.html">056 统一语言与领域分析模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/057 精炼领域分析模型.md.html">057 精炼领域分析模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/058 彩色 UML 与彩色建模.md.html">058 彩色 UML 与彩色建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/059 四色建模法.md.html">059 四色建模法.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/060 案例 订单核心流程的四色建模.md.html">060 案例 订单核心流程的四色建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/061 事件风暴与业务全景探索.md.html">061 事件风暴与业务全景探索.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/062 事件风暴与领域分析建模.md.html">062 事件风暴与领域分析建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/063 案例 订单核心流程的事件风暴.md.html">063 案例 订单核心流程的事件风暴.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/064 表达领域设计模型.md.html">064 表达领域设计模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/065 实体.md.html">065 实体.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/066 值对象.md.html">066 值对象.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/067 对象图与聚合.md.html">067 对象图与聚合.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/068 聚合设计原则.md.html">068 聚合设计原则.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/069 聚合之间的关系.md.html">069 聚合之间的关系.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/070 聚合的设计过程.md.html">070 聚合的设计过程.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/071 案例 培训领域模型的聚合设计.md.html">071 案例 培训领域模型的聚合设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/072 领域模型对象的生命周期-工厂.md.html">072 领域模型对象的生命周期-工厂.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/073 领域模型对象的生命周期-资源库.md.html">073 领域模型对象的生命周期-资源库.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/074 领域服务.md.html">074 领域服务.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/075 案例 领域设计模型的价值.md.html">075 案例 领域设计模型的价值.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/076 应用服务.md.html">076 应用服务.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/077 场景的设计驱动力.md.html">077 场景的设计驱动力.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/078 案例 薪资管理系统的场景驱动设计.md.html">078 案例 薪资管理系统的场景驱动设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/079 场景驱动设计与 DCI 模式.md.html">079 场景驱动设计与 DCI 模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/080 领域事件.md.html">080 领域事件.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/081 发布者—订阅者模式.md.html">081 发布者—订阅者模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/082 事件溯源模式.md.html">082 事件溯源模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/083 测试优先的领域实现建模.md.html">083 测试优先的领域实现建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/084 深入理解简单设计.md.html">084 深入理解简单设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/085 案例 薪资管理系统的测试驱动开发(上).md.html">085 案例 薪资管理系统的测试驱动开发(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/086 案例 薪资管理系统的测试驱动开发(下).md.html">086 案例 薪资管理系统的测试驱动开发(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/087 对象关系映射(上).md.html">087 对象关系映射(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/088 对象关系映射(下).md.html">088 对象关系映射(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/089 领域模型与数据模型.md.html">089 领域模型与数据模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/090 领域驱动设计对持久化的影响.md.html">090 领域驱动设计对持久化的影响.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/091 领域驱动设计体系.md.html">091 领域驱动设计体系.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/092 子领域与限界上下文.md.html">092 子领域与限界上下文.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/093 限界上下文的边界与协作.md.html">093 限界上下文的边界与协作.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/094 限界上下文之间的分布式通信.md.html">094 限界上下文之间的分布式通信.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/095 命令查询职责分离.md.html">095 命令查询职责分离.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/096 分布式柔性事务.md.html">096 分布式柔性事务.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/097 设计概念的统一语言.md.html">097 设计概念的统一语言.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/098 模型对象.md.html">098 模型对象.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/099 领域驱动设计参考过程模型.md.html">099 领域驱动设计参考过程模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/100 领域驱动设计的精髓.md.html">100 领域驱动设计的精髓.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/101 实践 员工上下文的领域建模.md.html">101 实践 员工上下文的领域建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/102 实践 考勤上下文的领域建模.md.html">102 实践 考勤上下文的领域建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/103 实践 项目上下文的领域建模.md.html">103 实践 项目上下文的领域建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/104 实践 培训上下文的业务需求.md.html">104 实践 培训上下文的业务需求.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/105 实践 培训上下文的领域分析建模.md.html">105 实践 培训上下文的领域分析建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/106 实践 培训上下文的领域设计建模.md.html">106 实践 培训上下文的领域设计建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/107 实践 培训上下文的领域实现建模.md.html">107 实践 培训上下文的领域实现建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/108 实践 EAS 系统的代码模型.md.html">108 实践 EAS 系统的代码模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/109 后记:如何学习领域驱动设计.md.html">109 后记:如何学习领域驱动设计.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>043 案例 培训管理系统</h1>
<p>接下来,我会用数据模型驱动设计的方法设计一个培训管理系统。在这个系统平台上,管理员可以发布课程,注册会员可以浏览所有的课程信息,并选择将自己感兴趣的课程加入到期望列表中,还可以订阅课程,并完成在线支付。为了让我们将注意力集中在该系统的核心业务中,我省去了用户管理、权限管理等通用功能。</p>
<h3>建立数据分析模型</h3>
<p>首先,我们需要分析业务,从中识别出我们需要关注的实体和实体之间的关系。一种简单的做法是通过识别需求中的名词来获得实体,之后就可以辨别它们之间的关系。我们还可以通过引入不同的用户视图来创建不同的实体关系模型。</p>
<p>在数据模型驱动设计过程中,通常会利用需求功能规格说明书详细阐述需求,如:</p>
<ul>
<li>管理员发布课程:<strong>管理员</strong>在发布<strong>课程</strong>时,需要提供课程名、类别、课程简介、目标收益、培训对象和课程大纲。每门课程还要给出课程费用、课程时长、课程排期和讲师的信息。<strong>讲师</strong>的信息包括讲师姓名、任职公司和职务以及老师的个人简历。</li>
<li>注册会员浏览课程信息:<strong>注册会员</strong>可以搜索和浏览课程基本信息,也可以查看课程的详细信息。</li>
<li>加入期望列表:注册会员在发现自己感兴趣的课程时,可以将其加入到<strong>期望列表</strong>中,除了不允许重复添加课程之外,没有任何限制。</li>
<li>订阅课程:注册会员可以订阅课程。订阅课程时,应该确定具体的课程排期。课程一旦被注册会员订阅,就应该从期望列表中移除。注册会员可以订阅多个课程。</li>
<li>购买课程:注册会员在订阅课程后,可以通过在线<strong>支付</strong>进行购买。购买课程时,需要生成课程<strong>订单</strong></li>
</ul>
<p>通过以上需求描述,很明显可以获得两个用户视图,即管理员和注册会员。分别针对这两种不同的用户,识别需求描述中代表业务概念的名词,可以获得如下实体关系模型:</p>
<p><img src="assets/623de310-8b23-11e9-8ada-d1fb8c4d56bd" alt="img" /></p>
<p>图中灰色实体恰好可以与需求功能描述中的名词相对应。黄色实体则针对业务做了进一步的提炼按照培训领域的业务将“注册会员”称为“学生Student将“一次课程订阅”称为“培训Training”。注意课程Course与培训Training、订单Order之间的关系。课程是一个可以反复排期、反复订阅的描述课程基本信息和内容的实体。当学生在订阅课程时需要确定具体的课程排期。一旦订阅就成为了一次具有固定上课日期的培训。只有学生为这次培训支付了费用后才会生成一个订单。</p>
<p>深化实体关系模型需要确定数据表以及数据表之间的关系和属性由此获得数据项模型这需要继续深挖业务需求由此获得数据实体的属性并按照数据库范式对实体进行拆分并合理安排数据表之间的关系。例如针对课程实体可以将其拆分为课程Course、日程Calendar与类别Category表。课程与日程、类别的关系是一对多的关系。针对订单实体可以将其拆分为订单Order和订单项OrderItem</p>
<p>数据项模型的主体是数据表以及数据表之间的关系。在培训系统中我们欣喜地发现数据模型中的关系表不仅在于消除表之间的多对多关系同时还体现了业务概念。例如期望列表WishList体现了学生与课程之间的多对多关系培训Training同样体现了这二者之间的多对多关系。</p>
<p>在梳理数据表之间的关系时有时候会因为建立了更细粒度的数据表而判断出之前的实体关系可能存在谬误。例如实体关系模型为支付Payment与培训、订单与培训之间建立了关系但在数据项模型中由于引入了订单项它与培训存在一对一关系从而解除了订单与培训之间的关系。在定义了支付的属性后最终发现为支付与培训之间建立关系是没有意义的。最终我们建立的数据项模型如下图所示</p>
<p><img src="assets/9ffb3ef0-8b23-11e9-8ada-d1fb8c4d56bd" alt="79456155.png" /></p>
<h3>建立数据设计模型</h3>
<p>在数据设计模型中,需要定义持久化对象、数据访问对象与服务对象。为简便起见,本例不考虑各个数据表增删改查的数据管理操作,而只需要设计如下业务功能:</p>
<ul>
<li>添加课程到期望列表</li>
<li>从期望列表中移除课程</li>
<li>预订课程</li>
<li>取消课程预订</li>
<li>购买课程</li>
</ul>
<p>服务将完成这些业务功能。通常,我们需要根据业务功能所要操作的表来判断功能的承担者。例如“学生添加课程到期望列表”操作的是期望列表,这个功能就应该定义到 WishListService 服务中。要注意区分功能描述的概念名词与实际操作数据表的区别。例如,“学生预订课程”功能表面上是操作课程数据表,实际生成了一个培训和订单;“学生购买课程”表面上是操作课程数据表,但实际上是针对订单表和支付表进行操作,这两个功能就应该定义到 OrderService 服务中。</p>
<p>数据项模型中的每个数据表对应每个持久化对象,这些持久化对象本质上都是传输对象,仅提供业务操作的数据,不具备业务行为。访问数据库的行为都放在持久化对象对应的数据访问对象中,业务行为则由服务来封装。因此,针对以上业务功能得到的设计模型如下所示:</p>
<p><img src="assets/c794e150-8b23-11e9-ae6c-75b709235ff6" alt="79894111.png" /></p>
<p>这里,我使用了 UML 类图来表达数据设计模型,这样可以清晰地看到服务、数据访问对象与持久化对象之间的关系。例如 OrderService 依赖于 PayService、PaymentMapper、OrderMapper、TrainingMapper 和 OrderItemMapper这些 Mapper 对象又各自依赖于对应的持久化对象。以 Mapper 结尾的对象扮演数据访问对象的角色,之所以这样命名,是沿用了 MyBatis 框架推荐的命名规范。选择 ORM 框架属于设计决策,仍然属于数据建模设计活动的一部分,而这个决策不仅会对设计模型带来影响,同时还会直接影响实现模型。</p>
<p>在定义数据设计模型时还需要理清持久化对象之间的关联关系。数据表之间的关联关系往往通过主外键建立例如在数据项模型中t_course 表的主键为 id在 t_wish_list 与 t_calendar 等表中则以 courseId 外键体现关联关系。在对象模型中,通常会通过对象引用的组合方式体现关联关系,如设计模型中 Order 与 OrderItem 之间的组合关系Category、Teacher 和 Calendar 之间的组合关系。</p>
<h3>建立数据实现模型</h3>
<p>数据实现模型首先包含了创建数据表的脚本。我使用了 FlywayDB 框架,在 db-migration 目录下创建了 SQL 文件 V1__create_tables.sql。例如创建 t_course、t_student 及 t_wish_list 数据表:</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS t_course(
id VARCHAR(36) PRIMARY KEY,
teacherId VARCHAR(36) NOT NULL REFERENCES t_teacher(id),
name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255) NOT NULL,
earning VARCHAR(255),
trainee VARCHAR(200),
outline TEXT,
price DECIMAL NOT NULL,
duration INT NOT NULL,
categoryId VARCHAR(36) NOT NULL REFERENCES t_category(id),
createdBy VARCHAR(36) NOT NULL REFERENCES t_administrator(id),
createdAt DATETIME NOT NULL,
updatedAt DATETIME NOT NULL
);
CREATE TABLE IF NOT EXISTS t_student(
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(50) NOT NULL,
mobilePhone VARCHAR(20) NOT NULL,
registeredTime DATETIME NOT NULL,
createdAt DATETIME NOT NULL,
updatedAt DATETIME NOT NULL
);
CREATE TABLE IF NOT EXISTS t_wish_list(
studentId VARCHAR(36) NOT NULL REFERENCES t_student(id),
courseId VARCHAR(36) NOT NULL REFERENCES t_course(id),
PRIMARY KEY(studentId, courseId)
);
</code></pre>
<p>我选择了 VARCHAR(32) 类型作为表的主键,它对应于 Java 的 UUID。t_wish_list 实际上是 t_student 与 t_course 的关联表,但也体现了业务概念。以上 SQL 脚本并没有创建索引,可以考虑在后续版本创建各个表的索引。</p>
<p>每个数据表对应的持久化对象都是一个贫血对象,可以使用 Lombok 来简化代码,例如 Order 类的定义:</p>
<pre><code class="language-java">import lombok.Data;
import java.sql.Timestamp;
import java.util.List;
import java.util.UUID;
@Data
public class Order {
private String id;
private Student student;
private OrderStatus status;
private Timestamp placedTime;
private Timestamp createdAt;
private Timestamp updatedAt;
private List&lt;OrderItem&gt; orderItems;
public Order() {
this.id = UUID.randomUUID().toString();
}
public Order(String orderId) {
this.id = orderId;
}
}
import java.sql.Timestamp;
@Data
public class OrderItem {
private String id;
private String orderId;
private Training training;
private Timestamp createdAt;
private Timestamp updatedAt;
}
</code></pre>
<p>注意 Order 类与 Student 及 OrderItem 之间是通过对象引用来体现的,对比数据表,可以看到数据表模型与对象模型在处理关系上的区别:</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS t_order(
id VARCHAR(36) PRIMARY KEY,
studentId VARCHAR(36) NOT NULL REFERENCES t_student(id),
status ENUM('New', 'Paid', 'Confirmed', 'Completed') NOT NULL,
placedTime DATETIME NOT NULL,
createdAt DATETIME NOT NULL,
updatedAt DATETIME NOT NULL
);
CREATE TABLE IF NOT EXISTS t_order_item(
id VARCHAR(36) PRIMARY KEY,
orderId VARCHAR(36) NOT NULL REFERENCES t_order(id),
trainingId VARCHAR(36) NOT NULL REFERENCES t_training(id),
createdAt DATETIME NOT NULL,
updatedAt DATETIME NOT NULL
);
</code></pre>
<p>如果订单的数据访问对象 OrderMapper 要根据 id 查询订单,就需要映射器实现 ResultSet 中每一行到 Order 的转换,其中还包括对 Student 与 OrderItem 对象的映射,而 OrderItem 又与 Training 对象有关Training 对象又牵涉到 Course 与 Calendar。要支持数据到对象的转换就需要定义数据表与持久化对象的映射关系同时访问数据表的 SQL 语句则需要执行关联查询,以获取横跨多个数据表的数据信息。</p>
<p>数据实现模型与我们选择的 ORM 框架有关。本例使用了 MyBatis 框架实现数据的持久化。该框架支持 Java 标记或 XML 文件来定义表与对象的映射关系,并允许嵌入访问数据表的 SQL 语句。倘若 SQL 语句比较复杂,一般建议使用 XML 映射文件。例如,在 OrderMapper 中定义根据 id 获取订单对象的方法就可以定义数据访问对象。MyBatis 框架一般以 Mapper 后缀来命名数据访问对象,并要求定义为一个抽象接口。例如访问订单的数据访问对象 OrderMapper 接口:</p>
<pre><code class="language-java">import org.apache.ibatis.annotations.Param;
import xyz.zhangyi.practicejava.framework.mybatis.model.Order;
public interface OrderMapper {
Order getOrder(@Param(&quot;orderId&quot;) String orderId);
}
</code></pre>
<p>获取订单的方法是一个接口方法,可以直接交给服务对象调用,例如在<code>OrderService</code>中:</p>
<pre><code class="language-java">@Component
@Transactional
@EnableTransactionManagement
public class OrderService {
private OrderMapper orderMapper;
@Autowired
public void setOrderMapper(OrderMapper orderMapper) {
this.orderMapper = orderMapper;
}
public Order getOrder(String orderId) {
Order order = orderMapper.getOrder(orderId);
if (order == null) {
throw new ApplicationException(String.format(&quot;Order by id %s is not found&quot;, orderId));
}
return order;
}
}
</code></pre>
<p>实现代码非常简单但在其背后MyBatis 需要建立一个非常繁琐的映射文件来规定映射关系,并将 getOrder() 方法绑定到 SQL 语句之上。这个映射文件为 OrderMapper.xml 文件:</p>
<pre><code class="language-xml">&lt;mapper namespace=&quot;OrderMapper&quot;&gt;
&lt;resultMap id=&quot;order&quot; type=&quot;Order&quot;&gt;
&lt;constructor&gt;
&lt;idArg column=&quot;orderId&quot; javaType=&quot;String&quot; /&gt;
&lt;/constructor&gt;
&lt;result property=&quot;status&quot; column=&quot;orderStatus&quot; typeHandler=&quot;org.apache.ibatis.type.EnumTypeHandler&quot;/&gt;
&lt;association property=&quot;student&quot; javaType=&quot;Student&quot;&gt;
&lt;id property=&quot;id&quot; column=&quot;studentId&quot; /&gt;
&lt;result property=&quot;name&quot; column=&quot;studentName&quot; /&gt;
&lt;result property=&quot;email&quot; column=&quot;email&quot; /&gt;
&lt;/association&gt;
&lt;collection property=&quot;orderItems&quot; ofType=&quot;OrderItem&quot;&gt;
&lt;id property=&quot;id&quot; column=&quot;itemId&quot; /&gt;
&lt;association property=&quot;training&quot; javaType=&quot;Training&quot;&gt;
&lt;id property=&quot;id&quot; column=&quot;trainingId&quot; /&gt;
&lt;association property=&quot;student&quot; javaType=&quot;Student&quot; /&gt;
&lt;association property=&quot;course&quot; javaType=&quot;Course&quot;&gt;
&lt;id property=&quot;id&quot; column=&quot;courseId&quot; /&gt;
&lt;result property=&quot;name&quot; column=&quot;courseName&quot; /&gt;
&lt;result property=&quot;description&quot; column=&quot;courseDescription&quot; /&gt;
&lt;association property=&quot;teacher&quot; javaType=&quot;Teacher&quot;&gt;
&lt;id property=&quot;id&quot; column=&quot;teacherId&quot; /&gt;
&lt;result property=&quot;name&quot; column=&quot;teacherName&quot; /&gt;
&lt;/association&gt;
&lt;/association&gt;
&lt;association property=&quot;calendar&quot; javaType=&quot;Calendar&quot;&gt;
&lt;id property=&quot;id&quot; column=&quot;calendarId&quot; /&gt;
&lt;result property=&quot;place&quot; column=&quot;place&quot; /&gt;
&lt;result property=&quot;startDate&quot; column=&quot;startDate&quot; /&gt;
&lt;result property=&quot;endDate&quot; column=&quot;endDate&quot; /&gt;
&lt;/association&gt;
&lt;/association&gt;
&lt;/collection&gt;
&lt;/resultMap&gt;
&lt;select id=&quot;getOrder&quot; resultMap=&quot;order&quot;&gt;
select
o.id as orderId,
o.status as orderStatus,
s.id as studentId,
s.name as studentName,
s.email as email,
oi.id as orderItemId,
t.id as trainingId,
c.id as courseId,
c.name as courseName,
c.description as courseDescription,
te.id as teacherId,
te.name as teacherName,
ca.id as calendarId,
ca.place as place,
ca.startDate as startDate,
ca.endDate as endDate
from t_order o
left outer join t_student s on o.studentId = s.id
left outer join t_order_item oi on oi.orderId = o.id
left outer join t_training t on oi.trainingId = t.id
left outer join t_course c on t.courseId = c.id
left outer join t_teacher te on c.teacherId = te.id
left outer join t_calendar ca on t.calendarId = ca.id
where o.id = #{orderId}
&lt;/select&gt;
&lt;/mapper&gt;
</code></pre>
<p>这个映射文件展现了如何通过定义 <code>&lt;resultMap&gt;</code> 来实现数据表到对象的映射。在 <code>&lt;resultMap&gt;</code> 中,通过 <code>&lt;association&gt;</code> 实现了对象之间的一对一组合关系,通过 <code>&lt;collection&gt;</code> 实现了一对多关系,通过为 <code>&lt;result&gt;</code> 指定 typeHandler 为 org.apache.ibatis.type.EnumTypeHandler 来处理枚举的映射。至于在 <code>&lt;select&gt;</code> 中,则是一个超级复杂的 SQL 语句,实现了多张表之间的关联查询。</p>
<p>只要数据访问对象实现了各种丰富的数据表访问功能,服务的实现就会变得相对容易。这实际上遵循了职责分离与协作的设计思想。服务可以用来编排业务,还可以干一些脏活累活,如处理异常或者事务。有的 MVC 框架如 Rails 使用活动记录模式来实现模型对象,却没有引入服务对象,混入在模型对象中的数据访问操作往往直接暴露给控制器,最后使得控制器变得臃肿不堪。实际上,服务才是真正封装业务逻辑的,哪怕它的实现采用了面向过程的事务脚本模式。如“购买课程”的业务功能实现:</p>
<pre><code class="language-java">public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private PaymentMapper paymentMapper;
@Autowired
private PayService payService;
public void purchase(int studentId, int orderId, Account account, String paymentStyle) {
try {
Order order = orderMapper.getOrder(orderId);
double totalAmount = 0.0d;
for (OrderItem orderItem : order.getOrderItems()) {
totalAmount += orderItem.getTraining().getPrice();
}
try {
PaymentResult result = payService.pay(account, totalAmount, paymentStyle);
if (result.isFailed()) {
throw new PaymentException(&quot;Failed to pay for order with id:&quot; + order.getId());
}
paymentMapper.insert(studentId, orderId, totalAmount, paymentSytle);
orderMapper.updateStatus(orderId, OrderStatus.Paid);
} catch (RemoteException ex) {
throw new PaymentException(ex);
}
} catch (Exception ex) {
throw new ApplicationException(ex);
}
}
}
</code></pre>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/领域驱动设计实践(完)/042 数据实现模型.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/领域驱动设计实践(完)/044 服务资源模型.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997e575ee03cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>