编写可在多种语言之间移植的 Transact-SQL 代码


可以在多种语言之间移植的 Transact-SQL 代码的开发人员所面临的一些问题。在 SQL Server 7.0 中引入了 Unicode 数据类型、排序规则以及其他各种国际化和本地化功能,从而与以前的版本相比,使得编写可以在多种语言之间移植的 Transact-SQL 代码变得更加容易。SQL Server 2000 通过添加列级别的排序规则等内容增强了 SQL Server 7.0 中的国际化功能,因此可供开发人员使用的工具已经随着时间的推移而变得越来越好。

SQL Server 联机图书 (BOL) 包含一个简短的部分,内容是关于编写可以在多种语言之间移植的代码的,您应该首先阅读一下。如果您尚未阅读 BOL 主题“Writing International Transact-SQL Statements”(编写国际性 Transact-SQL 语句),我建议您首先阅读一下,然后再阅读本专栏。

日期
在开始讨论编写可移植代码时,让我们首先探讨一下以可移植的方式处理日期时所涉及到的问题。尽管人们通常将编写可移植的数据库代码与以不同方式处理常规字符串联系起来,但一开始编写可移植的日期处理代码是一种很好的方法,因为它基本上只需知道一些简单的规则和选项就可以了。

有关以可移植的方式在 Transact-SQL 中处理日期方面的内容,我想谈的第一点就是:避免就日期格式或一周的第一天进行假设。第一点似乎是很显然的 — 大多数人都知道全世界的日期格式是不一样的。然而,第二点可能不是那么明显。在不同的文化中,将哪一天视为一周的第一天是不同的。例如,请看以下代码:

set language us_english
select datepart (dw, '20060606')
set language british
select datepart (dw, '20060606')
它将返回:

Changed language setting to us_english.
-----------
3
Changed language setting to British.
-----------
2
这两个查询返回不同的结果,这是因为在美国和英国,将哪一天视为一周的第一天是不同的。您可以通过查询系统函数 @@datefirst 来核实这一点:

Changed language setting to us_english.
----
7
Changed language setting to British.
----
1
您可以通过 SET DATEFIRST 命令更改将哪一天视为一周的第一天(从而更改 @@datefirst 返回的值)。默认设置随文化的不同而异。

对于以日期不可知的方式编写使用 datepart(dw,...) 的代码,有两种简单方法。第一种方法是在任何过程的开始简单地调用 SET DATEFIRST,以便就一周的第一天进行假设。这将在相应过程中覆盖 DATEFIRST 的连接设置。第二种方法是规格化 @@datefirst 返回的值,以使其与语言无关。下面是一些说明如何做到这一点的代码。

declare @ndf_dw int
set language us_english
select @ndf_dw = (@@datefirst + datepart(dw, '20060606')) % 7
select @ndf_dw, datepart(dw, '20060606')
set language british
select @ndf_dw = (@@datefirst + datepart(dw, '20060606')) % 7
select @ndf_dw, datepart(dw, '20060606')
如果您运行上述代码,您将看到 datepart(dw,...) 返回的值随当前语言设置的不同而不同,但是为 @ndf_dw 返回的值是相同的。

日期名称
Transact-SQL 日期函数为月份和星期几返回的名称将随语言设置的不同而不同。正如联机图书中所指出的,这意味着您应该使用数值日期部分而不是名称字符串来进行月份和星期几的比较。例如:

select DATENAME(dw,'20060606')
如果默认语言被设置为美国英语,则返回:

------------------------------
Tuesday
但如果默认语言被设置为法语,则返回:

------------------------------
Mardi
很明显,如果您编写的代码依赖于返回的特定语言的日期字符串,则当默认语言被设置为其他某种语言时,返回的字符串将可能破坏您的代码。使用数值日期部分可以缓解这一问题。

显示日期值
当然,在提供数据以供用户使用或显示时,您将希望使用日期部分的名称,因为它们通常会更有意义。对于基本的两层应用程序,最简单的方法可能是让 SQL Server 为您返回这些名称,而不是通过客户端应用程序中的其他某些方式来转换这些名称。如果登录语言的设置不正确,这显然不能按预期方式工作。

对于涉及到中间层或连接池的更复杂环境,假设特定最终用户的登录语言设置将是正确的可能不切实际。在这些情况下,一种更好的方法是从 T-SQL 代码中返回明确的二进制日期/时间值,从而可以在客户端应用程序中将其转换为有意义的字符串。

比较和存储日期
当在 DML 或比较语句中指定日期时,请务必使用在所有语言中都具有相同含义的常量。在客户端应用程序中做到这一点的最简单且最安全的方法是,将此类语句作为 RPC 事件提交给服务器(例如,使用托管代码中的 SqlCommand 对象),并且使用显式参数传递日期。从客户端应用程序传递的并且以其内置格式编码的参数在本质上是明确的。

