PowerLanguages.E: An EntityData

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包含三个规范:CSDLSSDLMSL。需要指出的是,语法与语意是两个不同层面的东西,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.pleConceptual Model & Entity Data Metaprogramming Model
EStore.plesStore Model
EMapping.plemMapping 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.csEntity Data Metaprogramming Model支撑库
__EntityFramework.impl.csEntity Framework实现支撑库
<ENamespaceName>.csdl由.ple生成的CSDL
<StoreNamespaceName>.ssdl由.ples生成的SSDL
<ContextName>_to_<StoreNamespaceName>.msl由.plem生成的MSL
<FileName>.ple.meta.csEntity Data Metaprogramming Model
<FileName>.ple.impl.csEntity Data Metaprogramming Model的Entity Framework的实现
Globals__.meta.csEntity Data Metaprogramming Model的全局信息
Globals__.impl.csEntity 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 FormatCanonical String
Byte System.Byte 无。但可以从Int32隐式转换,只要值没超出范围DecimalDigits,去掉开头的0及+号
SByte System.SByte 无。但可以从Int32隐式转换,只要值没超出范围DecimalDigits,去掉开头的0及+号
Int16 System.Int16 无。但可以从Int32隐式转换,只要值没超出范围DecimalDigits,去掉开头的0及+号
Int32 System.Int32 同C#。例:123DecimalDigits,去掉开头的0及+号
Int64 System.Int64 同C#。例:123LDecimalDigits,去掉开头的0及+号
Decimal System.Decimal 同C#。例:123.4MDecimalDigits(.DecimalDigits)?,去掉开头的0及+号
Single System.Single 同C#。例:123.4FDecimalDigits(.DecimalDigits)?,去掉开头的0及+号
Double System.Double 同C#。例:123.4DDecimalDigits(.DecimalDigits)?,去掉开头的0及+号
String System.String 同C#。例:"str", @"str"其值,没有前后的双引号
Boolean System.Boolean true, falsetrue, 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必须显式指明为notnullall 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
scaleDecimal的小数位数。例:
$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 TypeCLR Type
Primitive Type如前面所述
Complex or Enum Type元编译生成的class或enum
Collection TypeIEnumerable<T>
Entity Reference TypePowerLanguages.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 OptionContext对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"> </script><script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值