您应该始终尝试存储数据,并以尽可能好的方式将其与其他数据关联起来,因为这将减少在编码上花费的时间。它还将确保您正在构建的解决方案更加稳定,性能更好。
本章将向您展示如何创建可以存储数据的表,如何创建表之间的关系,以及如何通过创建唯一索引和删除操作来增强引用完整性。
您将在本章中了解以下主题:
• 表
• 扩展数据类型
• 关系
扩展数据类型
为了了解数据是如何存储在Dynamics AX 2012中的,您需要了解扩展的数据类型。
正如您在上一章中所读到的,在AX中编程时可以使用某些原始数据类型。在AX中编程时,可以扩展原始数据类型,并提供更多信息以帮助您。可以扩展原始数据类型以创建数量或金额字段,并且每个扩展版本都可以有自己的一组属性,例如,金额字段可能允许小数点后四位,但数量可能限制在小数点后零位。如果不使用扩展数据类型表示数量,则必须在使用该字段的所有表上设置标签、帮助文本、小数位数和其他相关信息。
扩展数据类型可以扩展任意次数,例如,类型数量可以从Real创建,数量可以进一步扩展以创建PurchQuantity和SalesQuantity。
创建扩展数据类型
要创建扩展数据类型(EDT),请打开AOT并展开节点“Data
Dictionary | Extended Data Types”。然后右键单击“Extended Data Types”并选择“新建”。然后,您将获得一个子菜单,如以下屏幕截图所示:
以下是不同类型的数据类型,您可以将其中一种作为扩展数据类型的基础:
数据类型 | 描述 |
---|---|
String | 字符串可以包含各种字符,长度由EDT的属性决定。 |
Integer | 整数可以包含-2147483647到2147483647之间的整数值。 |
Date | 日期可以保存日期值,该值将以3/23/2009的形式存储。 |
Time | 时间值可以保存一天中从上午12:00:00到晚上11:59:59的时间。 但是,存储在数据库中的值是一个整数,指定午夜后的秒数,范围从0到86400。 |
UtcDateTime | 此数据类型将日期和时间类型组合为一个数据类型。此外,它还保存有关时区的信息。其值范围为1900-01-01T00:00:00至2154-12-31T23:59:59。 |
Enum | 此数据类型应始终通过枚举属性Type链接到基枚举。 |
Container | 此数据类型包含具有不同数据类型的值列表。 |
GUID | 此数据类型是全局唯一标识符——一个由16个字节组成的数字。它以十六进制数字序列的形式写入文本,例如{5EFB37A3-FEB5-467A-BE2DDCE2479F5211}。在AX中使用它的一个示例是SysCompanyGUIDUsers表中的WebGUID字段。 |
Int64 | 此数据类型是一个64位整数,可以包含-9223372036854775808到9223372036864775808之间的整数值。 |
在这个例子中,我们将使用一个字符串。要打开属性窗口,右键单击字符串并选择属性。
属性窗口将显示如下:
正如您所看到的,有些属性具有黄色背景。这意味着最好填写这些属性。
还要注意,如果更改任何具有默认值的特性,则输入的值将为粗体。这样做只是为了使更改更加可见。如果您看到具有红色背景的属性,则它们是必需的。
通过按“类别”选项卡,您还可以在所有属性窗口中看到属性的分类列表。类别将根据活动的元素类型而变化。以下屏幕截图显示了我们创建的扩展数据类型的类别:
您首先要更改的是EDT的名称。在我们的例子中,我们将称之为CarId。您还应该为EDT提供一个标签,供用户在整个解决方案中看到。因此,通过单击“标签”字段右侧的正方形打开标签编辑器。创建一个带有文本CarId的标签。在“帮助文本”字段中执行相同操作,并为CarTable中的记录创建一个带有文本“唯一标识符”的标签。现在最不想检查的是字段的长度。默认情况下,字符串设置为10个字符,这对我们的示例来说很好。
有时你也会想要检查更多的属性,如果你正在创建一个整数类型的EDT,它显然有一组不同于字符串的属性,但这些只是基本的。
以下是各种扩展数据类型最常用的一些属性的列表。这里并没有解释所有的属性,因为这超出了本书的范围,所以如果你想了解更多关于本书中没有解释的属性,请查看开发人员的帮助文件。
属性 | 描述 |
---|---|
ID | ID由AX核心根据每个对象层的编号顺序设置,开发者不能更改。 |
Name | Name是在编写引用此扩展数据类型的代码时使用的系统名称。 |
Label | 标签是用户将在表单和报表中看到的字段的名称。 |
HelpText | 当字段在窗体中处于活动状态时,帮助文本显示在窗口的左下角。 |
FormHelp | 当用户对此字段执行查找操作时,FormHelp可用于指定专门的查找表单。 |
DisplayLength | DisplayLength是窗体或报表中显示的最大字符数。 |
Extends | Extends用于继承另一个扩展数据类型。 |
Tables(表)
AOT中Data Dictionary下的Tables节点对应于数据库中的表。在AOT中创建表时,它会自动与SQL服务器同步。同步过程在后台运行脚本,以确保创建/更新SQL表以反映AOT中的表。
AX中的表应始终从AOT创建,而不是通过运行SQL脚本或使用SQL Server Management Studio创建新表,因为AX还会在表中创建其他系统字段,并在系统表中保留对表及其字段的引用。
创建表
要创建新表,请打开AOT,展开“Data Dictionary”节点,然后展开“Tables”节点。右键单击“Tables”节点,然后选择“新建Table”,如以下屏幕截图所示:
现在已经创建了一个表,您要做的第一件事就是给表一个描述性的名称。让我们称我们的Table为CarTable。
要更改表的名称,请在表上单击鼠标右键,然后选择“属性”。然后将Name特性更改为CarTable。
现在,您将在AOT中的节点名称左侧看到一条红线:
此红线表示元素已更改但未保存。在AOT中选择CarTable,然后按Ctrl+S保存元素。请注意,红线消失了。
在开始添加字段、索引、关系等之前,让我们先看看表的属性:
此处的重要属性如下:
属性名 | 描述 |
---|---|
ID | ID由AX核心根据每个对象层的编号顺序设置,开发者不能更改。 |
Name | Name是在编写引用此扩展数据类型的代码时使用的系统名称。 |
Label | Label是用户将在窗体和报表中看到的表的名称。 |
FormRef | 这是一个链接,指向显示当“转到主表”对当前表进行引用时要执行的菜单项 AX中的功能。它也用于具有报表中表中的主索引字段以链接到表单的报表。 |
ReportRef | 这是创建主表报告时要执行的输出菜单项的链接。通常,当单击表单中的打印按钮时,活动数据源引用的表将具有指向ReportRef中报告的链接。如果没有,则使用自动报告字段组中的字段创建默认报告。 |
TitleField | TitleField1和TitleField2在主窗体的标题栏中用于提供有关所选记录的简要信息。 |
TableType | 可以定义的表类型有: •Regular:这些是存储在SQL server上的标准表,可以在AX客户端上显示/编辑。 •In Memory:这些表不会在SQL服务器上创建。插入临时表中的数据将仅存在于插入该数据的层和编程范围中的内存中。 •TempDB:这些是SQL临时表(也就是说,它们是在SQL服务器上创建的),可以作为常规表进行查询。但是,TempDB表不能直接显示在AX客户端上。 |
ConfigurationKey | 这是表连接到的配置键。如果配置键关闭,则表及其所有数据将从数据库中删除。 |
SecurityKey | 此属性不可编辑。它用于协助从AX 2009升级。 |
TableGroup | AX中的表分组是通过选择其中一个表组来完成的,即Miscellaneous、Parameter、Group、Main、Transaction、Worksheet Header和Worksheet Line。有关TableGroup选项的更多详细信息,请参阅以下链接 Table Groups | Microsoft Learn |
PrimaryIndex | 当您为一个表设置了多个索引时,应该指定哪一个是主索引。这样做是为了优化针对该表的数据提取。 |
ClusteredIndex | 这指定了需要聚集的索引。对于TableGroup属性设置为Group或Main的表,应始终设置聚集索引。 |
SupportInheritance | 它用于指定表是否支持AX 2012中的继承功能。默认情况下,此属性设置为“No”。 当更改为“Yes”时,将启用“Abstract”和“Extends”属性,并且可以对其进行更改。 |
Abstract | 如果值为“Yes”,则该表不能成为X++SQL语句(如select或update)的直接目标。 |
Extends | 表是从此属性中选择的另一个表派生而来的。有关更多信息,请参阅表继承部分。 |
ModifiedDateTime, CreatedDateTime, ModifiedTransactionId, and CreatedTransactionId |
这些属性用于存储有关谁创建了记录、何时创建、上次修改者以及上次修改时间的信息。还可以附加事务ID来创建事务,并附加另一个事务ID来修改事务。 |
还有其他可用的属性,您可以在开发人员的帮助部分找到更多关于它们的信息。这里我们只描述了最常用的属性。
向表中添加字段
没有任何字段的表没有任何用处,所以让我们看看如何将字段添加到新创建的表中。
要向表中添加字段,可以打开两个AOT窗口,并将扩展数据类型拖动到表的“字段”节点中。也可以通过右键单击表中的“字段”节点并选择“新建”来添加新字段。在“新建”下,您将获得一个子菜单,您可以在其中选择要创建的字段类型。
- 首先,我们创建CarId字段。在下面的示例中,我们将使用名为CarId的扩展数据类型,它是我们在本章的前一节中创建的。
要将其添加到表中,只需将其拖放到CarTable的“字段”节点中即可。
我们的结果应该如下:
- 接下来,我们创建ModelYear字段。我们将直接创建一个新的Integer字段。只需右键单击表下的“字段”节点,然后选择“新建|Integer”:
- 现在,我们为ModelYear字段设置属性。在新字段上单击鼠标右键,然后选择属性。将字段的名称更改为ModelYear。此外,将属性“扩展数据类型”设置为Yr,这是年的标准AX数据类型。标签和表单帮助现在将从扩展数据类型Yr继承,因此我们不必在此处填写任何内容(除非我们对标准标签和表单的帮助不满意)。
- 创建其余字段。我们还将在CarTable中添加另外两个字段,以便该表包含以下字段:
这里,PK表示字段应该是主键,UIdx表示字段包含在唯一索引中。您将在本章后面学习如何创建主键和唯一索引。 - 创建RentalTable表。我们还将创建另一个名为RentalTable的表,该表将具有以下字段:
将字段添加到字段组
在所有标准AX表单中,字段都被分组为彼此相关的字段集合,最佳做法是所有字段都存在于至少一个字段组中。
要将字段添加到字段组,只需从“字段”节点拖动字段,然后将其放入要添加字段的“字段组”节点即可。
AX中的所有表都有五个系统创建的字段组,称为AutoLookup、AutoReport、AutoIdentification、AutoSummary和AutoBrowse,如以下屏幕截图所示:
当用户希望在外键字段中选择值时,将使用“AutoLookup”字段组。我们将在下一章中对此进行更多研究。
“AutoReport”字段组用于基于AX中的标准报告模板自动创建报告,该模板包含“AutoReport"字段组中的字段。当用户单击打印图标,或在表格用作主数据源的窗体中按Ctrl+P时,会生成报告。
默认情况下,创建新表时,表上的主键设置为代理项(RecId)。RecId是系统生成的数字(Int64),每个表、每个公司、每个分区都是唯一的。代理键可以用作其他表上的外键,也可以显示在表单上。组AutoIdentification是AX使用的一种机制,用于在表单上存在代理项时,将代理项字段替换为此组中的字段。
AutoSummary用于设置快速选项卡上的默认摘要字段。摘要字段是快速选项卡折叠时显示的关键字段。
创建索引
AX中的索引用于保持唯一性和加快表搜索速度。Dynamics AX 2012自动创建一个称为代理键(RecId)的主索引。此字段在索引部分中不可见,但可以在表属性中看到。
当AX中的select语句执行时,它会被发送到数据库查询优化器,该优化器会分析该语句,并在数据库中执行该语句并将记录返回到AX之前决定哪个索引最适合用于该语句。
要找出表应该具有哪些其他索引以及由哪些字段组成,您需要查看使用该表的select语句以及它们如何选择数据。经常在范围、联接表以及分组或排序中使用的字段是索引的候选字段。但是,您应该限制表中的索引数量,因为每当从表中插入、更新或删除记录时,都必须更新每个索引。这可能会成为性能瓶颈,尤其是对于事务表。
右键单击AOT中表下的索引节点并选择“新建索引”,可以为表创建索引。然后将创建一个名为Index1的新索引。右键单击索引,选择“属性”,然后更改索引的名称。命名索引的最佳实践是使用索引中以Idx后缀的字段的名称,除非最后一个字段以Id结尾。在这种情况下,我们只在末尾添加一个x,因此在我们的示例中,我们创建了一个名为CarIdx的索引。
接下来要做的事情是向索引中添加一个字段。这是通过从表中的“字段”节点拖动字段并将其放到新创建的索引中来完成的。
由于CarId是表中的主键,因此它也是唯一索引的明显候选者。要使索引唯一,只需将索引上的属性AllowDuplicates设置为No。
CarTable现在应显示如下:
现在,您可以尝试自己在RentalTable上添加一个唯一的索引。它应该只包含RentalId字段。
属性AlternateKey表示其他表可以创建引用该键的外键关系,以替代引用主键。
一个表可以有几个备用键。任何一个替代密钥都可以切换为主键(前提是替代密钥仅包括一个字段)。
备用钥匙也可以设置为替换钥匙。这将自动将AlternateKey索引中存在的字段添加到“AutoIdentification”字段组中。
创建关系
在两个表之间创建关系的方法之一是直接在外键表上创建关系。
通过查看AOT中的表SalesTable并展开“Relations”节点,您可以看到表关系的外观示例。打开名为Agreement的关系,查看该关系中使用的字段。这些字段构成了关系所指向的表中的主键。查看关系中被称为表的属性,查看关系指向的表(选择关系,右键单击以打开“属性”窗口)。然后在AOT中找到该表,并注意到相同的字段构成了该表中的唯一索引。
构成关系的每个字段都可以作为两种不同的类型放入关系中。到目前为止,我们只看到了被称为正常的类型。另一种类型称为条件关系。这些关系与正常关系一起使用,以在相关数据上添加条件。为了更好地理解这是如何工作的,这里有一个例子。
查看SalesTable上的关系CustMarkupGroup:
关系中的一个字段是MarkupGroup,它保存对与其相关的表中的ID的引用。在这种情况下,是MarkupGroup表中的GroupId。
另一个字段是module,它是一个相关的固定枚举。这种关系类型(FixedField)意味着条件值必须存在于相关表中。在本例中,每个关系的值为1。在这种情况下使用的基本枚举是MarkupModuleType,其值1表示枚举客户:
您还可以创建用于表单之间导航的关系。这类关系称为航行关系。这些关系用于对完整性没有限制的表,例如外键和主键之间的关系。
这些关系用于对完整性没有限制的表,例如外键和主键之间的关系。
AX 2012中的基数和关系类型属性不控制关系的行为。相反,这些属性可作为开发人员的参考。
创建删除操作
为了增强引用完整性,为所有具有指向当前表的外键的表添加删除操作是至关重要的。
这将确保我们删除对任何被删除记录的任何引用,这样我们就不会留下指向其他不再存在的记录的记录。
为了说明这一点,假设您有一个名为RentalTable的表和另一个称为CarTable的表。RentalTable中的一条记录将始终具有指向CarTable中一条(并且只有一条)记录的链接,并且CarTable中的记录可以存在于RentalTable的多条记录中
此场景在这里的简单数据模型中可视化:
然后我们在RentalTable和CarTable之间有一个一对多的关系。因此,重要的是,如果CarTable中的记录即将被删除,则执行两个操作中的任何一个。RentalTable中所有链接到CarTable中记录的记录也被删除,或者用户不允许删除CarTable中的记录,因为它在RentalTable有引用记录。
执行以下步骤在CarTable中创建删除操作:
1.打开CarTable下的“DeleteActions”。
2.右键单击并导航到“新建Delete Action”。
3.打开“属性”窗口。
4.更改它应该指向的表,在我们的例子中,是RentalTable。
5. 根据以下列表中的选项,决定删除CarTable中的记录时应执行的操作:
Delete action | 描述 |
---|---|
None | 删除CarTable中的记录时,不会对RentalTable中的相关记录执行任何操作。 |
Cascade | RentalTable中与CarTable中正在删除的记录相关的所有记录也将被删除。 |
Restricted | 用户将收到一条警告,说明由于RentalTable中存在交易,因此无法删除CarTable中的记录。如果RentalTable中存在一个或多个相关记录,则用户将无法删除CarTable中的记录。 |
Cascade+Restricted | 当通过两个以上级别删除记录时,将使用此选项。假设存在另一个对CarTable有级联删除操作的表,而CarTable对RentalTable有级联+受限删除操作。如果顶层表中的记录即将被删除,它也会删除CarTable中的相关记录。反过来,RentalTable中与从CarTable中删除的记录相关的所有记录也将被删除。如果CarTable中只有一条记录将被删除,则用户将收到与使用Restricted操作时相同的消息。 |
表浏览器
如果要查看表中所有字段的所有数据,可以打开表浏览器。要在AX 2012中启动表格浏览器,只需在AOT中选择表格,然后按Ctrl+O或打开按钮。
您还会注意到,它列出了您尚未添加的某些字段。这些是系统字段。
字段名称 | 描述 | 可选 |
---|---|---|
Partition | 此字段定义与记录关联的分区。 | No |
dataAreaId | 此字段将存储创建记录的公司帐户。 | No |
recVersion | 第一次创建记录时,其记录版本将为1。对记录所做的任何更改都将导致recVersion获得一个随机的新整数值。该字段用于确保两个进程不能覆盖同一记录。 | No |
RecId | RecId是AX中每个表的系统集唯一标识符。在旧版本中,RecId是每个公司帐户唯一的,但从4.0版本起,它被更改为每个表唯一。 | No |
ModifiedDateTime, CreatedDateTime, ModifiedTransactionId, and CreatedTransactionId |
如“创建表”部分所述,可以将这些系统字段添加到表中,以查看记录是何时创建/修改的,以及是谁创建/修改了该记录。还可以为创建和修改添加事务ID。只有对记录的最后一次更改才会由修改后的字段存储。 | Yes |
表继承
从ER结构的角度来看,表继承(基派生表)的概念从最早的版本开始就在Dynamics AX中使用。然而,在AX 2012中,表继承已被嵌入到核心框架中,因此,它在整个应用程序(建模、运行时和执行)中都得到支持。
派生表可以从另一个表(基)扩展或派生。每个表都有SupportHeritage属性和Extends属性,它们一起控制表继承。
术语在这里很重要。Microsoft更喜欢在继承关系中使用派生表和基表,而不是父表或子表,因为父表和子表可以用来描述表之间的外键关系。
考虑以下场景。作为一名开发人员,你必须设计和构建一个数据结构,可以保存多个CarType实例的数据,例如SUV、Coupe等。尽管所有类型都有许多共同的特性(如发动机尺寸和平均每加仑英里数),但汽车可以根据类型具有独特的特性。例如:
•SUV:
° Bicycle storage(是/否)
° Dog friendly(是/否)
•Coupe:
°Golf bag storage (是/否)
可以通过以下方式使用表继承在Dynamics AX中构建前面的结构:
1. 创建一个名为CarType的新枚举,并添加SUV、Coupe和Sedan作为元素,如下所示:
2.创建一个名为CarTypeBase的表,并将SupportHeritage属性更改为Yes。
3. 创建一个名为InstanceRelationType的新Int64字段,并将该字段的ExtendedDataType属性设置为RelationType
InstanceRelationType字段用于保存派生表的RecId。这样,当用户查询派生表时,AX将自动在InstanceRelationType字段上向基表添加联接。
如果我们不手动添加此字段,则AX将自动创建一个名为RelationType的字段,并将其用于前面描述的目的。但是,该字段在AOT中不可用(但它将出现在SQL server上)。
4. 导航回表属性,更改InstanceRelationType属性,并将Abstract属性设置为Yes。
通过设置Abstract属性,我们不允许用户/开发人员直接查询基表,因为我们希望他们查询派生表(派生表将包含基表中的字段)
5.将枚举CarType与两个Real类型字段EngineSize和AvgMPG一起添加到此表中。
6.创建一个名为CarTypeSUV的新表,将SupportHeritage属性设置为Yes,并将Extends设置为CarTypeBase。
7.添加两个名为BicycleStorage和PetFriendly的枚举字段(NoYes):
查询继承表的数据
与常规表一样,继承层次结构中的所有表都是同步的,因此,物理表是在数据库上创建的。Dynamics AX会自动创建基表和派生表之间的关系,并在查询派生表时自动应用联接条件。例如,考虑在X++中编写以下代码:
Select * from CarTypeSUV
AX将自动向CarTypeBase添加一个联接,因此,AX将从两个表中检索字段。
由于Dynamics AX允许从其他派生表派生表,因此在未指定需要提取的字段列表的情况下调用数据库可能会导致性能问题。因此,建议在查询表时(尤其是当表支持继承时),必须指定要提取的字段列表,例如,当执行以下语句时,系统将只从数据库中提取CarTypeSUV表:
Select PetFriendly from CarTypeSUV
有效的时间状态表
日期生效表(也称为有效时间状态表)允许Dynamics AX将有效的开始日期和结束日期(以及时间)与应用程序工件相关联,例如,购买定价协议可以在一系列日期之间有效。
Dynamics AX使用有效的时间状态表框架来简化必须在不同时间点跟踪更改的数据的维护。例如,销售价格协议的折扣率第一年可以是5%,第二年可以是6%。在第二年,人们可能仍然想知道前一年的失业率是5%。
日期有效性特征是一个中心框架。它使开发人员能够编写更少的代码,并创建管理当前、过去和未来记录的查看和编辑的表单。
在AOT中的表上,可以设置ValidTimeStateFieldType属性使其成为有效的时间状态表。这会导致系统自动添加ValidFrom和ValidTo列,它们跟踪每行中的日期(和时间)范围。
系统通过自动防止日期范围之间的重叠,确保这些日期或日期时间字段中的值保持有效。
执行以下步骤以创建一个日期生效表,该表可用于记录给定汽车在某个日期范围内的里程数:
1.创建一个名为CarMileageTable的新表。
2.在表中添加一个名为“Mileage”的Real类型字段。
3.添加与CarTable的关系(基于主键carId),如下图所示:
4.将表的ValidTimeStateFieldType属性更改为Date。
5.系统现在将自动向表中添加两个日期字段(ValidFrom和ValidTo)。
如果属性更改为UtcDateTime,那么框架将添加两个UtcDateTime字段(这些字段可用于定义包括时间的日期范围)。
6.创建一个新索引,其中字段为CarTable、ValidFrom和ValidTo。属性如下:
° “Allow Duplicates ”设置为“No”。
° AlternateKey设置为Yes。
° ValidTimeStateKey设置为Yes。这将启用ValidTimeStateMode字段。
° ValidTimeStateMode设置为Gap。使用此字段可以允许和禁止在输入的日期范围内出现间隔。如果不允许有间隙,并且为同一个密钥字段(本例中为car)创建了多个记录,则系统将确保第一个记录的ValidTo和第二个记录的ValidatFrom之间没有间隙。
在X++中选择数据时,可以使用关键字validTimeState指定日期,以便返回的数据对这些日期有效。可以指定一个或两个日期。如果指定了一个,则返回在该日期有效的所有记录。
如果指定了两个,则返回日期范围内的所有有效记录,例如:
select validTimeState(asAtDate) from CarMileageTable;
select validTimeState(fromDate, toDate) from CarMileageTable;
需要注意的要点如下:
• 不能为从其他表继承的任何表设置ValidTimeStateFieldType属性
• ValidFrom和ValidTo列可以都是日期数据类型,也可以都是utcDateTime数据类型
• Query类还具有按日期范围提供筛选的方法,例如validTimeStateAsOfDateTimeRange
总结
在本章中,您已经阅读了如何在AX中创建表,如何设置扩展数据类型,以及如何基于扩展数据类型在表中创建字段。
还介绍了如何通过添加删除操作来强制引用完整性,如何创建索引以确保数据的唯一性,以及如何在表之间创建关系以链接相关信息。
现在,您应该准备好在AX中为表创建数据存储,并按照最佳做法进行设置。文章来源:https://www.toymoban.com/news/detail-765931.html
在下一章中,您将学习如何创建表单,使用户可以查看表中的数据,以及输入和更新用于显示数据的数据和导航页。您还将学习如何创建用作窗体中按钮和菜单中项目的菜单项,以及如何创建新菜单。文章来源地址https://www.toymoban.com/news/detail-765931.html
到了这里,关于学习MS Dynamics AX 2012编程开发 3. 存储数据的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!