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"> </script><script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>