PowerLanguages.E: An EntityData

来源:互联网 发布:wow 7.0.3数据库 编辑:程序博客网 时间:2024/06/05 15:01

What is PowerLanguages.E?

PowerLanguages.E是一门衍生自C#的实体数据特定的元编程语言,它实现了Entity Data Model和Entity SQL的语义,并让用户在抽象的Entity Data Metaprogramming Model上进行编程,隔离了实现细节,提高了生产力。它是一门强大清晰易用的元编程语言。

Why PowerLanguages.E?

Entity Data Model是Entity Relationship Model的进化,它以面向对象的视野来描述实体数据。Entity SQL是SQL的进化,它以面向对象的视野来查询数据。EDM包含三个规范:CSDL,SSDL和MSL。需要指出的是,语法与语意是两个不同层面的东西,EDM的语法是XML,Entity SQL的语法是SQL的方言。PowerLanguages.E实现了EDM和Entity SQL的语意,但自创了用户友好的语法。

模型只是个模型,浮在空中的精美模型,除非把模型落地成实现代码,它没任何用处。微软提供了两种实现方式:"Classic EDMX"和"Moden Code First”。经典方式使用designer创建维护edmx文件,designer虽然方便,但功能有限,手工创建维护edmx虽然终极强大,但XML非常臃肿繁琐,另一个问题是,虽然开发环境会自动根据模型生成合适的实现代码,但模型和实现代码是割裂的。Code First走上完全相反的道路,代码不仅是实现,也描述了模型。我不喜欢Code First,一个原因是PowerLanguages.E是Code First的竞争者:),其它原因是,首先,Code First把Conceptual Model,Store Model和Mapping Specification杂糅在一起,让思维混乱不堪,我认为三者独立开来虽然繁琐点,但清晰的层次有利于大中型项目的理解开发维护;其次,Code First功能有限,比如不支持Multiple Entity Sets per Type (MEST)。因为C#/VB是general-purpose programming languages,用它们来描述领域特定的模型是在强人所难,你可以看到Code First中对Expression Tree的滥用,Expression Tree已不是设计的本意,描述LINQ,而被“奇技淫巧”般的用来描述EDM。另一个问题是,经典方式与摩登方式都用LINQ to Entities来查询数据,同样,LINQ被设计成general-purpose的query构造,对于资深玩家,会隐约觉得LINQ to Entities没有Entity SQL来得自然爽快,且功能有限。虽然在经典方式中可以嵌入Entity SQL,但是用起来不方便,且被当作纯文本,无法对它们进行编译时的检查,一切错误都将在运行时抛出。

PowerLanguages.E尝试给出“终极”的解决方案。首先,它(几乎)完整的实现了EDM和Entity SQL的语意,抛弃了XML的臃肿语法,自创了用户友好的语法;其次,如同Code First,模型与C#是无缝集成的;最后,编译器会在编译时刻报错。

Using PowerLanguages.E

你需要Visual Studio 2012 Professional或更强大的版本。打开Tools/Extensions and Updates对话框,在Online Visulal Studio Gallery搜索PowerLanguages并安装:

打开New Project对话框,在Visual C# templates下可以看到PowerLanguages.E:

PowerLanguages.E project是对C# project的扩展,加入了meta compilation和code generation这两个步骤。建议把数据访问组件做成单独的class library,所以选择Class Library template即可。也可以对任何现有的C# project进行扩展,让其支持PowerLanguaes.E,只需进行下面四步:

一、Unload project

二、Edit .csproj

三、在.csproj文件最后添加<Import Project="…" />

  ...
  <Import Project="$([System.IO.Directory]::GetFiles($([System.IO.Path]::Combine($([System.Environment]::GetFolderPath(SpecialFolder.LocalApplicationData)), `Microsoft\VisualStudio\11.0\Extensions`)), `PowerLanguagesE.targets`, System.IO.SearchOption.AllDirectories))" />
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>
  <Target Name="AfterBuild">
  </Target> -->
</Project>

四、Reload project, done

注:PowerLanguages.E的编译器依靠Microsoft Roslyn,当前Roslyn对C#的支持不够完善,主要是不支持C# 4的dynamic和C# 5的async。

一个完整的PowerLanguages.E project需要三种构造:

名称 文件扩展名 描述了 E .ple Conceptual Model & Entity Data Metaprogramming Model EStore .ples Store Model EMapping .plem Mapping Specification

可以简单粗暴的把这三者和ORM的三元素进行类比。

使用Add New Item对话框添加它们:

已知的bug:当修改.ple, .ples, .plem的文件名或将其移动到其它文件夹后,元编译时不会在编辑器上显示错误信息,你需要关闭Visual Studio然后重新打开。

PowerLanguages.E的"Console Application with Example" project template包含一个示例代码EBusiness,它通过耳熟能详的Customer-Order-OrderDetail-Product-Supplier来演示语言特性,新建一个该项目以获得对PowerLanguages.E的感性认识:

待完善:当前代码着色做得很粗燥,IntelliSense,debug支持等根本没实现。

打开Example.ple, Example.ples和Example.plem闻闻味道。

Build Solution(F6)后,在Solution Explorer上点Show All Files,可以看到一些编译器生成的文件,如上图。

文件名 解释 __E.meta.cs Entity Data Metaprogramming Model支撑库 __EntityFramework.impl.cs Entity Framework实现支撑库 <ENamespaceName>.csdl 由.ple生成的CSDL <StoreNamespaceName>.ssdl 由.ples生成的SSDL <ContextName>_to_<StoreNamespaceName>.msl 由.plem生成的MSL <FileName>.ple.meta.cs Entity Data Metaprogramming Model <FileName>.ple.impl.cs Entity Data Metaprogramming Model的Entity Framework的实现 Globals__.meta.cs Entity Data Metaprogramming Model的全局信息 Globals__.impl.cs Entity Data Metaprogramming Model的全局信息的Entity Framework的实现

Meta Compilation, Code Generation and Normal Compilation

PowerLanguages.E是门元编程语言,相应就存在元编译和代码生成的过程:

元编译将.ple编译成.csdl和.ple.meta.cs;.ples编译成.ssdl;.plem编译成.msl。代码生成将metaprogramming model(.meta.cs)变成具体的实现(.impl.cs)。最后进行常规编译,.cs文件编译成MSIL,.csdl, .ssdl, .msl作为资源嵌入到程序集中。

Entity Data Metaprogramming Model

这是我杜撰的fancy术语。它是对Entity Data Model进行元编程思维后的必然产物,这也是我对C#元编程的探索。对一问题域(比如Entity Data Programming)定义一个编程模型,让用户只在编程模型这一抽象上进行思考编程,而不是面对具体的实现。打开Example.ple.meta.cs,可以看到,它通过C#语言的各种构造定义了一个抽象的Entity Data Metaprogramming Model,完全隔离了Entity Framework的具体实现。

肉身成道(Programming the model, not the implementation)

对Metaprogramming Model进行编程的好处是,一份代码可以得到不同的实现。

当前只支持生成EntityFramework的实现。生成WCF Data Services的实现理论上可行,但需要大量的努力。

我们已经(至少在精神上已经)从EntityFramework这一具体的“肉身”升华到Entity Data Meta Programming Model 这一普世的“真理”。是不是和LINQ to xx有似曾相识的感觉?

道成肉身

回到EBusiness的示例上来,下面让它实际运行起来。

新建一个Service-based Database名叫Example.mdf:

在下一个向导页面点"Cancel",设置Example.mdf的"Copy to Output Directory"属性为Do not copy。

Example.sql是根据store model由程序生成的,调用System.Data.Objects.ObjectContext.CreateDatabaseScript()就可得到,但此脚本并不完全,应此存在手写的ExampleX.sql。在Example.mdf上先后运行这两个脚本以创建数据库的表及存储过程等。以后版本可能会创建这样的工具,可以在store model(.ples)和数据库之间相互生成。

打开App.config,修改connectionString中的:

...attachdbfilename=D:\Full\Path\To\Example.mdf;...

为Example.mdf的全路径。

打开Program.cs,在Main()中设置必要的断点,F5运行。

可以通过IntelliTrace查看Entity Freamework生成的SQL,如下图:

当然也可通过SQL Server Profiler查看。还有一种(我喜欢的)方法:

添加Entity Framework Tracing Provider NuGet package:

去掉implEntityFramework.cs的第一行注释。F5运行程序,在Output窗口就可以看到EF生成的SQL:

不参与元编译

细心的读者可能注意到,项目中所有的.cs文件都会参与元编译,但implEntityFramework.cs中包含的是EntityFramework的实现代码,元编译肯定会报错。有两种方法让.cs文件不参与元编译:

一、文件名以"impl"开头

二、文件直接或间接存在于以"impl"开头的文件夹中

Impl prefix可以更改,打开.csproj文件,添加或修改<PowerLanguagesEImplPrefix>为你想要的名字:

  ...
  <PropertyGroup>
    <PowerLanguagesEImplPrefix>impl</PowerLanguagesEImplPrefix>
    <PowerLanguagesEAllInOne>true</PowerLanguagesEAllInOne>
  </PropertyGroup>
  <Import Project="$([System.IO.Directory]::GetFiles($([System.IO.Path]::Combine($([System.Environment]::GetFolderPath(SpecialFolder.LocalApplicationData)), `Microsoft\VisualStudio\11.0\Extensions`)), `PowerLanguagesE.targets`, System.IO.SearchOption.AllDirectories))" />
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>
  <Target Name="AfterBuild">
  </Target>
  -->
</Project>

Impl prefix不区分大小写。

PowerLanguages.E.Runtime.dll?(源代码包含与组件引用)

默认是源代码包含,对于每个PowerLanguages.E project,每次元编译时,__E.meta.cs和__EntityFramework.impl.cs都将被拷贝到project directory中,并参与Normal Compilation成为最终程序集的一部分,所以不要修改它们。

可以改成对运行库进行引用。运行库是C:\Users\<UserName>\AppData\Local\Microsoft\VisualStudio\11.0\Extensions\<RandomName>\PowerLanguages.E.dll。也可以自建一个运行库,只需将__E.meta.cs和__EntityFramework.impl.cs添加到任意C# library project中,然后添加对System.Data.Entity, System.ComponentModel.DataAnnotations和System.Runtime.Serialization程序集的引用,该project即为运行库。接着修改欲引用运行库的project的.csproj文件,添加或修改<PowerLanguagesEAllInOne>为false:

  ...
  <PropertyGroup>
    <PowerLanguagesEImplPrefix>impl</PowerLanguagesEImplPrefix>
    <PowerLanguagesEAllInOne>false</PowerLanguagesEAllInOne>
  </PropertyGroup>
  <Import Project="$([System.IO.Directory]::GetFiles($([System.IO.Path]::Combine($([System.Environment]::GetFolderPath(SpecialFolder.LocalApplicationData)), `Microsoft\VisualStudio\11.0\Extensions`)), `PowerLanguagesE.targets`, System.IO.SearchOption.AllDirectories))" />
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>
  <Target Name="AfterBuild">
  </Target>
  -->
</Project>

在设置 PowerLanguagesEAllInOne为false后,可以原地修改__E.meta.cs和__EntityFramework.impl.cs,不用担心重新编译时被覆盖。不建议修改它们,因为如果PowerLanguages.E出新版本时,极有可能会更新它们,你需要把官方版本与自己的修改版本进行合并,加重你的维护负担。如果你发现任何bug,或有任何建议,请告诉作者。

E Guide

语法。注:为了追求简明,语法存在显而易见的细微错误,所有的trailing separator都是可选的,在有些地方是被禁止的,省略trailing separator总是正确的。

Namespace:
  'namespace' C#NamespaceName '[' 'enamespace' ':' NamespaceName ']' '{' C#ExternAliasDirective* NamespaceProlog* NamespaceMember* '}'
NamespaceProlog:
  C#UsingDirective | NamespaceImport | StoreImport | ProviderImport
NamespaceImport:
  '$import' NamespaceName ('as' Alias)? ';'
StoreImport:
  '$importstore' StoreNamespaceName 'as' Alias ';'
ProviderImport:
  '$importprovider' ProviderName ProviderVersion 'as' Alias ';'
NamespaceMember:
  C#NamespaceMember | EntityType | StructType | EnumType | Context | Function
//
//
QualifiableName:
  (Alias ':')? Name
QualifiedName:
  Alias ':' Name
BaseTypeName:
  QualifiedName | C#BaseTypeName
CSAttributesModifiers:
  C#Attribute* CSModifier*
CSModifier:
  'public' | 'protected' | 'internal' | 'private' | 'abstract' | 'virtual' | 'override' | 'sealed' | 'new' | 'partial'
CSGetterAttributesModifiers:
  'getter' ':' C#Attribute* CSModifier*
CSSetterAttributesModifiers:
  'setter' ':' C#Attribute* CSModifier*
//
//
EntityType:
  '$entity' EntityTypeName ('[' (EntityTypeAttribute ';')? ']')? (':' (BaseTypeName ',')+)? '{' EntityTypeMember* '}'
