从AdventureWorks学习数据库建模——国际化

简介:

前一篇博客我已经把各个实体分析了一遍,从分析中可以看到,这个公司是做本地采购,生产,然后通过网站和门店进行国际销售的。所以这里会涉及到一些国际化的问题。接下来就来分析一下有哪些国际化需要注意的问题和数据库模型中的解决方案。

语言

AdventureWorks数据模型中,只有对ProductDescription进行了多语言设置。关于多语言的建模,我曾经写了一篇文章,详细介绍了多语言建模的几种方法,可以参考:http://www.cnblogs.com/studyzy/archive/2013/04/03/2998322.html

回过头来看看AdventureWorks是怎么处理多语言的。

 NewImage

他在ProductDescription的Description字段中用各种语言维护了一些描述信息,但是却并没有任何一个字段说明这一行维护的是什么语言。反而时建立了ProductDescription和Culture的多对多关系。这是一个很奇怪的设计,Culture和ProductDescription应该是一对多的关系,一种语言会维护很多句描述信息,而具体的一条描述信息,在写入Description的时候就应该已经确定了唯一的一种语言。比如我随便找了一行描述“充电式双光束车灯。”这行描述会对应英语?法语?会是对应多种语言吗?而实际的查询也证明了我这个观点:

SELECT  [ ProductDescriptionID ]
, count( [ CultureID ]as CultureCount
FROM  [ Production ]. [ ProductModelProductDescriptionCulture ]
group  by ProductDescriptionID
having  count(CultureID) > 1

返回0行,不会有哪个ProductDescription会去对应多个Culture。

那正确的模型应该是什么样的呢?简单的改法是:Culture和ProductDescription是一对多关系,ProductModel和ProductDescription是多对多关系,如下图所示:

NewImage

这种模型可以使得每一行的Description在定义输入的时候就指定了输入的到底是什么语言。但是这个模型有一个缺点,就是同一个ProductModel,在不同的语言情况下可能对应的描述不一致。比如有产品样品A,他关联的中文描述是:

  • 描述1
  • 描述2

而他关联的英文描述是:

  • Description 2
  • Description 3
  • Description 4

所以这个模型引用的多语言描述可能是混乱的。那么我们可以进一步改进模型。需要增加一个表ProductStandardDescription,这个表中维护了最常用的语言的描述,比如里面维护了英文描述。然后ProductStandardDescription和Culture形成多对多关系,把除了英文外的其他语言的描述信息翻译好维护进去。最后ProductStandardDescription和ProductModel也是一个多对多关系。

这样的好处是可以避免前面提到的不同语言关联的描述不一致的问题,而且以英语为标准描述,可以很容易找到对应的其他语言是否存在,不存在的话就使用默认语言英语。

格式

格式是一个在应用程序中需要注意的问题,主要是对日期和数字的显示格式处理。在数据库建模中,为了避免格式问题,不要用字符串类型去存储日期时间和数字。如果知道是日期类型的那么就使用Date类型,如果是要包含日期和时间的,那么就用datetime类型,只需要记录时间就用time类型。而对于数字或者是金额,也一定要使用对应的数字类型int decimal和金额类型money。

如果使用varchar来存储日期会出现什么问题呢?美国用户在输入日期时使用的格式是MM/dd/yy,而中国用户习惯的输入格式是yyyy-MM-dd或者yyyy/MM/dd,到英国又不一样,而这些格式存储到了数据库中,那么将无法进行日期大小的比较,而且在展示的时候也按原来用户输入的格式再展示给另外一个国家的用户,那么很可能引起误解。

时间类型也有类似的问题,美国用户喜欢使用AM PM来表示上午下午,而中国用户使用24小时制,按字符串存储到数据库中也会存在无法正确排序和比较的问题。

数字的字符串问题在于有的使用逗号作为千分符,而有的国家是使用顿号作为千分符,有的用户又不使用千分符。

金额的话有的输入前面会带货币符号,有的又不带。

回过头来看AdventureWorks数据库模型,他对数据类型的使用都很准确,不存在乱用varchar类型的问题。

币种和汇率

 如果在某个表单中涉及到多个币种问题,那么必须将币种属性添加到表单中。比如SalesOrderHeader。而对于采购订单PurchaseOrder中,由于都是本国采购,全部采用美元结算,所以在PurchaseOrder中没有币种字段。这里需要注意的是,并不是只要某个表单中只采用一种币种,就不需要记录币种信息了,表单的币种如果与财务核算的币种不一致,那么在财务做账时就得进行币种转换,这里由于采购的币种和财务核算的币种都是美元,所以才不需要记录币种。

我们在系统中记录币种信息主要是为了实现币种转换,而币种转换的关键就是汇率,而汇率是一个很复杂的东西,因为汇率是随着时间不断变化的。在系统进行汇率转换时应该取哪个汇率呢?两个币种的汇率存在买入汇率、卖出汇率、中间汇率等,基本介绍可以参考百度文档:http://wenku.baidu.com/link?url=MP0nC_0sIGEIlAfbr-rWSSKcE_bmqQrNclr80WHDfc4kAFZY6S9dskMt5PzPDzkm88iseIhGDhAz9SZEnoQVvtAIRVo13p1kFpTGnCEUVqe

汇率的时间取值可以是日汇率,月底中心汇率,月平均汇率,年度预算汇率等,一般系统都是以“日”为单位,每日记录一次汇率信息,使用中间汇率进行计算的较多。从以上的分析,我们可以建立汇率转换表如下:

NewImage

接下来在记录币种信息时有以下几种建模方法:

1. 完全范式化,只记录交易币种

范式化后汇率表我们可以变成多个表,而在表单中只记录交易币种,那是因为我们可能有多种核算方法,比如同一个系统,欧洲区希望以欧元为币种看到报表,总公司希望看到美元为币种的报表,那么转换后的币种是不确定的,所以不需要记录转换后币种,我们只需要在表单中记录原币种和日期,剩下的就由系统计算得到。

NewImage

这样做后系统是最灵活的,可以应对各种情况,交易时的真实币种为LocalCurrency,可以对应CurrencyRate中的FromCurrency,OrderDate就是CurrencyRateDate,而核算币种StatisticsCurrency(本位币)由系统输入,可以对应CurrencyRate中的ToCurrency,最后再由系统带人CurrencyRateType即可知道本位币的金额。但是由于范式化,也是最慢的,因为在出报表时需要进行多个表的Join,才能得到结果。

2.固定本位币,将汇率类型以列展示

毕竟对于大多数公司来说,核算时永远是用一种固定的本位币(比如美元)这也是AdventureWorks所采取的方式,那么我们可以将系统模型简化,也就是说CurrencyRate表中没有CurrencyRateType栏位,如果要记录日平均汇率进记入“AverageRate”字段,如果要记录当天的最后中间汇率,那么就记入“EndOfDayRate”中,如果我们还想记录更多类型的汇率值,只需要在这个表中增加栏位即可。我们再来看看AdventureWorks系统的模型:

SalesOrder在录入时就已知了LocalCurrency和CurrencyRateDate(一般也就是OrderDate),而我们的本位币是固定的USD,所以基于这3个字段就唯一的确定了CurrencyRate中的一条记录。所以在SalesOrder中并没有记录LocalCurrency,而是引用CurrencyRateID即可。这种模型虽然不会像上一种方案那么灵活,但是由于只需要Join一张表,所以查询效率比上面会快很多。

3. 固定本位币,固定汇率类型,反范式化

如果我们公司不仅本位币是唯一的,而且采用的汇率类型也是固定的(只使用每人中间汇率),那么我们的模型可以出于效率的考虑,进一步反范式化:

每个在记录金额的字段,都加上对应的货币字段,同时也加上按当时的汇率换算成本位币后的金额。

NewImage

这样做的特点是在后期做各种财务统计时不需要在进行表的链接也不需要进行汇率换算,在录入数据时都已经计算好,所以统计速度会很快。当然缺点也是显而易见的,一方面时反范式化后带来的缺点,另一方面是可能在录入数据时并不知道当时的汇率,所以本位币金额就无法计算,只有等公布了当天的汇率后再去补上本位币金额,这是相当麻烦的。还有一个缺点是本位币金额在计算时取的是中间汇率,如果哪天我们希望再以月平均汇率来统计本位币金额,那么还是得重新计算。

时间

时间问题主要是时区的问题,是个比较头疼的问题,在中国可能意识不到这个问题,因为整个中国都是实行+8区的统一时区,而像俄罗斯、美国等国家,他们从东部到西部都是实行不同的时区,而我们的IT系统如果是国际化的,那么就更会遇到时区问题。

时区

时区问题分为录入和显示两个方向。比如一个中国的客户,在2015-5-2早上9:00在系统中下了一个单,那么我们系统记录的是什么时间呢?直接计入2015-5-2 9:00:00吗?但是对于伦敦的管理员来说,他看到这个时间就会很奇怪,因为他们现在才2015-5-2 1:00:00啊,怎么会有未来的单子。所以我们必须将时区作为一个日期的部分,考虑到数据库的模型中。

关于时区,我们可以有两种解决方案:

1.直接记录时区到数据库

为了解决时区问题,SQL Server数据库专门提供了一个数据类型DateTimeOffset,以及相关的函数,用于处理带时区的日期时间。比如我们前面说到的订单日期,我们就可以将其数据类型改为datetimeoffset。而获取时间和时区,有三种数据源:

  • 客户端时间
  • 应用服务器时间
  • 数据库服务器时间

如果使用用户输入的时间值,那么就必须在读取用户输入的值的同时,也得读到用户电脑所设置的时区(这个无论是BS才是CS应用都很容易读取到),然后把时间和时区两个值传到后台数据库,最终以DatetimeOffset的格式存储到数据库中。如果是只需要取当前时间,那么就可以取应用服务器或者数据库服务器的时间和时区。数据库函数为:SYSDATETIMEOFFSET ( )

如果我们使用的数据库没有DateTimeOffSet这种数据类型,那么我也可以将来DateTime和Timezone两个字段来存储时间和时区。

2.转换为UTC时间存到数据库

前面说到我们可以建立Timezone字段来存储时区信息,但是这样做最大的缺点就是不方便数据库中的排序。而且新建的字段也会额外占用存储空间,导致性能降低。除了使用专门为时区而设计的DateTimeOffset数据类型外,我们还可以在应用程序中做时区转换,把所有时间都转换为UTC时间,然后在数据库中就存储UTC时间。

如果是用户从客户端输入或者是应用程序服务器时间,那么只需要在代码中调用系统函数做个简单的转换即可,如果是取数据库当前UTC时间,也有GETUTCDATE()函数可以用。

上面说到的都是录入,而在显示上面,应用程序也只需要读取客户端的时区,然后将数据库中的时间以新时区展示即可。

夏令时

夏令时问题也是一个在中国意识不到的问题,因为中国现在不实行夏令时制(以前实行过几年,后来取消了)。夏令时问题和时区问题独立出来,那是因为我们的系统可能并不是一个国际化的系统,只在一个时区使用,但是这个国家实行了夏令时制,那么我们就需要考虑夏令时给系统带来的影响。

如果我们的系统记录数据库服务器的时间为订单创建的时间,那么在凌晨1:59创建了一个订单,然后一分钟后由于夏令时时间调整,接下来下的订单就变成了1:00创建的了,这个时候如果我们按创建订单的时间进行排序就会有问题,明明先创建的订单,结果却排在了后创建订单的后面。

解决办法很简单,也是就跟时区问题一样,我们可以把时间转换为UTC时间再存储。

度量衡

 度量衡问题是在国际化过程中遇到的最麻烦的问题,因为度量衡的东西太多(长度、面积、体积、温度重量……)而其相互之间的换算还不一样(华氏度和摄氏度有个换算公式,磅和千克又有一个公式,米和英里、英尺、英尺又是不同的公式)这些足够让人抓狂。所以现在大部分系统都回避这个问题,只使用系统录入的时候输入的值和选择的单位。也就是说如果用户在系统中录入一件商品重10磅,那么所有用户看到的都是10磅,对于中国用户来说,根本不知道10磅是啥概念,只有自己去百度磅和斤怎么换算,然后自己拿个计算器算一下到底有几斤重。

AdventureWorks的模型中,只是使用UnitMeasure表存储了系统中用到的度量衡的单位名称,并没有涉及到度量衡的转换问题。假如我们要啃度量衡这块硬骨头,那我们的数据库模型该怎么设计呢?

首先,我们在数据库尽量要以国际标准的度量衡单位为准,存储数值(温度就用摄氏度做标准单位就行了,没必要用开做单位)。然后建立度量衡表,里面设置了以下栏位:单位名称,单位符号,换算方法(乘以倍率,套用换算公式,查询换算表)与标准单位的换算倍率,与标准单位的换算公式/函数(有些单位的换算不是简单的乘以一个倍数就能搞定的,摄氏度和华氏度就是个特例)。换算表表名(比如鞋子尺码的换算,我们可以通过查表获得)具体单位换算规则我们可以参考:http://baike.baidu.com/view/43851.htm

因为很少有系统在国际化中涉及到度量衡换算问题,所以我接下来举一个具体的例子,说明我这个模型的可行性。

我们以一个重量,一个温度和一个鞋码为例子。

NewImage

先按模型创建表:

复制代码
create  table StandardUnit (
StandardUnitCode  varchar( 10not  null,
StandardUnitName  nvarchar( 10not  null,
constraint PK_STANDARDUNIT  primary  key (StandardUnitCode)
)
go
create  table Test (
TestId  int  identity  not  null,
StandardUnitCode  varchar( 10not  null,
TestValue  decimal( 18, 2not  null,
constraint PK_TEST  primary  key (TestId)
)
go
alter  table Test
add  constraint FK_TEST_RELATIONS_STANDARD  foreign  key (StandardUnitCode)
references StandardUnit (StandardUnitCode)
go
create  table UnitMeasure (
UnitId  int  identity  not  null,
StandardUnitCode  varchar( 10not  null,
UnitName  nvarchar( 10not  null,
UnitCode  varchar( 10not  null,
ConvertType  varchar( 50not  null,
ConvertRate  double  precision  null,
ConvertFunction  varchar( 50null,
ConvertTable  varchar( 50null,
constraint PK_UNITMEASURE  primary  key (UnitId)
)
go
alter  table UnitMeasure
add  constraint FK_UNITMEAS_RELATIONS_STANDARD  foreign  key (StandardUnitCode)
references StandardUnit (StandardUnitCode)
go
create  table ShoeSize (
StandardValue  decimal( 10, 2not  null,
ToCode  varchar( 10not  null,
ToValue  decimal( 10, 2not  null,
constraint PK_SHOESIZE  primary  key (StandardValue, ToCode)
)
复制代码

go

接下来我们初始化一些数据:

insert  into StandardUnit  values( ' Kg ',N ' 千克 '),( ' C ',N ' 摄氏度 '),( ' OS ',N ' 欧码 ');
insert  into UnitMeasure  values( ' C ',N ' 华氏度 ', ' F ', ' Function ', null, ' dbo.ConvertC2F ', null);
insert  into UnitMeasure  values( ' OS ',N ' 美码 ', ' US ', ' Table ', null, null, ' dbo.ShoeSize ');
insert  into Test  values( ' Kg ', 0.5),( ' C ', 100),( ' OS ', 43);
insert  into ShoeSize  values( 41, ' US ', 8.5),( 42, ' US ', 9),( 43, ' US ', 9.5);

这里面涉及到一个温度转换函数,我们需要创建数据库函数dbo.ConvertC2F:

复制代码
create  function dbo.ConvertC2F
(
@c  decimal( 10, 2)
)
returns  decimal( 10, 2)
as
begin
return  @c * 1.8 + 32;
end
复制代码

好了,一切准备就绪,现在写一个SQL,把所有Test中的值,转换为磅,华氏度,美码显示出来,那么我们的SQL写为:

复制代码
select t.TestId,um.UnitName,t.TestValue *ConvertRate  as NewValue
from Test t
inner  join UnitMeasure um
on t.StandardUnitCode =um.StandardUnitCode
where um.ConvertType = ' Rate '
union  all
select t.TestId,um.UnitName,dbo.ConvertC2F(t.TestValue)  as NewValue
from Test t
inner  join UnitMeasure um
on t.StandardUnitCode =um.StandardUnitCode
where um.ConvertType = ' Function '
union  all
select t.TestId,um.UnitName,ss.ToValue  as NewValue
from Test t
inner  join UnitMeasure um
on t.StandardUnitCode =um.StandardUnitCode
inner  join dbo.ShoeSize ss
on t.TestValue =ss.StandardValue  and ss.ToCode =um.UnitCode
复制代码

where um.ConvertType='Table' 

这里由于Function和Table是动态配置的,所以这个SQL是程序先读取了UnitMeasure中的值,然后动态生成的。如果不依靠程序动态生成SQL,我们可以修改模型,去掉ConvertFunction和ConvertTable两个字段,写死一个固定的函数和查找表,毕竟需要用到转换函数的,我目前也就找到了温度,其他基本上都是乘以一个系数就搞定。下面我们就来看看改进后的模型:

NewImage

对应的SQL为:

 create table StandardUnit (

复制代码
StandardUnitCode  varchar( 10not  null,
StandardUnitName  nvarchar( 10not  null,
constraint PK_STANDARDUNIT  primary  key (StandardUnitCode)
)
go
create  table Test (
TestId  int  identity  not  null,
StandardUnitCode  varchar( 10not  null,
TestValue  decimal( 18, 2not  null,
constraint PK_TEST  primary  key (TestId)
)
go
create  table UnitMeasure (
UnitCode  varchar( 10not  null,
StandardUnitCode  varchar( 10not  null,
UnitName  nvarchar( 10null,
ConvertType  varchar( 50null,
ConvertRate  double  precision  null,
constraint PK_UNITMEASURE  primary  key (UnitCode)
)
go
create  table UnitValueMapping (
StandardUnitCode  varchar( 10not  null,
StandardValue  decimal( 18, 2not  null,
UnitCode  varchar( 10not  null,
ToValue  decimal( 18, 2null,
constraint PK_UNITVALUEMAPPING  primary  key (StandardUnitCode, UnitCode, StandardValue)
)
go
alter  table Test
add  constraint FK_TEST_RELATIONS_STANDARD  foreign  key (StandardUnitCode)
references StandardUnit (StandardUnitCode)
go
alter  table UnitMeasure
add  constraint FK_UNITMEAS_RELATIONS_STANDARD  foreign  key (StandardUnitCode)
references StandardUnit (StandardUnitCode)
go
alter  table UnitValueMapping
add  constraint FK_UNITVALU_RELATIONS_STANDARD  foreign  key (StandardUnitCode)
references StandardUnit (StandardUnitCode)
go
alter  table UnitValueMapping
add  constraint FK_UNITVALU_RELATIONS_UNITMEAS  foreign  key (UnitCode)
references UnitMeasure (UnitCode)
go
复制代码

初始化的数据也差不多,这里就不需要再写初始化脚本了,我们来看看转换语句:

复制代码
select t.TestId,um.UnitName,t.TestValue *ConvertRate  as NewValue
from Test t
inner  join UnitMeasure um
on t.StandardUnitCode =um.StandardUnitCode
where um.ConvertType = ' Rate '
union  all
select t.TestId,um.UnitName,t.TestValue * 1.8 + 32  as NewValue
from Test t
inner  join UnitMeasure um
on t.StandardUnitCode =um.StandardUnitCode
where um.ConvertType = ' Function '
union  all
select t.TestId,um.UnitName,m.ToValue  as NewValue
from Test t
inner  join UnitMeasure um
on t.StandardUnitCode =um.StandardUnitCode
inner  join dbo.UnitValueMapping m
on t.TestValue =m.StandardValue  and m.UnitCode =um.UnitCode  and m.StandardUnitCode =t.StandardUnitCode
复制代码

where um.ConvertType='Table' 

【其实鞋码转换问题不能算是度量衡问题,只是为了说明这个模型的扩展性,增加个查找表的转换模式,所以举了这个例子。】

目录
打赏
0
0
0
0
14
分享
相关文章
Java新纪元:模块化设计重塑开发体验,让代码如艺术品般优雅!
【8月更文挑战第30天】Java模块化设计是一种通过将应用程序分解为独立、可重用模块来提高代码可维护性和可扩展性的方法。其核心是自Java 9引入的Java Platform Module System(JPMS)。模块化设计增强了封装性,简化了依赖管理和系统维护,并通过按需加载模块提升了性能。每个模块有明确职责和接口,通过`module-info.java`文件定义。实战示例展示了如何创建和使用模块。最佳实践包括明确模块职责、减少依赖、使用模块描述符及模块化测试,从而构建健壮、可维护的应用程序。
107 2
低代码和无代码:简单概念之下的深刻内涵
从2020年到2024年,低代码和无代码开发平台凭借其独特优势,逐渐成为企业敏捷开发和快速响应市场变化的利器。本文深入探讨了这两种平台的概念、用户需求及开发内涵,揭示了它们在现代软件开发中的重要价值和应用场景,帮助读者更好地理解低代码和无代码平台的核心特点及其对企业数字化转型的推动作用。
安卓应用开发中的四大关键技巧月26日
在这篇技术性文章中,我们将深入探讨安卓应用开发的四大关键技巧。通过分析每个技巧的理论基础和实际应用案例,帮助读者更好地理解和掌握这些技巧,从而提升安卓应用的开发效率和质量。
49 2
无障碍设计的魔法:JSF让每个用户都能畅游数字世界!
【8月更文挑战第31天】本文介绍如何使用JavaServer Faces (JSF)构建无障碍Web应用,确保所有用户都能访问和使用。文章通过实际代码示例展示了如何利用ARIA属性增强组件、实现键盘导航、提供文本替代以及使用语义化标签等技术。无障碍设计不仅是道德责任,也是提升用户体验的关键。通过这些方法,JSF可以帮助开发者创建更加公平和包容的应用。
71 0
Spring 框架里竟藏着神秘魔法?国际化与本地化的奇妙之旅等你来揭开谜底!
【8月更文挑战第31天】在软件开发中,国际化(I18N)与本地化(L10N)对于满足不同地区用户需求至关重要。Spring框架提供了强大支持,利用资源文件和`MessageSource`实现多语言文本管理。通过配置日期格式和货币符号,进一步完善本地化功能。合理应用这些特性,可显著提升应用的多地区适应性和用户体验。
79 0
安卓开发中的MVP架构模式深度解析
【4月更文挑战第30天】在移动应用开发领域,模型-视图-呈现器(Model-View-Presenter, MVP)是一种广泛采用的架构模式。它旨在通过解耦组件间的直接交互来提高代码的可维护性和可测试性。本文将深入探讨MVP在安卓开发中的应用,揭示其如何促进代码的模块化,提升用户界面的响应性,并简化单元测试过程。我们将从理论概念出发,逐步过渡到实践案例,为读者提供一套行之有效的MVP实施策略。
uniapp项目实践拓展章:代码统一风格
uniapp项目实践拓展章:代码统一风格
150 0

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等