一、乱穿马路模式介绍
程序员通常使用逗号分隔的列表来避免在多对多关系中创建交叉表,这种设计方式定义为一种反模式,称为乱穿马路。
例如:在一个产品管理系统中,一个人可以有多个产品,一个产品必须对应一个人,因此有如下数据库:
但是,随着时间的推移,出现了一个产品可能会有多个联系人。于是为了最小限度地修改数据库,可能不少人会将Account_Id的类型修改成varchar,这样就可以列出该列中的多个账号Id,每个Id之间用逗号分隔。这样的设计貌似可行,因为并没有创建额外的表或者列。仅仅改变了一个字段的数据类型就成功达到目的。以下我们来列举一下这样做的缺点。改变之后的数据库外键去掉,同时Product_Account变为varcahr。
1、查询指定账号的产品。
因为所有的外键都合并在一个单元格内,查询会变得相当困难。这时候不能在使用逗号。下面来测试下查询所有账号Id为1的产品。
2、查询指定产品的账号
使用逗号分隔的列表来做多表联结查询定位一行数据也是既不优雅和耗时的。
3、执行聚合查询
聚合查询使用SQL内置的聚合函数。如count(),sum(),avg()。然后这些函数是针对分组进行而设计的,并不是为了逗号分隔的列表,使用这些聚合函数也变得困难。
4、更新指定产品的账号
你可以用字符串拼接的方式在列表尾端增加一个新的ID,但这并不能使列表按顺序存储。
5、删除指定产品的账号
6、验证产品ID
如何防止用户在Product_Account列中输入诸如'apple'这样的非法字段。
7、选择合适的分隔符
如果存储一个字符串列表,而不是数字列表,列表中的某些条目可能会包含分隔符。使用逗号作为分隔符可能会有问题。当然,到时候可以再换一个字符,但是即使这样也无法确保这个新字符永远不会出现的条目中,
8、列表长度限制
你能在一个varchar(50)的结构中存多少数据呢?这依赖于每个条目的长度,如果每个条目只有2个字符长,那你能够存10多条数据,但如果每个条目长度为6,你就只能存几个了。
如果你的团队中说过下面这些话,那么很有可能就是在项目中使用了“乱穿马路”的设计模式的线索了。
(1)列表最多支持存放多少条数据?
这个问题在选择varchar列的最大长度时被提及,因为长度不好确定。
(2)你知道在SQL中如何做分词查找吗?
如果你用了正则表达式来提取字符串中的组成部分,这可能是一种提示,意味着你应该把这些数据分开存储。
(3)哪些字符不会出现在任何一个列表条目中?
你想要使用一个不会令人困惑的分割符号。但任何字符都有可能在某天出现在字段中的某个值内。
二、合理使用反模式
如果你的应用程序需要逗号分隔的这种存储格式,也可能没必要获取列表中的单独项。同样,如果应用程序接收的源数据时有逗号分隔的格式,而你只需要存储和使用它们并且不对其做任何修改,你完全不必要分开其中的值。
三、终极解决方案 - 创建一张交叉表
将Account_Id存储在一张单独的表中,而不是存储在Product表中,从而每个Account都可以占据一行。这张新表称为Contacts。实现了Product与Account的多对多关系。
设计后的数据库关系图如下所示:
剩下的就不说了,你会发现,如果是基于以上3张表的设计,那么对于以上乱穿马路模式提出的8个问题都能够轻松解决。另外,为Contacts.Account_Id做索引的查询效率比用逗号分隔列表中分串要高效得多。在许多数据库当中,声明某一列为外键都会隐式地为该列创建索引。
另外,你还可以再交叉表中添加其他属性,比如,可以记录一个联系人被加入产品的具体日期,或产品的第一联系人及第二联系人。这些都是在逗号分隔的列表中无法做到的。