EntityTypeAttribute:
  CSAttributesModifiers
EntityTypeMember:
  C#ClassMember | Property | NavigationProperty
//
//
StructType:
  '$struct' StructTypeName ('[' (StructTypeAttribute ';')? ']')? (':' (C#BaseTypeName ',')+)? '{' StructTypeMember* '}'
StructTypeAttribute:
  CSAttributesModifiers
StructTypeMember:
  C#ClassMember | Property
//
//
Property:
  '$property' PropertyName ('[' (PropertyAttribute ';')? ']')? 'as' ScalarOrStructTypeQualifiableName ('[' (TypeFacet ';')* ']')? ';'
PropertyAttribute:
  CSAttributesModifiers | CSGetterAttributesModifiers | CSSetterAttributesModifiers | Key | ValueGenerationMode | ConcurrencyStamp
Key:
  'key'
ValueGenerationMode:
  'identity' | 'computed'
ConcurrencyStamp:
  'concurrencystamp'
//
//
TypeFacet:
  NotNull | DefaultValue | Length | LengthRange | ValueRange | Precision | Scale | Pattern | Enumerations | NonUnicode | Collation | Srid
NotNull:
  'notnull'
DefaultValue:
  'defaultvalue' ':' Literal
Length:
  'length' ':' C#IntegerLiteral
LengthRange:
  'lengthrange' ':' (C#IntegerLiteral '..' MaxLength? | '..' MaxLength)
MaxLength:
  C#IntegerLiteral | 'max'
ValueRange:
  'valuerange' ':' (LowerBound '..' UpperBound? | '..' UpperBound)
LowerBound:
  ('[' | '(') Literal
UpperBound:
  Literal (']' | ')')
Precision:
  'precision' ':' C#IntegerLiteral
Scale:
  'scale' ':' C#IntegerLiteral
Pattern:
  'pattern' ':' C#StringLiteral (',' 'ignorecase')?
Enumerations:
  'enumerations' ':' (Literal ',')+
NonUnicode:
  'nonunicode'
Collation:
  'collation' ':' Name
Srid:
  'srid' ':' (C#IntegerLiteral | 'variable')
//
//
Literal:
  PrimitiveTypeLiteral | EnumTypeLiteral
EnumTypeLiteral:
  EnumTypeMemberName
PrimitiveTypeLiteral:
  'true' | 'false'
  | C#StringLiteral
  | '-'? (C#IntegerLiteral | C#RealLiteral)
  | 'datetime' ("yyyy-MM-dd HH:mm" | "yyyy-MM-dd HH:mm:ss.FFFFFFF")
  | 'datetimeoffset' ("yyyy-MM-dd HH:mm zzz" | "yyyy-MM-dd HH:mm:ss.FFFFFFF zzz")
  | 'time' ("hh:mm" | "hh:mm:ss.FFFFFFF")
  | 'binary' "HexDigits"
  | 'guid' "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
  | 'geography' ("WellKnownText" | '(' "WellKnownText" ',' Srid ')')
  | 'geometry' ("WellKnownText" | '(' "WellKnownText" ',' Srid ')')
//
//
NavigationProperty:
  '$naviproperty' NavigationPropertyName ('[' (NavigationPropertyAttribute ';')* ']')? 'to' EntityTypeQualifiableName ('.' NavigationPropertyName)? ('*' | '?')? ';'
NavigationPropertyAttribute:
  CSAttributesModifiers | CSGetterAttributesModifiers | CSSetterAttributesModifiers | ForeignKeys | CascadeDelete | EndName
ForeignKeys:
  'foreignkeys' ':' (PropertyName ',')+
CascadeDelete:
  'cascadedelete'
EndName:
  'endname' ':' Name
//
//
EnumType:
  '$enum' identifier ('[' (EnumTypeAttribute ';')* ']')? (':' IntegerPrimitiveTypeQualifiableName)? '{' (EnumTypeMember ',')* '}'
EnumTypeAttribute:
  CSAttributesModifiers
EnumTypeMember:
  EnumTypeMemberName ('[' (EnumTypeMemberAttribute ';')* ']')? ('=' '-'? C#IntegerLiteral)?
EnumTypeMemberAttribute:
  CSAttributesModifiers
//
//
Context:
  '$context' ContextName ('[' (ContextAttribute ';')* ']')? (':' (BaseTypeName ',')+)? '{' ContextMember* '}'
ContextAttribute:
  CSAttributesModifiers
ContextMember:
  C#ClassMember | EntitySet | FunctionImport
//
//
EntitySet:
  '$entityset' EntitySetName ('[' (EntitySetAttribute ';')* ']')? 'of' EntityTypeQualifiableName ('associate' (EntitySetAssociation ',')+ )? ';'
EntitySetAttribute:
  CSAttributesModifiers | CSGetterAttributesModifiers
EntitySetAssociation:
  EntitySetName 'by' (EntityTypeQualifiableName '.')? NavigationPropertyName ('as' AssociationSetName)?
//
//
FunctionImport:
  '$functionimport' FunctionImportName ('[' (FunctionImportAttribute ';')* ']')? '(' (FunctionImportParameter ',')* ')' ('as' (FunctionImportResult ',')+ )? ';'
FunctionImportAttribute:
  CSAttributesModifiers | Composable
Composable:
  'composable'
FunctionImportParameter:
  ParameterName ('[' (FunctionImportParameterAttribute ';')* ']')? 'as' ScalarTypeQualifiableName
FunctionImportParameterAttribute:
  ParameterDirection
ParameterDirection:
  'in' | 'out' | 'inout'
FunctionImportResult:
  (GlobalTypeQualifiableName | '{' (FunctionImportResultMember ',')+ '}') '*' ('into' EntitySetName)?
FunctionImportResultMember:
  MemberName 'as' ScalarTypeQualifiableName
//
//
Function:
  '$function' FunctionNamer ('[' (FunctionAttribute ';')* ']')? '(' (FunctionParameter ',')* ')' ('as' LocalType)? '=>' Expression ';'
FunctionAttribute:
  Extensible
Extensible:
  'extensible'
FunctionParameter:
  ParameterName 'as' LocalType
//
//
LocalType:
  GlobalTypeQualifiableName | EntityReferenceType | CollectionType | AnonymousType
EntityReferenceType:
  EntityTypeQualifiableName '&'
CollectionType:
  LocalType '*'
AnonymousType:
  ('#' C#ClassName)? '{' (AnonymousTypeMember ',')+ '}'
AnonymousTypeMember:
  MemberName 'as' LocalType
//
//
C#PrimaryNoArrayCreationExpression:
  ... | ContextualLambdaQuery
ContextualLambdaQuery:
  C#PrimaryExpression '.' '$(' Expression ('[' MergeOption ']')? ')'
MergeOption:
  'appendonly' | 'overwritechanges' | 'preservechanges' | 'notracking'
//
//
Expression:
  SimpleExpression
  | QueryExpression
SimpleExpression:
  LogicalOrExpression
LogicalOrExpression:
  LogicalAndExpression
  | LogicalOrExpression '||' LogicalAndExpression
LogicalAndExpression:
  OverlapsExpression
  | LogicalAndExpression '&&' OverlapsExpression
OverlapsExpression:
  ExceptExpression
  | OverlapsExpression 'overlaps' ExceptExpression
ExceptExpression:
  UnionExpression
  | ExceptExpression 'except' UnionExpression
UnionExpression:
  IntersectExpression
  | UnionExpression ('union' | 'unionall') IntersectExpression
IntersectExpression:
  EqualityExpression
  | IntersectExpression 'intersect' EqualityExpression
EqualityExpression:
  RelationalExpression
  | EqualityExpression ('==' | '!=') RelationalExpression
RelationalExpression:
  AdditiveExpression
  | RelationalExpression ('<' | '<=' | '>' | '>=') AdditiveExpression
AdditiveExpression:
  MultiplicativeExpression
  | AdditiveExpression ('+' | '-') MultiplicativeExpression
MultiplicativeExpression:
  PrefixUnaryExpression
  | MultiplicativeExpression ('*' | '/' | '%') PrefixUnaryExpression
PrefixUnaryExpression:
  ('+' | '-' | '!') PrefixUnaryExpression
  | PrimaryExpression
PrimaryExpression:
  CSExpression
  | ParenthesizedExpression
  | NullExpression
  | LiteralExpression
  | NameRefExpression
  | MemberAccessExpression
  | InvocationExpression
  | CollectionTypeInstanceExpression
  | ComplexTypeInstanceExpression
  | AnonymousTypeInstanceExpression
  | TestExpression
  | IsNullExpression
  | IsNotNullExpression
  | IsEmptyExpression
  | IsNotEmptyExpression
  | AnyItemExpression
  | DistinctExpression
  | EntityReferenceExpression
  | EntityDereferenceExpression
  | EntityReferenceFromSetExpression
  | EntityReferenceKeyExpression
  | FlattenExpression
  | GroupCollectionExpression
  | DistinctGroupCollectionExpression
  | CastToExpression
  | TreatAsExpression
  | IsOfExpression
  | IsOfOnlyExpression
  | IsNotOfExpression
  | IsNotOfOnlyExpression
  | OfTypeExpression
  | OfTypeOnlyExpression
  | NavigationExpression
  | IsInExpression
  | IsNotInExpression
  | IsLikeExpression
  | IsNotLikeExpression
  | IsBetweenExpression
  | IsNotBetweenExpression
CSExpression:
  '#(' C#Expression ')'
ParenthesizedExpression:
  '(' Expression ')'
NullExpression:
  'null'
LiteralExpression:
  PrimitiveTypeLiteral
NameRefExpression:
  QualifiableName
MemberAccessExpression:
  PrimaryExpression '.' MemberOrExensibleFunctionName
InvocationExpression:
  PrimaryExpression '(' ('distinct' | '*')? (SimpleExpression ',')* ')'
CollectionTypeInstanceExpression:
  EntityOrScalarTypeQualifiableName? '{' (SimpleExpression ',')* '}'
ComplexTypeInstanceExpression:
  ComplexTypeQualifiableName '{' (InstanceMember ',')+ '}'
AnonymousTypeInstanceExpression:
  ('#' C#ClassName)? '{' (InstanceMember ',')+ '}'
InstanceMember:
  MemberName '=' SimpleExpression
TestExpression:
  '{' TestCase+ TestElse? '}'
TestCase:
  'if' '(' SimpleExpression ')' SimpleExpression
TestElse:
  'else' SimpleExpression
IsNullExpression:
  PrimaryExpression '.' 'isnull'
IsNotNullExpression:
  PrimaryExpression '.' 'isnotnull'
IsEmptyExpression:
  PrimaryExpression '.' 'isempty'
IsNotEmptyExpression:
  PrimaryExpression '.' 'isnotempty'
AnyItemExpression:
  PrimaryExpression '.' 'anyitem'
DistinctExpression:
  PrimaryExpression '.' 'distinct'
EntityReferenceExpression:
  PrimaryExpression '.' 'ref'
EntityDereferenceExpression:
  PrimaryExpression '.' 'deref'
EntityReferenceFromSetExpression:
  PrimaryExpression '.' 'refby' '(' AnonymousTypeInstanceExpression ')'
EntityReferenceKeyExpression:
  PrimaryExpression '.' 'key'
FlattenExpression:
  PrimaryExpression '.' 'flatten'
GroupCollectionExpression:
  PrimaryExpression '.' 'groupcollection'
DistinctGroupCollectionExpression:
  PrimaryExpression '.' 'distinctgroupcollection'
CastToExpression:
  PrimaryExpression '.' 'castto' '(' SaclarTypeQualifiableName ')'
TreatAsExpression:
  PrimaryExpression '.' 'treatas' '(' EntityTypeQualifiableName ')'
IsOfExpression:
  PrimaryExpression '.' 'isof' '(' EntityTypeQualifiableName ')'
IsOfOnlyExpression:
  PrimaryExpression '.' 'isofonly' '(' EntityTypeQualifiableName ')'
IsNotOfExpression:
  PrimaryExpression '.' 'isnotof' '(' EntityTypeQualifiableName ')'
IsNotOfOnlyExpression:
  PrimaryExpression '.' 'isnotofonly' '(' EntityTypeQualifiableName ')'
OfTypeExpression:
  PrimaryExpression '.' 'oftype' '(' EntityTypeQualifiableName ')'
OfTypeOnlyExpression:
  PrimaryExpression '.' 'oftypeonly' '(' EntityTypeQualifiableName ')'
NavigationExpression:
  PrimaryExpression '.' 'navigateas' '(' NavigationPropertyName ')'
IsInExpression:
  PrimaryExpression '.' 'isin' '(' Expression ')'
IsNotInExpression:
  PrimaryExpression '.' 'isnotin' '(' Expression ')'
IsLikeExpression:
  PrimaryExpression '.' 'islike' '(' SimpleExpression (',' SimpleExpression)? ')'
IsNotLikeExpression:
  PrimaryExpression '.' 'isnotlike' '(' SimpleExpression (',' SimpleExpression)? ')'
IsBetweenExpression:
  PrimaryExpression '.' 'isbetween' '(' SimpleExpression ',' SimpleExpression ')'
IsNotBetweenExpression:
  PrimaryExpression '.' 'isnotbetween' '(' SimpleExpression ',' SimpleExpression ')'
QueryExpression:
  'from' FromClause WhereClause? (GroupByClause HavingClause?)? SelectClause TopClause? (OrderByClause SkipClause? LimitClause?)?
FromClause:
  SimpleFromClause
  | FromClause ('crossjoin' | 'crossapply' | 'outerapply') SimpleFromClause
  | FromClause ('innerjoin' | 'leftouterjoin' | 'rightouterjoin' | 'fullouterjoin') SimpleFromClause 'on' SimpleExpression
SimpleFromClause:
  Name 'in' SimpleExpression
  | '(' FromClause ')'
WhereClause:
  'where' SimpleExpression
GroupByClause:
  'groupby' (GroupByClauseItem ',')+
GroupByClauseItem:
  Name '=' SimpleExpression
HavingClause:
  'having' SimpleExpression
SelectClause:
  ('select' | 'distinctselect') SimpleExpression
TopClause:
  'top' SimpleExpression
OrderByClause:
  'orderby' (OrderByClauseItem ',')+
OrderByClauseItem:
  SimpleExpression ('collation' Name)? ('ascending' | 'descending')?
SkipClause:
  'skip' SimpleExpression
LimitClause:
  'limit' SimpleExpression

E是C#的超集,E扩展了C#的语法,没有修改或删除C#的语法,E没有引入新的reserved keyword,这意味着你可以将任何C#代码拷贝到.ple文件中,都能正确编译(基本上是,但你知道,现实总有些不完美:)

Namespace

在C# namespace后添加E namespace宣告,则该C# namespace升级成为E namespace,例:

namespace Example.ProjectA[enamespace: com.example.projecta]
{
}

E namespace的名字是dotted identifier。

C# namespace的"effective value"是它自己的值依次加上父namespace的值,例:

namespace NS1.NS2
{
  namespace NS3
  {
  }
}

NS3的"effective value"是NS1.NS2.NS3,即,NS3中的member的full name是以NS1.NS2.NS3开始的。

C# Namespace总是open的,例:

namespace NS1
{
}
namespace NS1
{
}

多个具有相同"effective value"的C# namespace合成一个逻辑namespace。

E Namespace也总是open的,例:

namespace NS1.NS2[enamespace: X]
{
}
namespace NS1
{
  namespace NS2[enamespace: X]
  {
  }
}

相同的E Namespace必须拥有相同的C# Namespace 的"effective value"。如上例,相同的E namespace X拥有相同的C# namespace的"effective value" NS1.NS2。下面是错误的:

namespace NS1.NS2[enamespace: X]
{
}
namespace NS1
{
  namespace NS3[enamespace: X]
  {
  }
}

E Namespace之间不会形成层次,例:

namespace NS1[enamespace: X]
{
  namespace NS2[enamespace: Y]
  {
  }
}

和C#不同,嵌套的E namespace之间不会形成层次,Y的"effective value"就是Y,不是X.Y。

Namespace Member

E namespcae的成员可以是任意C# namespace的成员,也可以是E的构造:entity type($entity), struct type($struct), enum type($enum), function($function)和context($context)。它们都有名字,可以通过名字被引用。在同一namespace中,entity type, struct type, enum type, function, context的名字必须相互唯一,context的名字在所有namespace中必须唯一,function可以overload,即名字相同,但签名不同。例:

using System;
using System.Collections.Generic;

namespace Example.ProjectA[enamespace: com.example.projecta]
{
  class CSClass{}
  struct CSStruct{}
  enum CSEnum{}
  namespace NS1{}
  $entity E1[abstract]
  {
    $property Id[key; identity] as edm:Int32[notnull];
    $property P1 as String;
    $property P2 as S1[notnull];
  }
  $struct S1{}
  $enum Enum1{}
  $context C1{}
  $function F1(a as Int32, b as Int64) => a + b;
  $function F1(a as Int32) => a + 1;
}
namespace Example.ProjectB[enamespace: com.example.projectb]
{
  using System.Runtime.Serialization;
  $import com.example.projecta as pa;
  using PowerLanguages.E;

  $entity E1 : pa:E1
  {
    $property P3 as Enum1;
  }
}

Edm Namespace

Edm namepscae是系统内置的,它的名字是Edm。它包含primitive types和canonical functions。

Namespace Import

如同C#的using,一个namespace的成员想要访问另一个namespace的成员,首先得把该namespace import进来。如上例,com.example.projectb namespace import了com.example.projecta namespace,并alias为pa,alias是可选的。Edm namespace总是被隐含import到每个用户定义的namespace中,alias为edm。

Qualifiable Name

使用qualifiable name访问另一个namespace的成员。它有两种形式:unqualified和qualified。unqualified的格式就是name,qualified的格式是alias : name,alias为namespace import所取的alias。

决议一个qualified name,首先通过alias找到指定的imported namespace,然后在此namespace查找指定的名字。

决议一个unqualified name,首先在本namespace中查找名字,若找到则决议成功,若未找到,则在所有imported namespace中查找名字,若找到且仅找到一个,则决议成功,若找到多个,则名字含混。

如上例,com.example.projecta namespace的E1 entity type的Id property引用了Edm namespace的Int32 primitive type,使用了qualified的形式;P1 property引用了Edm namespace的String primitive type,使用了unqualified的形式;P2 property引用了本namespace的S1 struct type,使用了unqualified的形式;com.example.projectb namespace的E1 entity type继承自com.example.projecta namespace的E1 entity type,使用了qualified的形式;P3 property引用了com.example.projecta namespace的Enum1 enum type,使用了unqualified的形式。

Type

Global Type

Global type拥有唯一的名字,它们可以通过qualifiable name被引用。

Scalar Type

Scalar type是不可被拆分的类型。

Primitive Type

Primitive type是系统内置的,存在于Edm namespace中。

Primitive Type 对应的CLR Type Literal Format Canonical String Byte System.Byte 无。但可以从Int32隐式转换,只要值没超出范围 DecimalDigits,去掉开头的0及+号 SByte System.SByte 无。但可以从Int32隐式转换,只要值没超出范围 DecimalDigits,去掉开头的0及+号 Int16 System.Int16 无。但可以从Int32隐式转换,只要值没超出范围 DecimalDigits,去掉开头的0及+号 Int32 System.Int32 同C#。例:123 DecimalDigits,去掉开头的0及+号 Int64 System.Int64 同C#。例:123L DecimalDigits,去掉开头的0及+号 Decimal System.Decimal 同C#。例:123.4M DecimalDigits(.DecimalDigits)?,去掉开头的0及+号 Single System.Single 同C#。例:123.4F DecimalDigits(.DecimalDigits)?,去掉开头的0及+号 Double System.Double 同C#。例:123.4D DecimalDigits(.DecimalDigits)?,去掉开头的0及+号 String System.String 同C#。例:"str", @"str" 其值,没有前后的双引号 Boolean System.Boolean true, false true, false Binary System.Byte[] binary"HexDigits"。例:binary"ABCD1234" HexDigits Guid System.Guid guid"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"。例:guid"47911A9B-C596-4AA3-968D-11E195C16D2A" XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX DateTime System.DateTime datetime"yyyy-MM-dd HH:mm" or datetime"yyyy-MM-dd HH:mm:ss.FFFFFFF"。例:datetime"2012-12-21 15:36" yyyy-MM-dd HH:mm:ss.FFFFFFF DateTimeOffset System.DateTimeOffset datetimeoffset"yyyy-MM-dd HH:mm zzz" or datetimeoffset"yyyy-MM-dd HH:mm:ss.FFFFFFF zzz"。例:datetimeoffset"2012-12-21 15:36:56.1234567 +08:00" yyyy-MM-dd HH:mm:ss.FFFFFFF zzz Time System.TimeSpan time"hh:mm" or time"hh:mm:ss.FFFFFFF"。例:time"15:36:56" hh:mm:ss.fffffff Geometry System.Data.Spatial.Geometry geometry"WellKnownText" or geometry("WellKnownText", Srid)。例:geometry"POINT (30 10)" WellKnownText Geography System.Data.Spatial.Geography geography"WellKnownText" or geography("WellKnownText", Srid)。例:geography"POINT (30 10)" WellKnownText

Enum Type

Enum type和C#的相似。例:

$enum Reputation : Int16 {Bad = -1, None = 0, Bronze, Silver, Gold, Diamond}

Enum type存在一个underlying type,它是integer primitive type,可以显式指定,如上例,underlying type是Int16,若没指定,默认是Int32。若未显式为成员指定值,则其值为上一个成员的值加1,若未显式为第一个成员指定值,则为0。

PowerLanguages.E的enum type没C#的灵活。为成员赋值只能使用integer literal。

Complex Type

Complex type是结构化的类型,拥有零到多个成员,每个成员的名字必须唯一,成员可以是property或navigation property。

Entity Type

Entity type用来描述现实对象,如Customer, Order, Product, Supplier。Entity type可以是抽象的,它不能用来创建实例,这通过abstract attribute指定。Entity type间可以单根继承。Entity type的成员可以是property和navigation property,必须为entity type指定一个key property,但只有base entity type才能指定key property,derived entity type的成员名字不能与base entity type的成员名字重复。例:

$entity Contact[abstract]
{
  $property Id[key; identity] as Int32[notnull];
  $property Name as String[notnull; lengthrange: 1..20];
  $property Address as AddressInfo[notnull];
  $property Phone as String[lengthrange: 1..20];
  $property EMail as String[notnull; lengthrange: ..40; pattern: @"^(?("")(""[^""]+?""@)|(([0-9a-z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-z])@))(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-z][-\w]*[0-9a-z]*\.)+[a-z0-9]{2,17}))$", ignorecase];
  $property RegistrationDate as DateTimeOffset[notnull];
}
$struct AddressInfo
{
  $property Country as String[notnull; lengthrange: 1..20];
  $property State as String[notnull; lengthrange: 1..20];
  $property City as String[notnull; lengthrange: 1..20];
  $property Address as String[notnull; lengthrange: 1..40];
  $property ZipCode as String[notnull; lengthrange: 1..10];
}
$entity Customer : Contact
{
  $property Reputation as Reputation[notnull];
  $naviproperty Orders[cascadedelete] to Order*;
}
$entity Supplier : Contact
{
  $property BankAccount as String[notnull; lengthrange: 1..20];
  $naviproperty Products to Product*;
}

Contact是个abstract entity type,由它派生出两个concrete type Customer和Supplier。Contact有一个key property Id。

Struct Type

Struct type是一个值包,成员只能是property,且不能指定为key,struct type间不能继承,struct type总是直接或间接附属于entity type。示例见上。

注:PowerLanguages.E的struct type对应于CSDL的complex type。CSDL的complex type是个糟糕的命名,complex……难道entity type不complex吗?在PowerLanguages.E中,complex type是entity type和struct type的统称。

Property

Property构成了complex type的值。在property name后的方括号中可以指定可选的property attribute: key, value generation mode和concurrency stamp。Key指定该property作为entity type的“主键”。Value generation mode有两个值:identity和computed。identity通常和key一起使用,表明这是一个自增key。computed的意思是,当向data store添加或更新complex object时,data store库会计算出一个新值,并反向赋值给该property。Concurrency stamp通常和computed一起使用,和SQL Server的rowversion列意义相同。例:

$property ConcurrencyStamp[concurrencystamp; computed] as Binary[notnull];

Property的类型可以是scalar type或struct type,这在as keyword后指定,key property的类型只能是scalar type。

Type Facet

Facet对类型进行约束或注解,它们在类型名后的方括号中指定,如前面例子中的notnull。

Facet 说明 适用于 notnull 所有scalar type默认是nullable的,指定notnull则non-nullable,struct type必须显式指明为notnull all scalar and struct types defaultvalue 指定缺省值。对于primitive type,使用literal format;对于enum type,只需指定成员名字。例:
$property RegistrationDate as DateTimeOffset[notnull; defaultvalue: datetimeoffset"2012-12-21 15:36 +08:00"];
$property Reputation as Reputation[notnull; defaultvalue: Bronze];
all scalar types length 字符数或字节数必须等于该值,与lengthrange互斥。例:
$property P1 as String[length: 20]; String, Binary lengthrange 字符数或字节数范围。例:
$property P1 as Binary[lengthrangth: 2..10];//length >= 2 && length <= 10
$property P2 as String[lengthrangth: ..10];//length <= 10
$property P3 as String[lengthrangth: 10..];//length >= 10
String, Binary valuerange 值范围。'['或']'表示包含,'('或')'表示不包含。例:
$property P1 as Int32[valuerange: [3..5)];//value >= 3 && value < 5
$property P2 as Reputation[valuerange: ..Glod]];//value <= Reputation.Glod
$property P3 as DateTime[valuerange: [datetime"2013-01-01 00:00".. ];//value >= 2013-01-01 00:00
numeric types, DateTime, DateTimeOffset, Time, enum type precision Decimal的总位数或时间的精度 Decimal, DateTime, DateTimeOffset, Time scale Decimal的小数位数。例:
$property Price as Decimal[notnull; precision: 18; scale: 2]; Decimal pattern 测试canonical string是否符合正则表达式。对于enum type,canonical string是其成员的名字。例:
$property Ssn as String[notnull; pattern: @"\d{3}-\d{2}-\d{4}"]; all scalar types enumerations 值必须在枚举值中。例:
$property P1 as Int32[enumerations: 2, 3, 5, 7, 11, 13];
$property P2 as Reputation[enumerations: Diamond];
all scalar types nonunicode 仅为兼容CSDL String collation 仅为兼容CSDL。例:
$property P1 as String[nonunicode; collation: Latin1_General_CI_AS]; String srid 仅为兼容CSDL。例:
$property P1 as Geography[srid: 4326]; Geometry, Geography

Navigation Property

Navigation property用于建立entity type间的联合(association)或关系(relationship),PowerLanguages.E混用association和relationship这两词。比如一个customer拥有零到多个order,一个order有一到多个detail,一个detail对应一个product,一个product可由多个supplier提供,一个supplier可以提供多个product。在CSDL中,需要显式创建association,但在PowerLanguages.E中,association由navigation property隐式建立。一个association拥有两个end,每个end具有唯一的名字,每个end拥有重数(multiplicity),可以是1, 0或1, 0 到多(many)。可以这么描述:Customer_Order association拥有两个end,Customer end multiplicity为1,Order end multiplicity为many;Supplier_Product association拥有两个end,Supplier end multiplicity为many,Product end multiplicity为many。下面的例子建立Customer与Order间的联合:

$entity Customer : Contact
{
  $naviproperty Orders[cascadedelete] to Order*;
  ...
}
$entity Order
{
  $naviproperty Customer[foreignkeys: CustomerId] to Customer;
  $property CustomerId as Int32[notnull];
  ...
}

两个navigation property(Orders和Customer)相互指向对方,形成一个联合。to keyword后面指定对方entity type的qualifiable name,也可以在entity type name后明确指定navigation property name,例:

$naviproperty Orders[cascadedelete] to Order.Customer*;

$naviproperty Customer[foreignkeys: CustomerId] to Customer.Orders;

在无含混的情况下,编译器会自动分辨清到底是哪两个navigation property在相互指向。

最后指定对方的multiplicity:

Multiplicity 标记 1 <空> 0或1 ? many *

上例中,Customer end multiplicity为1,Order end multiplicity为many。

一个entity type中的两个navigation property可以相互指向,例:

$entity Employee
{
  $property Id[key; identity] as Int32[notnull];
  $property Name as String[notnull];
  $naviproperty DirectStaffs[endname: Leader] to Employee*;
  $naviproperty Leader[endname: Staffs; foreignkeys: LeaderId] to Employee?;
  $property LeaderId as Int32;

  public static Employee Create()
  {
    return new Employee
    {
      Name="Tank",
      DirectStaffs=
      {
        new Employee
        {
          Name="Mike",
          DirectStaffs=
          {
            new Employee{Name="Jack"},
            new Employee{Name="Eric"},
          }
        },
        new Employee{Name="Dick"}
      }
    };
  }
}

默认情况下,end name是navigation property的父entity type的名字,若两个entity type的名字相同,比如NS1.E1里的navigation property与NS2.E1里的navigation property相互指向,那么end名字都是E1,因为end name必须唯一,编译器会自动把一个end命名为E1_2,这时通常需要显式指定end名字,这通过endname attribute指定,如上例所示。

除非是many-to-many的联合,必须为multiplicity many end或multiplicity 0或1 end指定外键,这通过foreignkeys attribute指定。此外,还可以指定cascadedelete attribute。如上两例所示。

Context

Context是个逻辑容器,容纳entity set和function import。Context可以继承自另一个context,这极少用到。Context对应于CSDL中的EntityContainer。

Entity Set

Entity set是entity type instance(或叫entity object)的集合。例:

$context EBusiness
{
  $entityset Contacts of Contact associate Orders by Customer.Orders, Products by Supplier.Products as SuppliersProducts;
  $entityset Orders of Order associate OrderDetails by Details;
  $entityset OrderDetails of OrderDetail associate Products by Product;
  $entityset Products of Product;
}

Contacts entity set包含Contact entity type的实例,因为Contact是抽象的,不能创建实例,Customer和Supplier继承自它,则Contacts entity set将包含Customer和Supplier的实例。

Navigation property定义了entity type间的逻辑联合,但entity object间的物理联合却是未知的,考虑下面的情况:

$entityset CommonCustomers of Customer;
$entityset VipCustomers of Customer;
$entityset Orders of Order;

Orders entity set中的Order object到底是和CommonCustomers还是VipCustomers entity set中的Customer object联合呢?编译器不知道,这需要通过associate clause显式指定。

在上例中,Contacts entity set中的Customer object和Orders entity set中的Order object联合,通过Customer.Orders navigation property;Contacts entity set中的Supplier object和Products entity set中的Product object联合,通过Supplier.Products navigation property。以此类推。

技术上说,navigation property相互指向隐含创建了一个association,associate clause联合两个entity set隐含创建了一个association的实例,在CSDL中叫做association set,如果是many-to-many的association,则必须为association set指定一个名字,如上例中的as SuppliersProducts。只需要在一个entity set指定联合即可,在两边都指定会编译错误。当前编译器做得不够智能,以后版本,如果没有含混,编译器应能自动联合entity set。

Function Import

Function import将store model中的function import到context中。你需要跳到后面阅读对store model的function的叙述,然后回来阅读此节。

返回scalar值的user-defined function不能被import到context中,其它的可以:

$functionimport GetOrders[composable](customerId as Int32) as {Id as Int32, OrderDate as DateTimeOffset, ShippingCountry as String}*;
$functionimport GetAmountAndOrders(customerId as Int32, amount[out] as Decimal) as {Id as Int32, OrderDate as DateTimeOffset}*;
$functionimport GetContactsFrom(country as String) as {Id as Int32, Name as String, Reputation as Reputation}*, {Id as Int32, Name as String, BankAccount as String}*;
$functionimport DeleteContact(Id as Int32);

Function import的参数的名字必须和store function的参数的名字完全相同,且类型和方向也相同,但位置可以不同,Composable的值也必须相同。如果store function返回table值(或叫result set),那么function import的返回值的宣告可以有如下的选择:

首先可以像上面那样定义“匿名类”,编译器将根据定义自动生成一个struct type,名字为<ContextName>_<FunctionImportName>_Result。比如GetOrders,编译器将生成名为EBusiness_GetOrders_Result的struct type。对于multiple result set,那么第二个result set的名字后将加上one-based index。比如GetContactsFrom,编译器将生成名为EBusiness_GetContactsFrom_Result和EBusiness_GetContactsFrom_Result1的struct type。“匿名类”的成员数量可以比store function的少,名字不一定和store function的table返回值的成员名字相同。例:

$functionimport GetOrders[composable](customerId as Int32) as {Id as Int32, OrderDateXYZ as DateTimeOffset}*;

如果store function返回值只有一个成员,比如下面的store function:

function GetOrders2[composable](customerId as int) as {Id as int}*;

那么function import声明一个匿名类太overkill,可以声明返回scalar collection:

$functionimport GetOrders2[composable](customerId as Int32) as Int32*;

也可以预先声明一个struct type:

$struct OrderData
{
  $property Id as Int32;
  $property OrderDate as DateTimeOffset;
  $property ShippingCountry as String;
}

然后指定function import返回该struct collection:

$functionimport GetOrders[composable](customerId as Int32) as OrderData*;

如果多个function import返回相同的类型,建议使用这种方式以减少代码冗余。

Function import的返回值的宣告也可以使用现有的entity type,但有如下的限制(这其实是EntityFramework的限制):entity type不能包含struct property,所有的property必须赋值。下面的store function返回OrderDetail的所有数据:

function GetOrderDetail(orderId as int) as {Id as int, OrderId as int, ProductId as int, UnitPrice as decimal(18, 2), Quantity as int}*;

下面的function import将返回OrderDetail entity collection:

$functionimport GetOrderDetail(orderId as Int32) as OrderDetail* into OrderDetails;

into后面指定entity set的名字,如果function import返回entity collection,则必须指明。

Local Type

Local type用于function的参数和返回类型的宣告,以及构成表达式的语意。它有三种类型:collection type, anonymous type, entity reference type。前两种类型的含义不言自明,entity reference type是entity type的“指针”类型,本质上,它包含entity type key property的数据。

Function

Function是表达式复用的单元。例:

$function F1(a as Int32, b as Customer, c as Order*) as {X as AddressInfo, Y as Product&}* => <expression>;

Parameter a, b的类型是global type,c是collection type,返回值是一个anonymous collection type,anonymous type的成员X是global type, Y是entity reference type。

返回类型的宣告可以省略,因为可以从表达式的类型推导出来,若指明了,则表达式的类型必须兼容于宣告的类型。

Function可以overload ,即名字相同,但参数的签名不同。

如果function拥有至少一个parameter,则可以声明为extensible的。例:

$function F_Extensible[extensible](a as Customer) => <expression>;

Extensible function借鉴了C# extension method。

Edm namespace定义了canonical functions。用PowerLanguages.E的语法表达如下(实际编译不过):

...
$function Count[aggregate; extensible](collection as Int16*) as Int32;
$function Count[aggregate; extensible](collection as Int32*) as Int32;
$function Count[aggregate; extensible](collection as Int64*) as Int32;
$function Count[aggregate; extensible](collection as String*) as Int32;
...
$function Length[extensible](stringArgument as String) as Int32;
...
$function Year[extensible](dateValue as DateTimeOffset) as Int32;
$function Year[extensible](dateValue as DateTime) as Int32;
...

本文后面将列出所有canonical function的原型。

Expression

PowerLanguages.E的表达式实现了Entity SQL的语意,但自创了更友好的语法。PowerLanguages.E是门强类型的元编译语言,类型更是表达式的核心支撑。表达式可以自由组合,只要类型兼容。建议读者跳到前面熟悉语法,那么你已经对表达式掌握了一半,下面叙述剩下的一半。

Null Expression

null值。因为所有类型都是nullable的,理论上,null值兼容于所有type,然而,由于一些只有microsoft guy才能解释清楚的原因,null值不兼容collection type(其实是可以理解的,比如,一个C# method返回IEnumerable<T>类型,如果保证不返回null值,客户编程将轻松些)。

Is (Not) Null Expression

判断值为否是null。例:

$function F_IsNull(a as Int32) as Boolean => a.isnull;
$function F_IsNotNull(a as Customer) as Boolean => a.isnotnull;

Literal Expression

例:

$function F_Literal() => {A = 123, B = @"str", C = datetimeoffset"2012-12-21 15:36:56.12345 +08:00"};

Anonymous Type Instance Expression

构造anonymous type的instance,如F_Literal()所示,其实例的类型是:

{A as Int32, B as String, C as DateTimeOffset}

Name Reference Expression

引用一个名字。它的target可以是:context name, function name, enum type name, function parameter name, simple from clause name, group by clause item name。对于context name, function name, enum type name,可以使用qualified的形式,其它的只能使用unqualified的形式。例:

$function F_NameRef() as Reputation => Reputation.None;
$function F_NameRef2() as Contact* => EBusiness.Contacts;
$function F_NameRef(a as Int32, b as Int64) => a + b;

可以看出,凭直觉也能想出,entity set的类型是entity collection。

Member Access Expression

访问structural object的成员,structural object可以是:context, enum type, complex type, anonymous type。如上例,访问了enum type和context的成员。例:

$function F_MemberAccess(c as Contact) => c.Name;
$function F_MemberAccess(c as Customer) as Order* => c.Orders;
$funnction F_MemberAccess(o as Order) as Customer => o.Customer;
$funnction F_MemberAccess() => F_Literal().C;

可以看出,凭直觉也能想出,navigation property的类型是entity (collection)。

Invocation Expression

调用function。对于extensible function,可以“后缀式”调用。例:

$function F_NormalInvocation(a as Customer) => F_Extensible(a);
$function F_ExtensibleInvocation(a as Customer) => a.F_Extensible();
$function F_NormalInvocation(a as Int32*) => edm:Count(a);
$function F_ExtensibleInvocation(a as Int32*) => a.Count();

Composable function import也可以调用。例:

$function F_InvokeFunctionImport(customerId as Int32) => EBusiness.GetOrders(customerId);

Collection Type Instance Expression

构造collection type的instance。元素的类型要相互兼容。例:

$function F_CollectionTypeInstance(a as Int32) as Decimal* => {1, a * 2.6M, null, 3};

也可以构造scalar或non-abstract entity type的empty collection instance。例:

$function F_CollectionTypeInstance() as Customer* => Customer{};

Is (Not) Empty Expression

判断collection是否empty。例:

$function F_IsEmpty(a as Int32*, b as Int32*) => {X=a.isempty, Y=b.isnotempty};

Any Item Expression

得到collection中任意一个element,如果collection为empty,则返回null。例:

$function F_AnyItem(a as Int32*) as Int32 => a.anyitem;

Distinct Expression

去除collection中的重复element。例:

$function F_Distinct(a as Int32*) => a.distinct;

Flatten Expression

“平坦化”一层collection。例:

$function F_Flatten(a as Int32**) as Int32* => a.flatten;

Is (Not) In Expression

判断某值是否在collection中。例:

$function F_IsIn(a as Int32, b as Int64*) => {A=a.isin({1, 2, 3}), B=a.isnotin(b)};

Overlaps Expression, Except Expression, Union (All) Expression, Intersect Expression

集合操作。例:

$function F_Set(a as Int32*, b as Int32*) as {A as Boolean, B as Int32*, C as Int32*, D as Int32*, E as Int32*} => {A = a overlaps b, B = a except b, C = a union b, D = a unionall b, E = a intersect b};

Is (Not) Like Expression

例:

$function F_IsLike(a as String) => {A = a.islike("%new%"), B = a.isnotlike("[A-D]xy"), C=a.islike("%10-15!% off%", "!")};

第二个参数的escape。

Is (Not) Between Expression

例:

$function F_IsBetween(a as Int32) => {A = a.isbetween(1, 3), B = a.isnotbetween(1, 3)};

Test Expression

按顺序,对每个if求值,若为true,则表达式的值为该if的"then"值,否则继续求值,直到else。若没有if为真,且无else,则表达式值为 null。例:

$function F_Test(a as Reputation) as String => {if(a == Reputation.Bad) "敬而远之" if(a == Reputation.None) "观察一下" if(a == Reputation.Bronze) "做点小交易" else "来笔大买卖"};

Complex Type Instance Expression

构造complex type的instance。必须设置complex type中的所有non-nullable property的值,顺序任意。例:

$function F_ComplexTypeInstance() => Customer{Id=1, Name="Tank", Address=AddressInfo{Country="China", State="Sichuan", City="Suining", Address="somewhere", ZipCode="629000"}, EMail="someone@example.com", RegistrationDate=datetimeoffset"2012-12-21 15:36 +08:00", Reputation=Reputation.Bronze};

Entity Reference Expression

创建entity reference type。例:

$function F_EntityRef() as Order& => EBusiness.Orders.anyitem.ref;

Entity Dereference Expression

De-reference entity reference type。例:

$function F_EntityDeref(a as Order&) as Order => a.deref;

Entity Reference From Set Expression

Entity type的key property按宣告顺序,可以构成一个anonymous type(entity type keys anonymous type),用此anonymous type的instance从entity set中得到entity reference type。例:

$function F_EntityRefFromSet() as Order& => EBusiness.Orders.refby({Id=1});

Entity Reference Key Expression

从entity reference type得到entity type keys anonymous type。例:

$function F_ref_key(a as Order&) as {Id as Int32} => a.key;

Cast To Expression

在scalar type间相互cast。有些cast是无意义的,比如将boolean cast成datetime,运行时将报错。可以将null值cast成scalar type。例:

$function F_CastTo() => {A=1.castto(Int64), B=1.castto(Reputation), C=true.castto(String), D=null.castto(Int32)};

Treat As Expression

将entity type treat成它的继承层次的其它entity type,如同C#的as。可以将null值treat成entity type。不能将null值treat成struct type, anonymous type, entity reference type,只有Microsoft的guy才能解释清楚为什么。例:

$function F_TreatAs(a as Contact) as {A as Customer, B as Customer} => {A=a.treatas(Customer), B=null.treatas(Customer)};

Is (Not) Of (Only) Expression

判断entity type是(或仅仅是)指定类型 。例:

$function F_IsOf(a as Product) => {A=a.Name, B=a.isof(OutdoorShoe), C=a.isofonly(OutdoorShoe), D=a.isnotof(OutdoorShoe), E=a.isnotofonly(OutdoorShoe)};

Of Type (Only) Expression

从entity collection中返回指定(或仅仅是指定)类型的collection。例:

$function F_OfType(a as Product*) as {A as OutdoorShoe*, B as OutdoorShoe*} => {A=a.oftype(OutdoorShoe), B=a.oftypeonly(OutdoorShoe)};

Navigation Expression

通过navigation property进行navigate,和直接访问navigation property不同,它返回entity reference (collection)。例:

$function F_Navigation(a as Order) as OrderDetail&* => a.navigateas(OrderDetails);

Query Expression

from where? (groupby having?)? select top? (orderby skip? limit?)? 的形式。Query expression总是返回collection type。各个clause的语意和SQL的一样。例:

$function GetCustomerAndOrderAmounts() => from c in EBusiness.Contacts.oftype(Customer) select
  {
    Customer = c,
    OrderAmount = (from o in c.Orders select (from od in o.Details select od.UnitPrice * od.Quantity).Sum()).Sum(),
    Orders = (from o in c.Orders select
    {
      Order = o,
      Details = (from od in o.Details select
      {
        Detail = od,
        Product = od.Product
      })
    })
  };
$function GetCustomerAndOrderAmountsOver(amount as Decimal) => from i in GetCustomerAndOrderAmounts() where i.OrderAmount >= amount select i;

快速理解使用query expression的要点是:一切都是表达式

首先是from clause。"from name in collection",那么name的类型是collection element的类型。因为可以访问navigation property,entity type间的join操作变得极少使用。然后where clause,不言自明。下面的例子演示groupby:

$function GetCustomerRegistrations() => from c in EBusiness.Contacts.oftype(Customer) groupby year = c.RegistrationDate.Year() select
  {
    RegistrationYear = year,
    Count = Count(*),
    CountByCountry = c.Address.Country.Count(distinct)//等同于:Count(distinct c.Address.Country)
  };

Year(), Count()都是canonical functions,且使用了extensible的调用方式。Group by follower clause(having, select, top, orderby, skip, limit)要访问from clause的name,需要包含在aggregate canonical functions中。Invocation expression有两个可选的modifier:"distinct"和"*",含义同SQL,它们只能应用于aggregate function,且只能用在group by follower clause里,"*"只能用于Edm.Count()或Edm.BigCount()。

在上例中,c的数据类型是Customer,c.Address.Country的数据类型是Edm.String,既然Count()的parameter类型必须是collection,那为什么能调用成功呢?在group by follower clause中,对aggregate function的调用编译器将特殊处理。

(distinct) group collection expression用在group by follower clause中,用于生成collection实例。例:

$function F_GroupCollection() as {RegistrationYear as Int32, Customers as Customer*}* => from c in EBusiness.Contacts.oftype(Customer) groupby year = c.RegistrationDate.Year() select {RegistrationYear = year, Customers = c.groupcollection};

select, top, orderby, skip, limit cluase的含义和SQL的一样,请参阅前面的语法使用。

Entity Data Metaprogramming Model

前面的内容可以概括为,用自创的用户友好的语法实现了Entity Data Model和Entity SQL的语意。它们只是概念模型,现在要把它们变成编程模型,这就是Entity Data Metaprogramming Model,注意这也是模型,也是抽象的,与具体的实现,如Entity Framework,无关。建议读者阅读__E.meta.cs, __EntityFramework.impl.cs, Globals__.meta.cs, Globals__.impl.cs, Example.ple.meta.cs, Example.ple.impl.cs,你会发现,所谓的Entity Data Metaprogramming Model不过是EntityFramework功能的简单抽象与封装。

从Entity Data Metaprogramming Model角度看,complex type和context将元编译成C# class,enum type将元编译成C# enum,property, navigation proerty, entity set将元编译成C# property, function import将元编译成C# method。所以,E的语言构造肩负着双重目的:描述EDM和构造C#的语言元素(class, enum, property, method)。C#的class, enum, property, method可以指定attributes和modifiers(public, protected, internal, private, abstract, virtual, override, sealed, new, partial),且property还可以指定getter和setter的attributes和modifiers。C#的attributes and/or modifiers在E的attributes中指定。例:

namespace Example.ProjectA[enamespace: com.example.projecta]
{
    using System;
    using System.Runtime.Serialization;

    [AttributeUsage(AttributeTargets.All)]
    class CSAttr1 : Attribute {public string A; public bool B;}
    [AttributeUsage(AttributeTargets.All)]
    class CSAttr2 : Attribute { }
    [DataContract(IsReference = true)]
    [Serializable]
    public class CSClass
    {
        public virtual void Test(){}
    }
    public interface ICSInterface {}

    $entity E1[[CSAttr1(A="str", B=true)][CSAttr2] abstract] : CSClass, ICSInterface 
    {
        $property Id[key] as Int32[notnull];
        $property P1[[CSAttr1] internal; setter: [CSAttr2] private] as S1[notnull];
        $naviproperty NP1 to E2*;
        public override void Test(){}
    }
    $entity E1X[internal sealed] : E1
    {
        $property P2 as Enum1;
    }
    $struct S1 : CSClass{}
    $enum Enum1[[Flags]] : Int64
    {
        M1 = 1,
        M2 = 2,
        M3 = 4
    }
    $entity E2
    {
        $property Id[key] as Int32[notnull];
        $naviproperty NP1[foreignkeys: E1Id] to E1;
        $property E1Id as Int32[notnull];
    }
    $context C1 : CSClass
    {
        $entityset E1s of E1 associate E2s by NP1;
        $entityset E2s of E2;
        $functionimport F1[internal](a as Int32, b[inout] as Int32) as E1X* into E1s;
    }
}

E的语法是是显式及后置式的,首先指明这是什么($entity, $struct, $context, $property…),然后指定名字,然后在中括号中进行描述。C# attributes和modifiers属于描述,因此存在于中括号中,见上面的示例。要为C#的getter和setter指定attributes和modifiers,需要在前面加上"getter:"或"setter:"。如果未指定modifiers,则元编译出来的class的modifiers为public partial,partial总是存在的,无法去除,其余的,enum, property, method的modifiers为public。

Entity type间可以单根继承,context可以继承自另一个context,除此以外,complex type和context可以继承C# class和实现interface。如果entity type和context的第一个base type name为qualified name,则表明显式继承自E的构造,若为name,则编译器首先查找是否存名为此的E的构造,若否,则假设为C# class。下面是上例元编译成C#的代码:

namespace Example.ProjectA
{
    using System;
    using System.Runtime.Serialization;

    [AttributeUsage(AttributeTargets.All)]
    class CSAttr1 : Attribute
    {
        public string A;
        public bool B;
    }
    [AttributeUsage(AttributeTargets.All)]
    class CSAttr2 : Attribute
    {
    }
    [DataContract(IsReference = true)]
    [Serializable]
    public class CSClass
    {
        public virtual void Test(){}
    }

    [CSAttr1(A="str", B=true)]
    [CSAttr2]
    [global::System.SerializableAttribute()]
    [global::System.Runtime.Serialization.DataContractAttribute(Namespace = @"urn:com:example:projecta", IsReference = true)]
    [global::System.Runtime.Serialization.KnownTypeAttribute(typeof (global::Example.ProjectA.E1X))]
    public abstract partial class E1 : CSClass, ICSInterface, global::PowerLanguages.E.IEntityObject
    {
        public int Id { get {...} set {...} }
        [CSAttr1]
        internal global::Example.ProjectA.S1 P1 { get {...} [CSAttr2] private set {...} }
        public global::PowerLanguages.E.IEntityCollection<global::Example.ProjectA.E2> NP1 { get {...} } 
        public override void Test(){}
        ...
    }
    [global::System.SerializableAttribute()]
    [global::System.Runtime.Serialization.DataContractAttribute(Namespace = @"urn:com:example:projecta", IsReference = true)]
    internal sealed partial class E1X : global::Example.ProjectA.E1, global::PowerLanguages.E.IEntityObject
    {
        public global::Example.ProjectA.Enum1? P2 { get {...} set {...} }
        ...
    }
    [global::System.SerializableAttribute()]
    [global::System.Runtime.Serialization.DataContractAttribute(Namespace = @"urn:com:example:projecta", IsReference = true)] 
    public partial class S1 : CSClass, global::PowerLanguages.E.IStructObject
    {
        ...
    }
    [Flags]
    public enum Enum1 : long
    {
        M1 = 1,
        M2 = 2,
        M3 = 4
    } 
    [global::System.SerializableAttribute()]
    [global::System.Runtime.Serialization.DataContractAttribute(Namespace = @"urn:com:example:projecta", IsReference = true)]
    public partial class E2 : global::PowerLanguages.E.IEntityObject
    {
        public int Id { get {...} set {...} }
        public global::Example.ProjectA.E1 NP1 { get {...} set {...} }
        public int E1Id { get {...} set {...} }
        ...
    }
    public partial class C1 : CSClass, global::PowerLanguages.E.IContextObject
    {
        public global::PowerLanguages.E.IEntitySet<global::Example.ProjectA.E1> E1s { get {...} }
        public global::PowerLanguages.E.IEntitySet<global::Example.ProjectA.E2> E2s { get {...} }
        internal global::System.Collections.Generic.IEnumerable<global::Example.ProjectA.E1X> F1(int? a, int? b, out Func<int?> b_Out, global::PowerLanguages.E.MergeOption mergeOption_ = global::PowerLanguages.E.MergeOption.AppendOnly) {...}
        ...
    }
}

Entity type和struct type的实例通常要被序列化,元编译会自动加上序列化的支持,支持DataContract和Serializable这两种序列化方式。如果一个C# class作为entity type或struct type的基类,那么它通常需要提供序列化的支持,否则在序列化时将会出错。见上面示例。 

Lambda Query

Labmda query将PowerLanguages.E的表达式与C#的表达式集成在一起。例:

$context EBusiness
{
    internal void GetContacts(int start){
        foreach(var i in this.$(from c in EBusiness.Contacts select c orderby c.Name skip #(start) limit #(new Random((int)DateTime.Now.Ticks).Next(1, 11)) [notracking]))
            Console.WriteLine(i.Id + ", " + i.Name);
    }
}

#(…)中为任何C#表达式。元编译成C#代码:

public partial class EBusiness
{
    internal void GetContacts(int start)
    {
        foreach (var i in ((global::System.Func<object, global::System.Collections.Generic.IEnumerable<global::Example.ProjectA.Contact>>)((__arg_0) =>
        {
            var p___0_0 = start;
            var p___0_1 = new Random((int)DateTime.Now.Ticks).Next(1, 11);
            throw new global::System.NotImplementedException();
        }
        ))(this))
            Console.WriteLine(i.Id + ", " + i.Name);
    }
}

之所以叫lambda query,因为它被元编译成C# lambda expression。Lambda query必须附着于一个contextual C# expression之后,在上例中是this。Contextual C# expression可以是任何C#表达式,但在运行时必须是context, entity或struct object,且entity或struct object必须attach到context中,这意味着可以在entity或struct的C# method中进行query。例:

$entity E1
{
    public void Method()
    {
        this.$(...);
    }
}

甚至这样:

static void Method(object obj)
{
    obj.$(...);
}

在运行时,obj必须是context, entity或struct object。

E的类型是这样和CLR的类型对应的:

E Type CLR Type Primitive Type 如前面所述 Complex or Enum Type 元编译生成的class或enum Collection Type IEnumerable<T> Entity Reference Type PowerLanguages.E.EntityKey Anonymous Type 元编译自动生成一个伪匿名类

例:

var x = this.$({A=1, B=EBusiness.Products, C=(from i in EBusiness.Contacts select i.ref)});

元编译生成:

var x = ((global::System.Func<object, global::Example.ProjectA.__AnonymousType_3>)((__arg_0) =>
{
    throw new global::System.NotImplementedException();
}
))(this);
...
...
public class __AnonymousType_3
{
    public int ? A
    {
        get;
        private set;
    }
    public global::System.Collections.Generic.IEnumerable<global::Example.ProjectA.Product> B
    {
        get;
        private set;
    }
    public global::System.Collections.Generic.IEnumerable<global::PowerLanguages.E.EntityKey> C
    {
        get;
        private set;
    }
    public __AnonymousType_3(int ? A__, global::System.Collections.Generic.IEnumerable<global::Example.ProjectA.Product> B__, global::System.Collections.Generic.IEnumerable<global::PowerLanguages.E.EntityKey> C__)
    {
        A = A__;
        B = B__;
        C = C__;
    }
}

可以显式为伪匿名类指定一个名字。例:

public MyQueryResult Test(){
  return this.$(#MyQueryResult{A=1, B=EBusiness.Products, C=(from i in EBusiness.Contacts select i.ref)});
}

则C# class name __AnonymousType_3将变成MyQueryResult,其余的不变。

所有的CLR值类型都是nullable的,因为数据库里的所有数据类型都是nullable的。

嵌入到E表达式中的C#表达式#(…)的类型必须是对应的E的scalar类型,E表达式和C#表达式可以相互嵌套。#(…)只能用在lambda query中,不能用在$function中。

Composable store function可以在lambda query中被调用,首先import store,必须取个alias。例:

$importstore com.example.projecta.store as store;

然后通过qualified name形式调用store function:

public bool IsContactNameExists(string name){
  return (bool)this.$(store:IsContactNameExists(#(name)));
}

Data provider可能定义了自己的function,比如SQL Server。在lambda query中可以调用这些function。首先import provider,必须取个alias。例:

$importprovider "System.Data.SqlClient" "2008" as sql;

然后通过qualified name形式调用provider function:

var hostName = this.$(sql:HOST_NAME());

本文后面列出所有SQL Server provider function的原型。

注意,store/provider function只能在lambda query中调用,不能在$function中调用,我大致能理解微软这么做的原因,因为EDM是store/provider不可知的,但我觉得这限制过于严厉了。

Change Tracking

Lambda query的结果可能包含complex object,这意味着complex type必须拥有无参构造函数(public与否不重要),引擎才能new出complex object,为其property赋值。entity object可以被添加进context,实现change tracking。struct type总是附属于entity type的,struct object被context间接tracking。

Context通过EntityKey辨析entity object。元编译会为每个entity type生成EntityKey_ C# property:

public global::PowerLanguages.E.EntityKey EntityKey_ {get{...}}

EntityKey标识了该entity object在哪个context的哪个entity set中,其key property的值。

元编译会为每个property生成current value C# property,original value C# property,以及IsModified C# property:

public <PropertyType> <PropertyName> {get{...} set{...}}//current value
public <PropertyType> <PropertyName>_Original {get{...} set{...}}//如果property的类型是struct,则无此属性
public bool <PropertyName>_IsModified {get{...} set{...}}//struct type的property无此属性

Lambda query的表达式后有一个可选的MergeOption,对应于下面的枚举:

public enum MergeOption {
    AppendOnly = 0,
    OverwriteChanges = 1,
    PreserveChanges = 2,
    NoTracking = 3,
}

Merge Option Context对query生成的entity object的行为 AppendOnly(缺省值) 如果context中存在相同的EntityKey,则丢弃query出来的结果,否则将其添加进context OverwriteChanges 如果context中存在相同的EntityKey,则将其所有property的current value与original value赋为query生成的entity object的相应的值,且对象状态是Unchanged,否则将其添加进context PreserveChanges 如果context中存在相同的EntityKey,则将其所有property的original value赋为query生成的entity object的相应的值,否则将其添加进context NoTracking 顾名思义

元编译会为每个entity type生成EntityState_属性:

public global::PowerLanguages.E.EntityState EntityState_ {get{...} set{...}}

[Flags]
public enum EntityState {
    Detached = 1,
    Unchanged = 2,
    Added = 4,
    Deleted = 8,
    Modified = 16,
}

引擎new出entity object后,会为所有的current value赋值,如果entity object被添加进context,则EntityState_值为Unchanged,且original value的值和current value的值相同,否则为Detached。如果用户修改了某属性的current value,则相应的IsModified变为true,且EntityState_的变为Modified,如果把IsModified设为false,则引擎丢弃对current value的更改,将其还原成original value。用户也可以自己new出一个entity object,添加到context中,其EntityState_值为Added,从context删除一个entity object,它的EntityState_值变为Deleted。注意,只有当EntityState_为Unchanged或Modified时,才能访问属性的original value和IsModified,否则会抛出异常。

元编译会为每个complex type生成一个complex type bag:

[global::System.SerializableAttribute()]
[global::System.Runtime.Serialization.DataContractAttribute(Namespace = @"...")]
public partial class <ComplexTypeName>_Bag : global::PowerLanguages.E.Bag
{
    public <ComplexTypeName>_Bag() {...}
    public <ComplexTypeName>_Bag DeepClone_() {...}
    public <PropertyType> <PropertyName> { get {...} set {...} }
}

public abstract class Bag : IDictionary<string, object> {
    public bool? UseCurrentValues_ { get { ... } }
    public object GetValue_(string name) {...}
    public void SetValue_(string name, object value) {...}
    public void FillFrom_(object sourceObject, FillBagOptions options = FillBagOptions.None, bool publicPropertiesOnly = true) {...}
    public void FillTo_(object targetObject, FillBagOptions options = FillBagOptions.None, bool publicPropertiesOnly = true) {...}
    ...
}

Complex type bag包含complex type所有property的值。如同complex type,complex type bag也是可以序列化的。PowerLanguages.E.Bag实现了IDictionary<string, object>,TKey为complex type的property name,TValue是complex type的property value。若UseCurrentValues_值为true,则该bag连接到complex object的current values值上;若UseCurrentValues_值为false,则该bag连接到complex object的original values值上;若UseCurrentValues_值为null,则该bag不与comple object相连(disconnected bag),自己独立维护属性值。Complex type bag有一个public无参构造函数,用它将创建出disconnected bag,调用DeepClone_()也将创建出disconnected bag,对disconnected bag的struct bag property赋值,值将被深拷贝。

元编译为每个complex type生成两个属性以访问current values bag和original values bag:

public <ComplexTypeName>_Bag Bag_ {get{...}}//current values bag
public <ComplexTypeName>_Bag OriginalBag_ {get{...}}//original values bag

如果一个bag连接到complex object上(UseCurrentValues_值不为null),则对bag的属性赋值的结果是对相应complex object的属性赋值,同理,bag的属性将返回complex object的属性的即时值。

FillFrom_和FillTo_,顾名思义。注意,sourceObject和targetObject参数的类型是object,这意味着它们可以是非bag,有三种情况:Fill bag from bag,Fill bag from CLR object,Fill CLR object from bag。不管是bag还是CLR object,都只对C# property进行操作,要将一个属性的值赋给另一个属性,首先它们的名字要相同,其次类型要兼容。若publicPropertiesOnly参数值为true,则只考虑CLR object的public property,否则考虑CLR object的所有property。options参数的值可以是FillBagOptions.IgnoreKey,顾名思义,忽略对entity type的key property赋值。

Serialization

分布式应用通常是无状态的,这对context的change tracking功能是“致命打击”,因为change tracking功能需要context sessionful的存在,而分布式应用为了可伸缩性通常是随建随毁context。还有, 分布式应用通常使用WCF,WCF通常使用DataContract这一序列化方法,WCF client通过service的WSDL生成的complex type的“等价类”,service side的complex type和cilent side的“等价类”是具有相同数据契约的不同CLR type。在Entity Framework中对entity object进行离线式更新相对麻烦。

在PowerLanguages.E中,Complex type和complex type bag支持DataContract和Serializable这两种序列化方法,entity type会自动序列化和反序列化所有的property和navigation property,struct type和bag会自动自动序列化和反序列化所有的property。如果一个complex type不仅序列化current values,还序列化original values bag,即,client side得到一个能self-tracking的数据契约。下面把Contact及其派生类实现成self-trackingable:

$entity Contact[abstract]
{
    ...
    internal bool SerializeOriginalBag {get; set;}
    [DataMember(Name = "__OriginalBag")]
    private Contact_Bag _originalBagSerialization;
    partial void OnSerializing_(StreamingContext context){
        if(SerializeOriginalBag) _originalBagSerialization = OriginalBag_;
    }
    partial void OnSerialized_(StreamingContext context){
        _originalBagSerialization = null;
    }
    partial void OnDeserialized_(StreamingContext context){ }
    internal void ApplyOriginalBagSerialization(){
        OriginalBag_.FillFrom_(_originalBagSerialization);
        _originalBagSerialization = null;
    }
}

只需要在基类中实现即可。Service side在序列化之前,设置SerializeOriginalBag为true,序列化后,client side将得到这样的数据:

Client side将XML数据反序列化成自己的数据契约类,修改部分property,然后序列化,将数据回传给service side,service side反序列化XML数据成enity object,注意,不能在OnDeserialized_中为OriginalBag_进行fill,因为这时EntityState_为Detached,对象未被context track,将对象attach进context,然后调用ApplyOriginalBagSerialization,则对象的current values是client修改后的值,original values是query时数据库中的值。

显而易见的是,反序列化出来的entity object是Detached,而bag是disconnected的。

Change Tracking续

元编译为context生成:

public partial class <ContextName> : global::PowerLanguages.E.IContextObject
{
    public void Initialize_() {...}
    void global::System.IDisposable.Dispose() {...}
    public int SaveChanges_(bool acceptAllChangesAfterSave = true) {...}
    public void AcceptAllChanges_() {...}
    public global::PowerLanguages.E.IEntitySet<<EntityTypeName>> <EntitySetName> { get {...} }
    ...
}

public interface IEntitySet<TEntityObject> : IList<TEntityObject> where TEntityObject : class, IEntityObject {
    void Attach(TEntityObject entity);
    void Detach(TEntityObject entity);
    ...
}

使用context前,需要Initialize_,通常在构造函数中调用Initialize_,用完后,进行Dispose。

如果query出来的entity object被context tracking,则会被自动添加到相应的entity set中。一个隐约的陷阱是,只有对query的结果进行枚举,context才会真正的对entity object进行tracking。例:

public void Test(string name){
    var products = this.$(from p in EBusiness.Products where p.Name.islike(#(name)) select p);
    //这时,Products entity set中无任何值
    products.EnumerateAll();//EnumerateAll()是个extension method,简单的枚举IEnumerable<>
    //这时,Products entity set中将包含query生成的对象
}

在Entity Framework中,当你枚举一个EF中的entity set时,EF会自动向数据库发出一个query,请求所有的数据。在PowerLanguages.E中,entity set要么包含显式query出来的结果,要么是用户显式添加的新对象,没有自动行为。

回到上面离线式更新entity object的问题上,当反序列化出entity object后,它的状态是Detached,调用IEntitySet.Attach将它添加进context,则它的状态变为Unchanged,这时对象的current values和original values相同,都是client(可能)修改过的值,这时调用ApplyOriginalBagSerialization,则对象的original values变成query时数据库中的值,若current value和original value不等,则该IsModified返回true,当向数据库提交更新时,只有被修改过的属性将被提交到数据库。具体的示例请参见EBusiness示例中的ModifyCustomer()方法。

对于navigation property,如果对方end的重数是many,元编译生成:

public global::PowerLanguages.E.IEntityCollection<<ToEntityTypeName>> <NavigationPropertyName> { get {...} }

public interface IEntityCollection<TEntityObject> : ICollection<TEntityObject>, IEntityNavigation where TEntityObject : class, IEntityObject {
    void Attach(TEntityObject entity);
    ....
}

否则生成:

public <<ToEntityTypeName>> <NavigationPropertyName> { get {...} set {...} }
public global::PowerLanguages.E.IEntityReference<<ToEntityTypeName>> <NavigationPropertyName>_Reference { get {...} }

public interface IEntityReference<TEntityObject> : IEntityNavigation where TEntityObject : class, IEntityObject {
    TEntityObject Entity { get; set; }
    void Attach(TEntityObject entity);
    ....
}

要添加数据,首先new出entity object,可以通过Add方法添加到entity set中,也可以通过Add方法添加到navigation property的IEntityCollection中,或设置navigation property的值。对navigation property进行操作不仅将对象添加进context,还建立了这两个对象间的联系。

要删除数据,调用entity set的Remove方法。调用IEntityCollection的Remove或将navigation property设为null只会取消这两个对象间的联系,不会将对象从context中删除。

IEntityCollection和IEntityReference的Attach方法attach两个对象间的联系,前提是这两个对象已经存在于context,且外键约束有效。

在PowerLanguages.E中,除了many-to-many的联合,其它联合必须设置外键。设置外键也能建立或取消entity object间的联系,在离线式更新的情况下更方便。

在Entity Framework中,默认设置下(LazyLoadingEnabled=true),枚举或访问navigation property将自动向数据库query相应的数据,PowerLanguages.E没有这自动行为,一切都要显式的query。

要向数据库提交更新,使用SaveChanges_方法:

try{ ctx.SaveChanges_(); }
catch(PowerLanguages.E.OptimisticConcurrencyException ex) {...}
catch(PowerLanguages.E.UpdateException ex) {...}

OptimisticConcurrencyException继承自UpdateException,其InnerException将包含实现的异常,在这里是Entity Framework的同名异常。

如果更新成功,调用AcceptAllChanges_方法,对context中所有entity object,如果对象状态是Added或Modified,变为Unchanged,且将current values赋值给original values。如果是Deleted,变为Detached。

如果key property为identity,那么提交更新后,数据库将自动生成一个key值,并反向赋值给对象的property。如果property是computed,那么提交更新后,数据库将自动生成一个值,并反向赋值给对象的property。

元编译为function import生成C# method,如果参数方向为out或inout,则C#的参数类型是out Func<T>。例:

public IEnumerable<EBusiness_GetAmountAndOrders_Result> GetAmountAndOrders(int? customerId, out Func<decimal?> amount_Out, MergeOption mergeOption_ = MergeOption.AppendOnly) {...}

需要out Func<T>的原因是,只有把存储过程返回的result set全部枚举后,出参数才会被赋值:

static void DisplayAmountAndOrders(int customerId)
{
  using (var ctx = new EBusiness())
  {
    Func<decimal?> amount;
    var results = ctx.GetAmountAndOrders(customerId, out amount);
    Log("Amount={0}", amount());//null
    foreach (var i in results)
      Log("Id={0}, OrderDate={1}", i.Id, i.OrderDate);
    Log("Amount={0}", amount());
  }
}

Optimistic Concurrency

实现乐观并发很简单。在entity type中添加这样一个property:

$property ConcurrencyStamp[concurrencystamp; computed] as Binary[notnull];

它将映射到数据库的rowversion列。在提交更新时,ConcurrencyStamp的original value将作为SQL where clause的一个条件,如果数据库返回零条结果,说明在query数据与提交数据间,有人已经修改了该数据,那么将抛出PowerLanguages.E.OptimisticConcurrencyException,如果返回非零条结果,表明更新成功,则新产生的rowversion值将反向赋值给entity object,以便下次更新。

Validation

Defaultvalue facet对类型进行注解。元编译为每个complex type生成:

public void SetScalarPropertyValueToDefault_(string propertyName) {...}
public void SetAllScalarPropertyValuesToDefault_() {...}

如果某property存在defaultvalue facet,则将它的值设为指定的值,否则无操作。

对于NotNull, Length, LengthRange, ValueRange, Precision, Scale, Pattern, Enumerations facet,它们对type进行约束,可以对property的值进行验证,以确认是否满足约束。元编译为每个complex type生成:

public bool Validate_(global::PowerLanguages.E.ValidationResultList resultList, bool validateChildStructs = true, global::System.Collections.Generic.IDictionary<object, object> args = null) {...}

调用Validate_对complex object进行验证,例:

var validationResultList = new ValidationResultList();
if (!<complexObject>.Validate_(validationResultList))
{
    foreach (var i in validationResultList)
        Console.WriteLine(i.ErrorMessage);
}

元编译为context生成:

public bool Validate_(ValidationResultList resultList, Func<IEntityobject, bool> entityFilter = null, EntityState entityState = EntityState.Added | EntityState.Modified, bool validateChildStructs = true, IDictionary< object, object> args = null){...}
public ValidationResultList Validate_(Func<IEntityobject, bool> entityFilter = null, EntityState entityState = EntityState.Added | EntityState.Modified, bool validateChildStructs = true, IDictionary<object, object> args = null){...}

调用它们对context中所有entity object进行验证。

PowerLanguages.E使用System.ComponentModel.DataAnnotations的验证机制,这意味着你可以自由扩展它。

Metadata and Reflection

元数据描述了Entity Data Model,元数据的数据类型以Info结尾,元数据的入口是global::EProgramInfo_.Instance。通过元数据,程序具有自省能力,可以实现一些高级功能。

EStore Guide

语法:

CompilationUnit:
  Namespace*
Namespace:
  'store' NamespaceName '[' (NamespaceAttribute ';')+ ']' '{' NamespaceMember* '}'
NamespaceAttribute:
  Provider | Schema
Provider:
  'provider' ':' ProviderName ProviderVersion
Schema:
 'schema' ':' Name
NamespaceMember:
  Table | Function
//
//
Table:
  'table' TableName ('[' (TableAttribute ';')* ']')? '{' (Column ',')+ '}'
TableAttribute:
  Schema | NameInDb | ForeignKeys
NameInDb:
  'nameindb' ':' Name
ForeignKeys:
  'foreignkeys' ':' ((ColumnName ',')+ 'ref' TableName 'cascadedelete'? ',')+
Column:
  ColumnName ('[' (ColumnAttribute ';')* ']')? 'as' ProviderType ('[' (TypeFacet ';')* ']')?
ColumnAttribute:
  Key | ValueGenerationMode
//
//
ProviderType:
  ProviderTypeName ('(' MaxLengthOrPrecision (',' Scale)? ')')?
TypeFacet:
  NotNull | DefaultValue | Collation
DefaultValue:
  'defaultvalue' ':' PrimitiveTypeLiteral
//
//
Function:
  'function' FunctionName ('[' (FunctionAttribute ';')* ']')? '(' (Parameter ',')* ')' ('as' (ProviderType | FunctionComplexResult+))? ';'
FunctionAttribute:
  Schema | NameInDb | Composable
Parameter:
  ParameterName ('[' (ParameterAttribute ';')* ']')? 'as' ProviderType
ParameterAttribute:
  ParameterDirection
FunctionComplexResult:
  '{' (FunctionComplexResultMember ',')+ '}' '*'
FunctionComplexResultMember:
  MemberName 'as' ProviderType

EStore定义了store model,对关系数据库的部分对象进行简单的抽象描述,它实现了SSDL的语意。

EStore实现了与C#相同的preprocessing directives,具体的,你可以像C#中那样使用#define, #undef, #if, #elif, #else, #endif, #region 和 #endregion。当然,也实现了与C#相同的注释。

Namespace name是不分层次的dotted identifier,且Store namespace name不能和E namespace name重复。EStore是data provider不可知的,通过provider attribute指定具体的data provider。

对于SQL Server,provider name是"System.Data.SqlClient",version是"2008", "2005" 或"2000"。

对于Oracle,provider name是 "Oracle.DataAccess.Client",version是"12.2", "12.1", "11.2", "11.1", "10.2", "10.1"或"9.2"。你需要到Oracle网站下载最新版本的ODP.NET并安装,才能正确的编译。

例:

store com.example.projecta.store[provider: "System.Data.SqlClient" "2008"] {...}

Schema attribute指定EStore Table或Function在数据库中的schema,若Table或Function没指定,则使用Namespace中指定的值,若Namespace中也没指定,则默认是dbo。

NameInDb attribute指定EStore Table或Function在数据库中的名字,若没指定,则和EStore中的名字相同。

Table

Table对数据库的table或view进行描述。即便数据库的table/view是可写的,都可以把EStore的table当作是只读的,若要修改数据,那么需要在数据库中定义Add/Update/Delete存储过程,并在EStore中用Function描述。

Column attribute的含义和E中相同。

不同的provider提供了不同的数据类型,SQL Server的数据类型见此页面,Oracle的数据类型见此页面。需要强调的是,provider的数据类型与EDM的primitive类型具有等价关系,具体的对应见各自的页面。

Type facet的含义和E中相同。

例:

store com.example.projecta.store[provider: "System.Data.SqlClient" "2008"; schema: abc]
{
  table T1
  {
    Id[key; identity] as int[notnull],
    C1 as nvarchar(20)[notnull],
    C2 as varbinary(max)[notnull],
    C3 as decimal(18, 2)[defaultvalue: 9.99M]
  }
  table T2[schema: dbo; nameindb: TT2; foreignkeys: T1Id ref T1 cascadedelete]
  {
    Id[key; identity] as int[notnull],
    T1Id as int[notnull],
    C2[computed] as rowversion[notnull],
  }
}

因为T1的shcema值是abc,T2在数据库中的名字是TT2,T1与T2之间存在外键约束。

Function

Function对数据库中的stored procedure或user-defined function进行描述。Composable attribute的意思是,该函数的调用可以直接作为另一个函数的函数调用的argument,user-defined function是composable的,stored procedure不是。只有non-composable(即stored procedure)的函数的参数的方向可以指定为out或inout。对于user-defined function,它可以返回一个scalar值,也可以返回table值;对于stored procedure,它可以不返回值,也可以返回一到多个table值(single or multiple result set)。例:

function IsContactNameExists[composable](name as nvarchar(20)) as bit;
function GetOrders[composable](customerId as int) as {Id as int, OrderDate as datetimeoffset, ShippingCountry as nvarchar(20)}*;
function GetAmountAndOrders(customerId as int, amount[out] as decimal(18, 2)) as {Id as int, OrderDate as datetimeoffset}*;
function GetContactsFrom(country as nvarchar(20)) as {Id as int, Name as nvarchar(20), Reputation as smallint}*, {Id as int, Name as nvarchar(20), BankAccount as nvarchar(20)}*;
function DeleteContact(Id as int);

IsContactNameExists是返回scalar值的UDF;GetOrders是返回table值的UDF;GetAmountAndOrders是返回table值(single result set)的SP,它有一个出参数;GetAmountAndOrders是返回多table值(multiple result sets)的SP;DeleteContact是无返回值的SP。

EMapping Guide

语法:

CompilationUnit:
  ContextMapping*
ContextMapping:
  'mapping' ContextName 'to' StoreNamespaceName '{' NamespaceImport* ContextMappingMember* '}'
NamespaceImport:
  'import' ENamespaceName 'as' Alias ';'
ContextMappingMember:
  EntitySetMapping | AssociationSetMapping | FunctionImportMapping
EntitySetMapping:
  'entityset' EntitySetName ('[' (EntitySetMappingAttribute ';')* ']')? EntityTypeMapping+ ';'
EntitySetMappingAttribute:
  EntitySetMappingMode
EntitySetMappingMode:
  'tph' | 'tpt' | 'tpc'
EntityTypeMapping:
  ('type' EntityTypeQualifiableName)? Fragment+ ('insert' ModificationFunctionWithResult)? ('update' ModificationFunctionWithResult)? ('delete' ModificationFunction)?
Fragment:
  'table' StoreTableName ('if' (Condition '&&')+)? PropertyMappingBlock
Condition:
  ColumnName ('isnull' | '==' PrimitiveTypeLiteral)
PropertyMappingBlock:
  'auto'? '{' (PropertyMapping ',')* '}'
ScalarPropertyMappingBlock:
  'auto'? '{' (ScalarPropertyMapping ',')* '}'
PropertyMapping:
  ScalarPropertyMapping | StructPropertyMapping
ScalarPropertyMapping:
  ScalarPropertyName ('[' (ScalarPropertyMappingAttribute ';')* ']')? 'to' StoreTableColumnOrFunctionParameterOrResultMemberName
ScalarPropertyMappingAttribute:
  OriginalValue
OriginalValue:
  'original'
StructPropertyMapping:
  StructPropertyName 'auto'? '{' (PropertyMapping ',')* '}'
ModificationFunction:
  StoreFunctionName ('[' (ModificationFunctionAttribute ';')* ']')? PropertyMappingBlock
ModificationFunctionAttribute:
  RowsAffectedParameter
RowsAffectedParameter:
  'rowsaffectedparameter' ':' StoreParameterName
ModificationFunctionWithResult:
  StoreFunctionName ('[' (ModificationFunctionAttribute ';')* ']')? PropertyMappingBlock ('as' ScalarPropertyMappingBlock)?
AssociationSetMapping:
  'association' AssociationSetName 'table' StoreTableName AssociationSetEndMapping AssociationSetEndMapping ';'
AssociationSetEndMapping:
  'end' AssociationSetEndName ScalarPropertyMappingBlock
FunctionImportMapping:
  'functionimport' FunctionImportName 'function' StoreFunctionName ('as' (ScalarPropertyMappingBlock ',')+ ) ';'

EMapping建立conceptual model与store model的映射,它实现了MSL的语意。

EMapping实现了与C#相同的preprocessing directives,具体的,你可以像C#中那样使用#define, #undef, #if, #elif, #else, #endif, #region 和 #endregion。当然,也实现了与C#相同的注释。

需要把conceptual model的context及其成员entity set, function import, association set映射到store model的元素。

最外层是context到store namespace的映射。例:

mapping EBusiness to com.example.projecta.store {...}

Entity Set Mapping

Entity set是entity type实例的集合。如果entity type没有继承层次,这是最简单的:

mapping EBusiness to com.example.projecta.store
{
  entityset OrderDetails table OrderDetails auto{};
}

上例将OrderDetail entity type映射到OrderDetails table。

一个entity type也可以映射到多个表。例:

//.ple
$entity E1
{
  $property Id[key; identity] as Int32[notnull];
  $property P1 as String[notnull];
  $property P2 as Binary;
}
$context EBusiness
{
  $entityset E1s of E1;
}
//.ples
table E1s
{
  Id[key; identity] as int[notnull],
  P1 as nvarchar(max)[notnull]
}
table E1Xs[foreignkeys: Id ref E1s cascadedelete]
{
  Id[key] as int[notnull],
  P2 as varbinary(max)
}
//.plem
entityset E1s table E1s auto{} table E1Xs auto{};

上例中,Id key property映射到E1s table和E1Xs table的Id key column,P1 property映射到E1s table的P1 column,P2 property映射到E1Xs table的P2 column。注意,key property必须映射到每个table的key column。每个property必须映射,每个non-nullable column必须映射,除非它有default value。表E1Xs到E1s的外键约束并不是必须的,加上更能强调逻辑。

每个provider type都对应一个等价的EDM primitive type,property和column要映射成功,则它们的primitive type必须相同。

上面的例子使用了property自动映射,如果property的名字和column名字相同,且指定了auto关键字,编译器将自动映射。下面使用手工映射:

entityset E1s table E1s {Id to Id, P1 to P1} table E1Xs {Id to Id, P2 to P2};

如果property的名字和column名字不同,则必须手工映射,假设E1s table的P1 column改名为PP1:

entityset E1s table E1s auto{P1 to PP1} table E1Xs auto{};

但仍可以使用auto关键字,这样只需要手工映射不同名字的部分。

如果property的类型是struct:

//.ple
$entity E1
{
  $property Id[key; identity] as Int32[notnull];
  $property P1 as String[notnull];
  $property P2 as Binary;
  $property P3 as S1[notnull];
}
$struct S1
{
  $property P1 as String[notnull];
  $property P2 as Int32[notnull];
}
//.ples
table E1s
{
  Id[key; identity] as int[notnull],
  P1 as nvarchar(max)[notnull],
  S1P1 as nvarchar(max)[notnull],
  S1P2 as int[notnull]
}
//.plem
entityset E1s table E1s auto{P3{P1 to S1P1, P2 to S1P2}} table E1Xs auto{};

Struct只是conceptual model的“语法糖”,在数据库中,数据是平坦的。

如果Entity set的entity type有继承层次,则存在三种映射模式:TPH, TPT, TPC:

Table per Hierarchy (TPH):继承层次中所有类都映射到一个表,该表包含继承层次中的所有数据,表中有一个discriminator column来区分不同的类。

Table per Type (TPT):继承层次中每个类映射到各自的表,每个表只包含该类的数据,不包含基类的数据,通过join本表和基类的表得到完整数据。

Table per Concrete type (TPC):继承层次中每个具体类映射到各自的表,每个表不仅包含该类的数据,也包含基类的数据。TPC存在固有缺陷,很少使用。

在EBusiness示例中,Contact是个抽象基类,它有两个具体的派生类,Customer和Supplier。

$entity Contact[abstract]
{
  $property Id[key; identity] as Int32[notnull];
  $property Name as String[notnull; lengthrange: 1..20];
  ...
}
$entity Customer : Contact
{
  $property Reputation as Reputation[notnull];
  ...
}
$entity Supplier : Contact
{
  $property BankAccount as String[notnull; lengthrange: 1..20];
  ...
}

要对Contact继承层次使用TPH映射,则数据库的表将包含继承层次中的所有数据,且有一个discriminator column。

table Contacts
{
  Id[key; identity] as int[notnull],
  Name as nvarchar(20)[notnull],
  ...
  Type as nvarchar(20)[notnull],
  Reputation as smallint,
  BankAccount as nvarchar(20)
}

Type column是discriminator,如果它的值是"Customer",则该行数据是Customer,若它的值是"Supplier",则该行数据是Supplier。注意Reputation和BankAccount列,它们是nullable的,这是显而易见的。最后指定映射:

entityset Contacts[tph]
  type Contact table Contacts auto{Address auto{}}
  type Customer table Contacts if Type == "Customer" auto{}
  type Supplier table Contacts if Type == "Supplier" auto{}
;

首先指明使用tph映射,然后指定继承层次中的每个类,它们都映射到Contacts表,对于每个具体类,需要指定映射条件,即当discriminator column为何值时,该行数据代表该类。

TPH映射有一个微妙之处,abstarct type只能是继承层次的根,不能处于层次的中间,否则运行时会出错,只有microsoft的guy才能解释清楚为什么。TPT没有此限制。

在EBusiness示例中,Product是个抽象基类,它有多个派生类:

$entity Product[abstract]
{
  $property Id[key; identity] as Int32[notnull];
  $property ConcurrencyStamp[concurrencystamp; computed] as Binary[notnull];
  $property Name as String[notnull; lengthrange: 1..20];
  ...
}
$entity Clothing[abstract] : Product
{
  ...
}
$entity TShirt : Clothing
{
  ...
}
$entity Outerwear : Clothing
{
  ...
}
$entity Shoe[abstract] : Product
{
  ...
}
$entity OutdoorShoe : Shoe
{
  ...
}
$entity SuperOutdoorShoe : OutdoorShoe
{
  ...
}
$entity Boot : Shoe
{
  ...
}

要对Product继承层次使用TPT映射,则需要在数据库为层次中的每个类创建一个表,表中只包含该类的数据以及相同的主键: 

table Products
{
  Id[key; identity] as int[notnull],
  ConcurrencyStamp[computed] as rowversion[notnull],
  Name as nvarchar(20)[notnull],
  ...
}
table Clothings[foreignkeys: Id ref Products cascadedelete]
{
  Id[key] as int[notnull],
  ...
}
table TShirts[foreignkeys: Id ref Products cascadedelete]
{
  Id[key] as int[notnull],
  ...
}
table Outerwears[foreignkeys: Id ref Products cascadedelete]
{
  Id[key] as int[notnull],
  ...
}
table Shoes[foreignkeys: Id ref Products cascadedelete]
{
  Id[key] as int[notnull],
  ...
}
table OutdoorShoes[foreignkeys: Id ref Products cascadedelete]
{
  Id[key] as int[notnull],
  ...
}
table SuperOutdoorShoes[foreignkeys: Id ref Products cascadedelete]
{
  Id[key] as int[notnull],
  ...
}
table Boots[foreignkeys: Id ref Products cascadedelete]
{
  Id[key] as int[notnull],
  ...
}

实际上,表之间的外键约束并不是必须的,加上更能强调逻辑。最后指定映射:

entityset Products[tpt]
  type Product table Products auto{}
  type Clothing table Clothings auto{}
  type TShirt table TShirts auto{}
  type Outerwear table Outerwears auto{}
  type Shoe table Shoes auto{}
  type OutdoorShoe table OutdoorShoes auto{}
  type SuperOutdoorShoe
  table SuperOutdoorShoes auto{}
  type Boot table Boots auto{}
;

可以为每个具体的entity type指定insert, update, delete存储过程,这时,store model中的表变成只读,修改数据将通过存储过程,而不是直接对表操作。下面把第一个例子改成使用存储过程,创建下面的SP:

CREATE PROCEDURE [dbo].[InsertE1]
  @P1 as nvarchar(max),
  @P2 as varbinary(max)
AS
  declare @Id as int;
  insert dbo.E1s(P1) values(@P1);
  set @Id = scope_identity();
  insert dbo.E1Xs(Id, P2) values(@Id, @P2);
  select @Id as Id;
go
CREATE PROCEDURE [dbo].[UpdateE1]
  @Id as int,
  @P1 as nvarchar(max),
  @P2 as varbinary(max)
AS
  update dbo.E1s set P1 = @P1 where Id = @Id;
  update dbo.E1Xs set P2 = @P2 where Id = @Id;
go
CREATE PROCEDURE [dbo].[DeleteE1]
  @Id as int
AS
  delete dbo.E1Xs where Id = @Id;
  delete dbo.E1s where Id = @Id;
go

因为E1s表的主键是自增的,插入新数据后,必须将数据库产生的值返回给客户端,通过result set返回,对于rowversion列,insert和update都会改变其值,都需要返回给客户,示例见EBusiness中对Product的添加更新。然后在store model中描述这三个SP:

function InsertE1(P1 as nvarchar(max), P2 as varbinary(max)) as {Id as int}*;
function UpdateE1(Id as int, P1 as nvarchar(max), P2 as varbinary(max));
function DeleteE1(Id as int);

最后建立映射:

entityset E1s table E1s auto{} table E1Xs auto{}
  insert InsertE1 auto{} as auto{}
  update UpdateE1 auto{}
  delete DeleteE1 auto{}
;

在这里,property将映射到SP的参数。InsertE1的result set中的Id member将反向映射到Id property。

如果entity set包含继承层次,则需要为所有的具体类使用存储过程,见EBusiness示例。

Association Set Mapping

如前面所述,对于entity type间many-to-many的逻辑联合,在建立entity object间的物理联合时,必须指定association set name。例:

$context EBusiness
{
  $entityset Contacts of Contact associate Products by Supplier.Products as SuppliersProducts;
  $entityset Products of Product;
  ...
}

在数据库实现中,这需要一个中间表:

table SuppliersProducts[foreignkeys: SupplierId ref Contacts, ProductId ref Products]
{
  SupplierId[key] as int[notnull],
  ProductId[key] as int[notnull]
}

SupplierId和ProductId既是主键也是外键。

需要把conceptual model的association set映射到store model的中间表:

association SuppliersProducts table SuppliersProducts
  end Supplier {Id to SupplierId}
  end Product {Id to ProductId}
;

上面的代码说,把SuppliersProducts association set映射到SuppliersProducts表,对于Supplier end,将Contact entity type的Id key property映射到中间表的SupplierId key column,对于Product end,将Product entity type的Id key property映射到中间表的ProductId key column。

Function Import Mapping

非常简单。例:

functionimport GetAmountAndOrders function GetAmountAndOrders as auto{};
functionimport GetContactsFrom function GetContactsFrom as auto{}, auto{};

 


<script type="text/javascript"><!--google_ad_client = "ca-pub-1944176156128447";/* cnblogs 首页横幅 */google_ad_slot = "5419468456";google_ad_width = 728;google_ad_height = 90;//--></script><script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>