大家好,又见面了,我是你们的朋友全栈君。
本文的核心内容:记录积累一些库表设计方案与技巧
数据库实体与实体间的对应关系
1)数据库表的菜单【分类】设计:如省市关联、图书的一、二级分类。
2)数据库表设计之树形结构的表
3)表的简化方案(特定情况,例如,用户触发过的场景记录表)
4)数据库表设计之购物车,利用Session暂时存储购物车信息。
实体与实体间的对应关系
一对一
一对一,一般用于对主表的补充。假设A表为用户信息表,存储了用户的姓名、性别、年龄等基本信息。用户的家庭住址信息也属于用户的基本信息。我们可以选择将用户的家庭住址信息放到用户信息表,也可以单独建一张表,存储用户的家庭住址信息,以用户信息表的主键作为关联。
需不需要拆分取决:表信息的关联程度、表的字段个数限度。
一对多
一对多,是最常见的一种设计。就是 A 表的一条记录,对应 B 表的多条记录,且 A 的主键作为 B 表的外键。举几个 例子:
-
- 班级表 与 学生表,一个班级对应多个学生,或者多个学生对应一个班级。
- 角色表 与 用户表,一个角色对应多个用户,或者多个用户对应一个角色。
-
- 商品表 与 图片表,一个商品对应多张图片,或者多张图片对应一个商品。
多对多
构建一张关系表将两张表进行关联,形成多对多的形式。例如:
-
- 老师表、学生表;一个学生可以选修多个老师的课程、同时一个老师也可以教多个学生。
--教师表
CREATE TABLE #Teacher(TeacherId int, Name nvarchar(20));
INSERT INTO #Teacher VALUES(1, '张老师'), (2, '王老师');
--学生表
CREATE TABLE #Student(StudentId int, Name nvarchar(20));
INSERT INTO #Student VALUES(1, '小张'), (2, '小赵');
--老师学生关系表
CREATE TABLE #Teacher_Student(StudentId int, TeacherId int);
INSERT INTO #Teacher_Student VALUES(1, 1), (1, 2),(2, 1), (2, 2);
一:数据库表的菜单【分类】设计:如省市关联、图书的一、二级分类
BookType 一级分类: 少儿、外语、计算机
BookClass 二级分类: 少儿[0-2岁、3-6岁、7-10岁、11-14岁、儿童文学]
外语[英语、日语、韩语、俄语、德语]
计算机[计算机理论、计算机考试、数据库、人工智能、程序设计]
BookInf 图书详情 : 图书信息的详细字段。。。
基于以上关系:我们建表有两种方法
①:建立三张表 一级分类表,二级分类表、图书详情表
一级分类ID->作为二级分类的外键
二级分类ID->作为图书详情的外键
这一种依赖外键,实体模型也比较简单。(不再过多描述)
查询语句:可以采用 left join on 或者 等值连接 将二级分类的外键与一级分类的主键等值连接即可查询。
②:建立两张表 一级分类和二级分类合并成一张表
图书详情表(引用TypeID为外键)
TypeID 指一级二级分类的ID(唯一标识、主键) 序列自增从1开始。
TypeName 指一级二级分类的名字
ParentID 指二级分类所属一级分类TypeID (若为一级分类则填”0”与二级分类加以区分)
countNumber 指一级图书包含二级图书的个数
二级分类所包含详细图书的个数
数据库查询一级分类信息的SQL
代码语言:javascript复制select typeid,typename,parentid,countnumber
from t_booktype where parentid='0'
数据查询二级分类信息(利用表的自连接)
代码语言:javascript复制select child.typeid,child.typename,
child.parentid,child.countnumber
from t_booktype child ,t_booktype parent
where child.parentid=parent.typeid
二:MySQL 表中存储树形结构数据
由上面设计技巧引出,如果数据层级有多级呢?简言之就像一棵树一样,我们如何存储树形的数据到数据库。
存储父节点
存储于数据库中,最简单直接的方法,就是存储每个元素的父节点ID,即parent_Id->父节点Id。这种方式方便了插入,但是在某些情况下的查询会束手无策。我们可以增加两个字段(deep,is_leaf)帮助我们更快的查询。
deep=1表示父节点,deep>1 表示子节点。
代码语言:javascript复制id
parent_id
deep //当前树的深度
is_leaf //是否叶子节点
查询所有父节点deSQL如下:
代码语言:javascript复制select * from tree where deep=1
查询某个父节点下的所有子节点:
代码语言:javascript复制select * from tree where parent_id=""
查询某个父节点下的所有后代节点,采用这种库表设计方式,这个需要依靠程序才能实现。
存储路径
将存储根结点到每个节点的路径,这种数据结构,可以一眼就看出子节点的深度。要插入自己,然后查出父节点的Path,并且把自己生成的ID更新到path中去。
如果要查询某个节点下的子节点,只需要根据path的路径去匹配,比如要查询D节点下的所有子节点。
代码语言:javascript复制select * from tree where path like '1/4/%'
总结
我建议存储树形结构可以将两者结合起来。
代码语言:javascript复制id
parent_id
deep //当前树的深度
path //根路径
is_leaf //是否叶子节点
三:MySQL 表的简化(特定情况,例如:用户触发过的场景记录表)
假设业务中有N多道具,比如用户首次使用某个道具触发特效。根据表里有没有用户相关的道具触发记录来完成判断。
代码语言:javascript复制id
user_id //用户Id
item_id //道具ID
flag //是否触发过特效 0-1
add_time
update_time
毫无疑问,上述表结构是能够满足并实现我们需求的,但是如果有20种道具,那么每个用户最终将有20条数据,数据冗余,如何简化?
解决方案:
定义道具枚举值
代码语言:javascript复制public enum ItemOnceFlagEnum {
NONE(0),
ITEM_ONE(1),//道具1
ITEM_TWO(1 << 1), //道具2
ITEM_Three(1 << 2), //道具3
;
private int code;
ItemOnceFlagEnum(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static ItemOnceFlagEnum valueOf(int code) {
ItemOnceFlagEnum[] values = ItemOnceFlagEnum.values();
for (ItemOnceFlagEnum flag : values) {
if (flag.getCode() == code) {
return flag;
}
}
return NONE;
}
}
建表
代码语言:javascript复制id
user_id //用户Id
flag //是否触发过特效 2的0次幂、2的1次幂。。。
add_time
update_time
user_id设置为唯一索引
判断是否使用过某道具与添加道具使用记录
代码语言:javascript复制 //判断是否使用过某项道具
public static boolean isHasThisFlag(long flag, ItemOnceFlagEnum itemOnceFlagEnum) {
return (flag & itemOnceFlagEnum.getCode()) > 0;
}
//添加道具使用记录
public Result<Boolean> addOnceFlag(long userId, itemOnceFlagEnum flag) {
ItemOnceFlagDO itemOnceFlagDO = itmeOnceFlagService.getOnceFlagMap(Collections.singletonList(userId));
long calculateFlag = (Objects.isNull(blockOnceFlagDO) ? 0L : itemOnceFlagDO.getFlag()) | flag.getCode();
itemOnceFlag.setFlag(calculateFlag);
boolean res = itemOnceFlagService.addOrUpdateOnceFlag(itemOnceFlag);
}
添加记录SQL
代码语言:javascript复制"INSERT INTO xxxx ( ) VALUES ( :1.userId, :1.flag ) "
" ON DUPLICATE KEY UPDATE flag = :1.flag "
某一项道具为具体2次幂值(仅能够维护2种状态,有或无),flag代表所有触发道具2次幂和。
通过 (flag & itemOnceFlagEnum.getCode())>0 判断是否有某项道具
通过 flag | temOnceFlagEnum.getCode(); 添加道具记录
四:购物车模块的库表设计
在电商软件,必不可少的模块就是购物车。
我分享两种设计方法:
①:维护一张购物车表,以用户ID为外键
一个用户一个购物车,用户注册成功的同时,为用户在购物车表内维护一个专属于用户的购物车。(根据我以前学到的知识,这一步可以为用户表创建Insert触发法器,当用户注册成功[触发器将用户ID作为外键插入购物车表],用户即拥有了唯一的购物车)
T_Car
字段 | 类型 | 说明 |
---|---|---|
Car_ID | Varchar2(36) | 购物车编号 主键 |
User_ID | Varchar2(36) | 外键 用户唯一标识 |
Car_Status | Varchar2(4) | 购物车状态 |
T_Shop_Item
字段 | 类型 | 说明 |
---|---|---|
Shop_item_ID | Varchar2(36) | 购物项编号 主键 |
Car_ID | Varchar2(36) | 购物车编号 外键 |
Product_ID | Varchar2(36) | 商品编号 外键 |
Count | Number(4) | 数量 |
Price | number(8,2) | 价格 |
ProductName | Varchar(30) | 商品名 |
这么实现购物车的弊端:①:非该网站的注册用户无法将商品加入购物车。这与现实的情况不符合。一般我们访问某宝,某东,我们可以以游客的方式将商品加入购物车,直到下订单、付款时才要求我们必须登录。②:每个用户维护一下购物车似乎不太明智,顾客将商品加入购物车到下订单,完成交易,这一需求对数据库更改频繁。
②:所有用户共用一个”购物车”
我们可以直接以用户ID为标识,区分购物车商品所属的用户。
T_Shop_Item
字段 | 类型 | 说明 |
---|---|---|
Shop_item_ID | Varchar2(36) | 购物项编号 主键 |
User_ID | Varchar2(36) | 用户编号 外键 |
ProductID | Varchar2(36) | 商品编号 外键 |
Count | Number(4) | 数量 |
Price | number(8,2) | 价格 |
ProductName | Varchar(30) | 商品名 |
即使减少了一张购物车表,但是这表还需要用户登录才能记录用户添加商品情况。
③:利用Session暂时存储购物车内的东西
[用户不登录就能添加商品到购物车;用户登录状态将Session中的信息存入非关系型数据库、关系型数据库。将购物车内的东西持久化存储]
明确一点:Session -> 一次会话有效(在用户不关闭浏览器的前提下,默认存在30分钟;用户关闭浏览器再次打开后,未登录用户的购物车将清空。)
解决如何用Session存储购物车内信息。
第一个难点:那么如何准确区分不同的商品? (自然是商品ID)
第二个难点:那么如何准确标记一个进入购物车的商品? (只有商品ID是不行的)
商品ID,通过商品ID,我们可以查询到商品详情。(价格、名称等等)
同一商品的购买数量。这一信息是不存在于商品详情的。【重点,不能忽略】
这两个信息必须存储。于是我想到Map(两个原因)。
①:存储两个值
Map<String,Integer> map=new HashMap<String,Integer>();
key存储 商品ID, value存储 商品数量(购买数量)
②:保证商品ID(键)的唯一性
Map的特点:键唯一,值可以重复。我们以商品ID为键,这样就可以保证商品的唯一性。
我们再将map存入Session中就可以了。
当用户添加商品时,只需先从Session中取出map,迭代遍历判断key是否已经存在,若存在取value值加1;若不存在则将商品ID作为key,value数量默认为1,Put进map。
当用户(未登录)查看购物车时,只需从Session中取商品ID和数量,就可以显示购车内商品的详细信息,计算购物车内的商品总价格。
这种存储方式简化了添加商品进入购物车和删除购物车里商品的操作。但是却不得不再次封装一个Map对象将购物车详情页面的信息存储进去,以供购物车展示页面显示数据。
(这是我第一次考虑的存储方案,写到查看购物车详情页面才发现不合理之处。)
改进版本:
Map <String,Goods> map=new HashMap<String,Goods> ();
Key ->商品id
Value->购物车页面需要展示的商品详情(商品名、商品ID、商品数量、商品价格等)
核心:value值维护不再是一个商品数量,而是我们封装的模拟购物车实体对象
JSP页面的效果图
源代码:添加商品的Action
代码语言:javascript复制 //添加商品Action
public String addToCar() throws Exception{
//尝试从Session取出购物车
ValueStack vs=ActionContext.getContext().getValueStack();
Map<String,Goods> car=(Map<String,Goods>)vs.findValue("#session.car");
BookService service=new BookServiceImpl();
if(car==null){
//判断购物车是否存在
//不存在则创建,将用户添加的商品加入Map 存入Session
Map<String,Goods> map=new HashMap<String,Goods>();
//查询数据将商品详情查询出出来,把我们感兴趣的属性封装到Goods实体中
Book book=service.getSingleBook(bookid);
Goods goods=new Goods();
goods.setBookid(book.getBookid());
goods.setCount(1);
goods.setPrice(book.getPrice());
goods.setCurrentprice(book.getCurrentprice());
goods.setProductname(book.getProductname());
map.put(bookid, goods);
vs.setValue("#session.car", map);
}else{
//判断用户添加的商品是否已经存在于购物车里
//若存在根据key取出Goods->修改商品选购的数量
//不存在则是首次添加,数量默认为1 ,将商品Put入map
if(car.containsKey(bookid)){
Goods product=car.get(bookid);
product.setCount(product.getCount() 1);
}else{
Book book=service.getSingleBook(bookid);
Goods goods=new Goods();
goods.setBookid(book.getBookid());
goods.setCount(1);
goods.setPrice(book.getPrice());
goods.setCurrentprice(book.getCurrentprice());
goods.setProductname(book.getProductname());
car.put(bookid, goods);
}
}
return Action.SUCCESS;
}
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/151591.html原文链接:https://javaforall.cn