第五章 SQL定义表(二)
主键
InterSystems IRIS提供了两种方法来唯一标识表中的行:RowID
和主键。
可选的主键是一个有意义的值,应用程序可以使用该值唯一地标识表中的行(例如,联接中的行)。主键可以是用户指定的数据字段,也可以是多个数据字段的组合。主键值必须是唯一的,但不必是整数值。 RowID
是一个内部用于标识表中行的整数值。通常,主键是由应用程序生成的值,而RowID
是由InterSystems IRIS生成的唯一整数值。
系统会自动创建一个主map,以使用RowID
字段访问数据行。如果定义主键字段,系统将自动创建并维护主键索引。
显然,具有两个不同的字段和索引来标识行的双重性不一定是一件好事。可以通过以下两种方式之一解析为单个行标识符和索引:
- 使用应用程序生成的主键值作为
IDKEY
。
可以通过使用关键字PrimaryKey
和IdKey
在类定义中标识主键索引来实现这一点(如果为此目的设置了PKey is IdKey
标志,也可以在DDL中实现这一点)。
这使得主键索引成为表的主映射。
因此,主键将被用作行的主要内部地址。
如果主键包含多个字段,或者主键值不是整数,那么这种方法的效率会较低。 - 不要使用应用程序生成的主键值,而应在应用程序中使用系统生成的
RowID
整数作为应用程序使用的主键(例如,在joins
中)。这样做的好处是,整数RowID
有助于进行更有效的处理,包括使用位图索引。
根据应用程序的性质,可能希望解析为单个行标识符和索引,或者为应用程序生成的主键和系统生成的RowID
具有单独的索引。
RowVersion,AutoIncrement和串行计数器字段
InterSystems SQL支持三种专用数据类型,用于自动增加计数器值。这三种数据类型都是扩展%Library.BigInt
数据类型类的子类。
-
%Library.RowVersion
:计算在命名空间范围内所有RowVersion
表的插入和更新。只有在包含ROWVERSION
字段的表中进行插入和更新时,此计数器才会递增。ROWVERSION
值是唯一的且不可修改。此名称空间范围的计数器永远不会重置。 -
%Library.Counter
(也称为SERIAL计数器字段):对表中的插入进行计数。默认情况下,此字段接收一个自动递增的整数。但是,用户可以为此字段指定一个非零的整数值。用户可以指定重复值。如果用户提供的值大于系统提供的最高值,则将自动递增计数器设置为从用户指定的值开始递增。 -
%Library.AutoIncrement
:计数插入到表中的次数。默认情况下,此字段接收一个自动递增的整数。但是,用户可以为此字段指定一个非零的整数值。用户可以指定重复值。指定用户值对自动增量计数器无效。
这三个字段以及IDENTITY
字段均返回AUTO_INCREMENT = YES
,如以下示例所示:
SELECT COLUMN_NAME,AUTO_INCREMENT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'MyTable'
RowVersion Field
RowVersion
字段是一个可选的用户定义字段,它提供行级版本控制,使可以确定对每个命名空间范围内的行中的数据进行更改的顺序。 InterSystems IRIS维护一个整个命名空间范围的计数器,并在每次修改行数据(插入,更新或%Save
)时向该字段分配一个唯一的增量正整数。因为此计数器是整个名称空间范围的,所以对具有ROWVERSION
字段的一个表进行的操作将设置ROWVERSION
计数器的增量点,该值将用于同一名称空间中具有ROWVERSION
字段的所有其他表。
通过指定数据类型为ROWVERSION(%Library.RowVersion)
的字段来创建RowVersion
字段。每个表只能指定一个ROWVERSION
数据类型字段。尝试创建具有多个ROWVERSION
字段的表会导致5320编译错误。
该字段可以具有任何名称,并且可以出现在任何列位置。 ROWVERSION(%Library.RowVersion)
数据类型映射到BIGINT(%Library.BigInt)
。
此字段从自动递增计数器接收一个从1开始的正整数。只要通过插入,更新或%Save
操作修改了任何启用ROWVERSION
的表中的数据,此计数器就会递增。递增的值记录在已插入或更新的行的ROWVERSION
字段中。
名称空间可以包含具有RowVersion
字段的表和不具有该字段的表。仅对具有RowVersion
字段的表的数据更改会增加整个命名空间范围的计数器。
当用数据填充表时,InterSystems IRIS会为每个插入的行将此字段分配连续的整数。如果使用ALTER TABLE
将ROWVERSION
字段添加到已经包含数据的表中,则该字段将被创建为NULL
以用于预先存在的字段。对该表的任何后续插入或更新都会为该行的RowVersion
字段分配一个顺序整数。该字段是只读的;尝试修改RowVersion
值会生成SQLCODE -138
错误:无法为只读字段插入/更新值。因此,RowVersion
字段被定义为唯一且不可修改,但不是必需字段或非null
。
RowVersion
值始终递增。它们不被重用。因此,插入和更新按时间顺序分配唯一的RowVersion
值。删除操作从该序列中删除数字。因此,RowVersion
值可能在数字上不连续。
此计数器永远不会重置。删除所有表数据不会重置RowVersion
计数器。即使删除名称空间中包含ROWVERSION
字段的所有表,也不会重置此计数器。
RowVersion
字段不应包含在唯一键或主键中。 RowVersion
字段不能是IDKey
索引的一部分。
分片表不能包含RowVersion
字段。
RowVersion
字段未隐藏(通过SELECT *
显示)。
在同一名称空间中的三个表的以下示例中显示了这一点。
- 创建
表1
和表3
,每个都有一个ROWVERSION
字段,并创建表2没有一个ROWVERSION
字段。 - 在
Table1
中插入十行。这些行的ROWVERSION
值是接下来的十个计数器增量。由于以前未使用过计数器,因此它们是1到10。 - 在
Table2
中插入十行。由于Table2
没有ROWVERSION
字段,因此计数器不会增加。 - 更新
表1
的行。该行的ROWVERSION
值将更改为下一个计数器增量(在这种情况下为11)。 - 在
Table3
中插入十行。这些行的ROWVERSION
值是接下来的十个计数器增量(12到21)。 - 更新
表1
的行。该行的ROWVERSION
值更改为下一个计数器增量(在这种情况下为22)。 - 删除
表1
的行。ROWVERSION
计数器不变。 - 更新
Table3
的一行。该行的ROWVERSION
值将更改为下一个计数器增量(在这种情况下为23)。
Serial Counter Field
可以使用SERIAL
数据类型(在持久性类表定义中为%Library.Counter
)来指定一个或多个可选的整数计数器字段,以记录在表中插入记录的顺序。每个串行计数器字段都维护自己的独立计数器。
每当将一行插入表中时,串行计数器字段都会从其自动增量计数器接收一个正整数,该行没有提供任何值(NULL
)或值为0。但是,用户可以指定非零整数值插入期间针对此字段的值,将覆盖表计数器的默认值。
- 如果
INSERT
没有为计数器字段指定非零整数值,则计数器字段将自动接收正整数计数器值。计数从1开始。每个连续值都是从为此字段分配的最高计数器值开始的1增量。 - 如果
INSERT
为counter
字段指定了一个非零的整数值,则该字段将接收该值。它可以是正整数或负整数,可以低于或高于当前计数器值,并且可以是已经分配给该字段的整数。如果该值大于任何分配的计数器值,它将自动增量计数器的增量起始点设置为该值。
尝试更新计数器字段值会导致SQLCODE -105
错误。
TRUNCATE TABLE
命令将该计数器重置为1。即使使用DELETE
命令删除表中的所有行,也不会通过DELETE命令将其重置。
分片表不能包含串行计数器字段。
AutoIncrement Field
可以使用%Library.AutoIncrement
数据类型(或BIGINT AUTO_INCREMENT
)来指定一个整数计数器字段,以记录在表中插入记录的顺序。每个表只能指定一个%AutoIncrement
数据类型字段。每当将一行插入表中时,此字段都会从自动增量计数器接收一个正整数,该行没有提供任何值(NULL
)或值为0。但是,用户可以为此指定非零整数值插入过程中的字段,将覆盖表计数器的默认值。
- 如果
INSERT
没有为计数器字段指定非零整数值,则计数器字段将自动接收正整数计数器值。计数从1开始。每个连续值都是从为此字段分配的最高计数器值开始的1增量。 - 如果
INSERT
为counter
字段指定了一个非零的整数值,则该字段将接收该值。它可以是正整数或负整数,可以低于或高于当前计数器值,并且可以是已经分配给该字段的整数。用户分配的值对自动增量计数器无效。
尝试更新计数器字段值会导致SQLCODE -105
错误。
TRUNCATE TABLE
命令将该计数器重置为1。即使使用DELETE
命令删除表中的所有行,也不会通过DELETE
命令将其重置。
分片表可以包含一个AutoIncrement
字段。
通过创建持久性类来定义表
在InterSystems IRIS中定义表的主要方法是使用Studio创建持久性类定义。当这些类在InterSystems IRIS数据库中保存并编译时,它们会自动投影到与类定义相对应的关系表中:每个类代表一个表;每个类代表一个表。每个属性代表一列,依此类推。可为一个类(表)定义的属性(列)的最大数量为1000。
例如,以下定义了持久类MyApp.Person
:
Class MyApp.Person Extends %Persistent
{
Property Name As %String(MAXLEN=50) [Required];
Property SSN As %String(MAXLEN=15) [InitialExpression = "Unknown"];
Property DateOfBirth As %Date;
Property Sex As %String(MAXLEN=1);
}
编译后,这将在MyApp模式中创建MyApp.Person
持久类和相应的SQL表Person
。
在此示例中,指定了程序包名称MyApp
。定义持久类时,未指定的程序包名称默认为User
。这对应于默认的SQL模式名称SQLUser
。例如,将名为“Students”
的表定义为持久类将创建类User.Students
,以及相应的SQL schema.table
名称SQLUser.Students
。
在此示例中,持久类名称Person
是默认的SQL表名称。可以使用SqlTableName
类关键字来提供其他SQL表名称。
可以使用DDL CREATE TABLE
语句(指定SQL schema.table
名称)定义相同的MyApp.Person
表。成功执行此SQL语句会生成一个相应的持久性类,其包名称为MyApp,类名称为Person:
CREATE TABLE MyApp.Person (
Name VARCHAR(50) NOT NULL,
SSN VARCHAR(15) DEFAULT 'Unknown',
DateOfBirth DATE,
Sex VARCHAR(1)
)
CREATE TABLE
在相应的类定义中未指定显式的StorageStrategy
。相反,它将采用已定义的默认存储策略。
默认情况下,CREATE TABLE
在相应的类定义中指定Final class
关键字,指示它不能具有子类。
请注意,诸如上图所示的持久性类定义在编译时会创建相应的表,但是无法使用SQL DDL命令(或通过使用Management Portal Drop操作)来修改或删除此表定义,这会向显示消息“未为类'schema.name'启用DDL ...”)。必须在表类定义中指定[DdlAllowed]才能进行以下操作:
Class MyApp.Person Extends %Persistent [DdlAllowed]
可以在类定义中指定%Populate
以启用使用测试数据自动填充表。
Class MyApp.Person Extends (%Persistent,%Populate) [DdlAllowed]
这为该类提供了Populate()
方法。运行此方法将在表中填充十行测试数据。
定义数据值参数
每个属性(字段)定义都必须指定一个数据类型类,该类指定该属性所基于的类。指定的数据类型将字段的允许数据值限制为该数据类型。定义投影到表的持久类时,必须使用%Library
包中的类指定此数据类型。可以将此类指定为%Library.Datatype
或%Datatype
。
许多数据类型类提供的参数使可以进一步定义允许的数据值。这些参数特定于单个数据类型。以下是一些较常见的数据定义参数:
- 数据值物理限制
- 允许的数据值:枚举或模式匹配
- 通过定义唯一索引来唯一数据值
- 通过定义
SqlComputeCode
计算数据值
数据值限制
对于数字数据类型,可以指定MAXVAL
和MINVAL
参数以限制允许值的范围。根据定义,数字数据类型具有最大支持值(正数和负数)。可以使用MAXVAL
和MINVAL
进一步限制允许的范围。
对于字符串数据类型,可以指定MAXLEN
和MINLEN
参数以限制允许的长度(以字符为单位)。根据定义,字符串数据类型具有最大支持的长度。可以使用MAXLEN
和MINLEN
进一步限制允许的范围。默认情况下,超过MAXLEN
的数据值会生成字段验证错误:INSERT
的SQLCODE -104
或UPDATE
的SQLCODE -105
。可以指定TRUNCATE = 1
以允许超过MAXLEN
的字符串数据值。指定的字符串将被截断为MAXLEN
长度。
允许的数据值
可以通过两种方式限制实际数据值:
- 允许值的列表(带有
VALUELIST
和DISPLAYLIST
的枚举值)。 - 允许值的匹配模式(
PATTERN
)。
枚举值
通过将表定义为持久类,可以定义仅包含某些指定值的属性(字段)。这是通过指定VALUELIST
参数来完成的。 VALUELIST
(指定逻辑存储值的列表)通常与DISPLAYLIST
(指定相应的显示值的列表)一起使用。这两个列表都以列表定界符开头。几种数据类型可以指定VALUELIST
和DISPLAYLIST
。下面的示例定义两个带有枚举值的属性:
Class Sample.Students Extends %Persistent
{
Property Name As %String(MAXLEN=50) [Required];
Property DateOfBirth As %Date;
Property ChoiceStr As %String(VALUELIST=",0,1,2",DISPLAYLIST=",NO,YES,MAYBE");
Property ChoiceODBCStr As %EnumString(VALUELIST=",0,1,2",DISPLAYLIST=",NO,YES,MAYBE");
}
如果指定了VALUELIST
,则INSERT
或UPDATE
只能指定VALUELIST
中列出的值之一,或者不提供值(NULL
)。 VALUELIST
有效值区分大小写。指定与VALUELIST
值不匹配的数据值会导致字段值验证失败:INSERT
的SQLCODE -104
或UPDATE
的SQLCODE -105
。
在ODBC模式下显示时,%String
和%EnumString
数据类型的行为不同。使用上面的示例,当以逻辑模式显示时,ChoiceStr
和ChoiceODBCStr
都显示其VALUELIST
值。在“显示”模式下显示时,ChoiceStr
和ChoiceODBCStr
均显示其DISPLAYLIST值。当以ODBC模式显示时,ChoiceStr
显示VALUELIST
值;否则显示VALUELIST
值。 ChoiceODBCStr
显示DISPLAYLIST
值。
值的模式匹配
几种数据类型可以指定PATTERN
参数。 PATTERN
将允许的数据值限制为与指定的ObjectScript模式匹配的数据值,指定为带引号的字符串,省略前导问号。以下示例使用模式定义属性:
Class Sample.Students Extends %Persistent
{
Property Name As %String(MAXLEN=50) [Required];
Property DateOfBirth As %Date;
Property Telephone As %String(PATTERN = "3N1""-""3N1""-""4N");
}
由于将模式指定为带引号的字符串,因此模式中指定的文字必须将其双引号引起来。请注意,模式匹配是在MAXLEN
和TRUNCATE
之前应用的。因此,如果为可能超过MAXLEN
并被截断的字符串指定了一个模式,则可能希望以“ .E
”(任何类型的尾随字符数不限)结束该模式。
与PATTERN
不匹配的数据值会生成字段验证错误:INSERT
的SQLCODE -104
或UPDATE
的SQLCODE -105
。
唯一值
CREATE TABLE
允许将字段定义为UNIQUE
。这意味着每个字段值都是唯一(非重复)值。
将表定义为持久类不支持相应的uniqueness
属性关键字。相反,必须同时定义属性和该属性的唯一索引。下面的示例为每个记录提供唯一的Num值:
Class Sample.CaveDwellers Extends %Persistent [ DdlAllowed ]
{
Property Num As %Integer;
Property Troglodyte As %String(MAXLEN=50);
Index UniqueNumIdx On Num [ Type=index,Unique ];
}
索引名称遵循属性的命名约定。可选的Type
关键字指定索引类型。 Unique
关键字将属性(字段)定义为唯一。
使用INSERT
或UPDATE
语句时,必须具有唯一的值字段。
计算值
下面的类定义示例定义一个表,该表包含一个字段(生日),该字段在最初设置DateOfBirth
字段值时使用SqlComputed
来计算其值,而在更新DateOfBirth
字段值时使用SqlComputeOnChange
来重新计算其值。 Birthday
字段值包括当前时间戳,以记录该字段值的计算/重新计算时间:
Class Sample.MyStudents Extends %Persistent [DdlAllowed]
{
Property Name As %String(MAXLEN=50) [Required];
Property DateOfBirth As %Date;
Property Birthday As %String
[ SqlComputeCode = {SET {Birthday}=$PIECE($ZDATE({DateOfBirth},9),",")_
" changed: "_$ZTIMESTAMP},
SqlComputed, SqlComputeOnChange = DateOfBirth ];
}
```java
请注意,对`DateOfBirth`的`UPDATE`指定现有的`DateOfBirth`值不会重新计算`Birthday`字段值。
## 嵌入式对象(%SerialObject)
可以通过引用定义属性的嵌入式串行对象类来简化持久表的结构。例如,希望`MyData.Person`包含地址信息,包括街道,城市,州和邮政编码。可以定义一个定义这些属性的串行对象(`%SerialObject`)类,而不是在`MyData.Person`中指定这些属性,然后在`MyData.Person`中指定一个引用该嵌入式对象的`Home`属性。在以下类定义中显示了这一点:
```java
Class MyData.Person Extends (%Persistent) [ DdlAllowed ]
{ Property Name As %String(MAXLEN=50);
Property Home As MyData.Address;
Property Age As %Integer;
}
Class MyData.Address Extends (%SerialObject)
{ Property Street As %String;
Property City As %String;
Property State As %String;
Property PostalCode As %String;
}
不能直接访问串行对象属性中的数据,必须通过引用它的持久类/表访问它们:
- 要从持久性表中引用单个串行对象属性,请使用下划线。例如,
SELECT名称Home_State FROM MyData.Person
返回状态串行对象属性值作为字符串。串行对象属性值以查询中指定的顺序返回。 - 要引用持久性表中的所有串行对象属性,请指定引用字段。例如,
SELECT Home FROM MyData.Person
以%List
结构形式返回所有MyData.Address
属性的值。串行对象属性值以串行对象中指定的顺序返回:Home_Street,Home_City,Home_State,Home_PostalCode。在Management Portal SQL
界面“目录详细信息”中,此引用字段称为“容器”字段。这是一个Hidden字段,因此SELECT *
语法不返回。 - 持久类的
SELECT *
单独返回所有串行对象属性,包括嵌套的串行对象。例如,SELECT * FROM MyData.Person
返回Age,Name,Home_City,Home_PostalCode,Home_State和Home_Street
值(按此顺序);它不返回Home%List
结构值。串行对象属性值以排序顺序返回。SELECT *
首先按排序顺序(通常按字母顺序)列出持久性类中的所有字段,然后按排序顺序列出嵌套的串行对象属性。
请注意,嵌入式串行对象不必与引用它的持久性表位于同一程序包中。
定义嵌入式对象可以简化持久性表定义:
- 持久表可以包含多个属性,这些属性引用同一嵌入式对象中的不同记录。例如,
MyData.Person
表可以包含Home
和Office
属性,这两个属性均引用MyData.Address
串行对象类。 - 多个持久表可以引用同一嵌入式对象的实例。例如,
MyData.Person
表的Home
属性和MyData.Employee WorkPlace
属性都可以引用MyData.Address
串行对象类。 - 一个嵌入式对象可以引用另一个嵌入式对象。例如,
MyData.Address
嵌入式对象包含引用MyData.Telephone
嵌入式对象的Phone
属性,其中包含CountryCode
,AreaCode
和PhoneNum
属性。在持久类中,使用多个下划线来引用嵌套的串行对象属性,例如Home_Phone_AreaCode
。
编译串行对象类会在存储定义中生成数据规范。编译器通过在串行对象类名称后附加单词“State”
来为该规范分配数据名称。因此,为MyData.Address
分配了。如果此名称(在此示例中为
AddressState
)已经用作属性名称,则编译器将附加一个整数以创建唯一的数据名称:。
类方法
可以将类方法指定为表定义的一部分,如以下示例所示:
Class MyApp.Person Extends %Persistent
{
Property Name As %String(MAXLEN=50) [Required];
Property SSN As %String(MAXLEN=15) [InitialExpression = "Unknown"];
Property DateOfBirth As %Date;
Property Sex As %String(MAXLEN=1);
ClassMethod Numbers() As %Integer [ SqlName = Numbers, SqlProc ]
{
QUIT 123
}
}
在SELECT查询中,可以按以下方式调用此方法:
SELECT Name,SSN,Sample.Numbers() FROM Sample.Person
通过创建持久性类来定义分片表
必须先建立分片环境,然后才能定义作为分片表投影的持久性类。
要将持久性类定义为分片,请指定类关键字Sharded = 1
。 (类关键字Sharded = 2
保留供生成的类内部使用。)
注意:请勿尝试设置或更改现有类定义的与分片相关的类属性。仅应为不包含数据的新表指定这些属性。这包括设置Sharded
类关键字和与分片相关的索引关键字。尝试编辑现有类的任何与分片相关的属性都可能导致数据无法访问。
下例显示了Sharded = 1
持久类的类定义:
Class Sample.MyShardT Extends %Persistent [ ClassType = persistent, DdlAllowed, Final, Sharded = 1]
{
...
}
如果将一个类定义为分片,则它必须是持久性的ClassType
。如果未将分片类定义为ClassType
持久类,则在类编译期间将返回错误,例如:ERROR#5599:分片类'Sample.Address'必须为ClassType'persistent',而不是ClassType'serial'
。分片类使用的存储类必须为%Storage.Persistent
或其子类%Storage.Shard
。如果分片类的存储类不是%Storage.Persistent
,则在类编译期间将返回以下错误:错误#5598:分片类'Sample.Vendor'必须使用存储类型%Storage.Persistent,而不是存储类型' %Storage.SQL”
。
定义分片类时,应定义参数DEFAULTCONCURRENCY = 0
。
然后,可以定义ShardKey
索引。
创建分片表时,将自动生成抽象的分片键索引。分片键索引的目的是用作确定行所在的分片的键。
分片类方法
分片类(Sharded = 1
)支持%Library.Persistent
方法%Open(),%OpenId(),%Save(),%Delete()和%DeleteId()
具有以下限制:并发concurrency参数
被忽略;删除将始终使用并发concurrency= 0
,而不管用户提供的并发值如何。完全支持回调方法%OnDelete(),%OnAfterDelete(),%OnOpen(),%OnBeforeSave()和%OnAfterSave()
。这些回调方法在分片主机上执行,而不是在分片服务器上执行。分片本地类(Sharded = 2
)不支持这些方法。
分片类(Sharded = 1
)不支持%Library.Persistent
方法%LockExtent()
和%UnlockExtent()
。定义并发参数的对象方法中的所有并发参数都要求值concurrency = 0
;否则,值为0。可以通过设置DEFAULTCONCURRENCY = 0
来建立默认值
分片类限制
- 分片类不支持的类参数:
CONNECTION
,DEFAULTGLOBAL
,DSINTERVAL
,DSTIME
,IDENTIFIEDBY
,OBJJOURNAL
。 - 分片类不支持的类关键字:
language
,ViewQuery
。 - 分片类不支持的超级类:
%Library.IndexBuilder
,%DocDB.Document
。 - 分片类不支持的属性数据类型:
%Library.Text
。 - 分片类不支持关系属性。
- 分片类不支持投影。
- 分片类不支持功能索引(无法定义索引
TypeClass
)。 - 分片类不支持使用除
“对象”
以外的语言的任何方法。 - 分片类不支持任何非
%SQLQuery
类型的类查询。
尝试使用任何这些功能来编译分片类都会导致编译时错误。