另一种选择是使用 ODBC 转义子句来表示在各种语言中一致的特定格式。应用程序不需要是 ODBC 应用程序,因为转义子句由服务器解释。前面提到过的联机图书主题“Writing International Transact-SQL Statements”(编写国际性 Transact-SQL 语句)详细介绍了受支持的转义子句。

应用程序还可以使用不带分隔符的、各个部分按重要性从高到低的顺序(即 yyyymmdd)排序的字符串。无论语言设置如何,SQL Server 都将正确地解释具有这种格式的日期。

另一种选择是使用 CONVERT() Transact-SQL 函数及其样式参数,以便显式指定转换日期的目标格式或源格式。这将消除日期/时间字符串的歧义,并且可以在各种语言之间移植。

Unicode
在编写可以在各种语言之间移植的 Transact-SQL 代码时,最简单最直接的方法是使用 Unicode 数据类型来表示和存储字符数据。这意味着您应该使用 nchar 来代替 char,使用 nvarchar 来代替 varchar,以及使用 ntext 来代替 text。尽管使用 Unicode 数据类型会在可存储的字符串的长度方面有所限制,并且可能比使用非 Unicode 类型稍微慢一些和麻烦一些,但它仍不失为处理语言可移植性问题的最简单方法,并且是唯一的、无须考虑其他编码注意事项就能行得通的解决方案。

在采用这一方法时,必须按照联机图书主题“Using Unicode Data”(使用 Unicode 数据)中所规定的那样,小心地在 Unicode 字符串常量前面加上一个大写字母 N 前缀。正如知识库文章 239530, INF:Unicode String Constants in SQL Server Require N Prefix 所指出的,不这样做将导致 SQL Server 在使用相应的字符串之前将其转换为当前数据库的非 Unicode 代码页。下面是一个示例:

-- Assumes the default code page is not Greek
create table #t1 (c1 nchar(1))
insert #t1 values(N'Ω')
insert #t1 values('Ω')
select * from #t1
这将返回:

c1
----
Ω
O
两个 insert 语句都试图将单个字符(希腊字母 Ω)插入到表中。正如您所看到的,在第一行的 insert 中没有数据丢失。然而,在第二行中,Ω 已经被转换为不同的字符(一个大写的 O),这是因为在第二个 INSERT 语句中缺少 N 前缀,从而导致字符串被转换为默认的代码页,而在该代码页中不存在 Ω(大写的 O 是最近似的字符)。

尽管该表的列使用 Unicode 数据类型,仍然不能防止数据丢失。发生数据丢失的原因是在源数据中省略了大写 N 前缀,因而 SQL Server 在插入源数据之前将其转换为默认代码页。对于日期值,如果您将采用 Unicode 参数的查询作为 RPC 事件提交,则可以缓解通过 N 前缀来区分 Unicode 字符串和非 Unicode 字符串的需要 — RPC 参数本身就可以向服务器定义其数据类型。

排序规则
请观察下面的代码,看一下您是否能够确定它有什么错误:

create function hexnum(@hexstr varchar(10))
returns bigint
as
begin
if left(@hexstr,2) in ('0x','0X') set @hexstr=substring(@hexstr,3,10)  -- Lop off 0x prefix
declare @i int, @res bigint, @l int, @c char
select @i=1, @l=len(@hexstr), @res=0
if @hexstr is null OR @l=0 return null
while @i<=@l begin
set @c=upper(substring(@hexstr,@i,1))
if @c<'0' OR @c>'F' return(null)
set @res=@res+cast(1.0 as bigint)*case when isnumeric(@c)=1 then
cast(@c as int) else ascii(@c)-55 end*power(16,@l-@i)
set @i=@i+1
end
return(@res)
end
hexnum() UDF 的明显目的是将传递给该函数的十六进制字符串转换为相应的整数值。您看出该函数有什么错误了吗?让我给您一点提示:它与排序规则依赖性有关。让我们仔细看一下以下的代码行:

if @c<'0' OR @c>'F' return(null)
SQL Server 2000 将一个字符视为小于还是大于另一个字符与排序规则相关:排序规则定义了用于表示字符串中各个字符的位模式,以及对字符的排序和比较方式进行控制的规则。

上述代码行的目的是筛选掉函数的无效输入。如果一个字符不在 0 和 9 之间或者 A 和 F 之间,则它不可能是有效的十六进制数字,从而将导致函数失败。

本文作者:
« 
» 
快速导航

Copyright © 2016 phpStudy | 豫ICP备2021030365号-3