Astoria (ADO.NET Data Service)客户端操作精粹

[ 2008-08-17 12:30:23 | Author: ccBoy ]
Font Size: Large | Medium | Small
呵呵,这篇文章中,我尝试将ADO.NET Data Service和Entity Framework的一些常用标准操作体验记录下来,这基本上是Astoria开发人员必须的操作和掌握的。

使用了数据库很简单,如下图

ͼƬСŴ


作者(Author)和他著作的书(Book),可以说是一个一对多,更确切的说是父-子关系。从数据库来看,Book表保留作者的ID作为外键。
因为测试练习,所以Book ID和Author ID都没有设置成自增的字段,由自己来维护。
如果你仔细看,你会发现上面Entity Framework 产生的E/R模型图,和之前我的一篇Weblog-Astoria to SQLite to Entity Framework to 建立你的ORM观念中的不同?Author ID消失了(呵呵这就是ORMapping的好处),Entity Framework屏蔽和封装了Book表中Author ID属性,从而让客户端或用户看起来也更加面向对象。
数据库定义很简单,如果需要你可以在这里下载

基本上,我们可以从这个模型中枚举出有下面9种操作:
1. 新增一个作者和他的一本书。
2. 新增一个作者,但不增加书。
3. 给一个给定的作者,增加一本书的记录
4. 修改作者的基本信息,并更新,但未修改作者和书的关联关系
5. 修改书的基本信息,并更新;但未修改作者和书的关联关系
6. 改变一个已关联的作者和书的关系,将书关联到另外一个作者。
7. 删除给定作者的一本书。
8. 删除一个作者,目前作者没有创作任何一本书。
9. 删除一个作者,也包括他创作了所有书的记录。

下面就是我使用System.Data.Services.Client (.NET Client) 和一个Entity Framework Context 以及AstoriaHosting在控制台中的Astoria(ADO.NET Data Service)服务,具体的操作句法和尝试,当然比较重要的是后面的总结部分,可以说在目前资源非常困乏的情况下,这些体验,代码和资料对大家会非常珍贵和有帮忙,所以我起名叫“精粹”
  • 1.新增一个作者和他的一本书

  • Author au = new Author()
    {
    ID = 7,
    FirstName = "Charles",
    LastName = "Petzold"
    };
    _context.AddToAuthor(au);

    Book newbook = new Book()
    {
    ID = 8 ,
    Price = 50,
    Title = "3D Programming for Windows" ,

    };
    newbook.Author = au;

    _context.AddToBook(newbook);
    _context.SetLink(newbook, "Author", newbook.Author);

    DataServiceResponse ret = _context.SaveChanges(SaveChangesOptions.Batch);

  • 2.加一个新的作者

  • Author au9 = new Author()
    {
    ID = 9,
    FirstName = "FirstName 9",
    LastName = "LastName 9 "
    };

    _context.AddToAuthor(au9);

    try
    {
    DataServiceResponse ret = _context.SaveChanges();
    }

  • 3.给已有的作者新增加一本书

  • Author au9 = _context.Author.Where(a1 => a1.ID == 9).First();
    Console.WriteLine("Author ID:{0}, FirstName+Last Name:{1},{2}", au9.ID, au9.FirstName, au9.LastName);

    Book book9 = new Book()
    {
    ID = 10,
    Title = "book 10 of Author 9" ,
    Price = 50.0M
    };

    book9.Author = au9;

    _context.AddToBook(book9);
    _context.SetLink(book9, "Author", book9.Author);

    try
    {
    DataServiceResponse ret = _context.SaveChanges();
    if (ret != null)
    UpateResponseDump(ret);
    }

  • 4.更新作者的属性,未改变作者和书的关系

  • Author au9 = _context.Author.Where(a1 => a1.ID == 9).First();
    Console.WriteLine("Author ID:{0}, FirstName+Last Name:{1},{2}", au9.ID, au9.FirstName, au9.LastName);

    au9.FirstName = "FirstName9-Change";
    au9.LastName = "LastName9-Change";

    _context.MergeOption = MergeOption.OverwriteChanges;

    _context.UpdateObject(au9);
    try
    {
    DataServiceResponse ret = _context.SaveChanges();
    if (ret != null)
    UpateResponseDump(ret);
    }

  • 5.更新某本书的属性,未改变作者和书的关系

  • Book book10 = _context.Book.Where(b1 => b1.ID == 10).First();
    Console.WriteLine("Book ID:{0}, Titel:{1}, Price:{2}", book10.ID, book10.Title, book10.Price);

    book10.Title = "Book10 Title-Change";
    book10.Price = 10.0M;

    _context.MergeOption = MergeOption.OverwriteChanges;

    _context.UpdateObject(book10);

    try
    {
    DataServiceResponse ret = _context.SaveChanges();
    if (ret != null)
    UpateResponseDump(ret);
    }

  • 6. 修改作者和书之间的所属关系

  • Author au6 = _context.Author.Where(a1 => a1.ID == 6).First();
    Console.WriteLine("Author ID:{0}, FirstName+Last Name:{1},{2}", au6.ID, au6.FirstName, au6.LastName);

    Book book10 = _context.Book.Where(b1 => b1.ID == 10).First();
    Console.WriteLine("Book ID:{0}, Titel:{1}, Price:{2}", book10.ID, book10.Title, book10.Price);

    _context.MergeOption = MergeOption.OverwriteChanges;

    book10.Author = au6;
    _context.UpdateObject(book10);
    _context.SetLink(book10, "Author", au6);

    try
    {
    DataServiceResponse ret = _context.SaveChanges();
    if (ret != null)
    UpateResponseDump(ret);
    }

  • 7.删除某个作者其中的一本书

  • // Pre action : insert book #99 to databse
    Book book99 = _context.Book.Where(b1 => b1.ID == 99).First();
    Console.WriteLine("Book ID:{0}, Titel:{1}, Price:{2}", book99.ID, book99.Title, book99.Price);

    _context.LoadProperty(book99, "Author");
    Author a = book99.Author;
    Console.WriteLine("Author ID:{0}, FirstName+Last Name:{1},{2}", a.ID, a.FirstName, a.LastName);

    _context.MergeOption = MergeOption.OverwriteChanges;

    _context.DetachLink(book99, "Author", book99.Author);
    _context.DeleteObject(book99);

    try
    {
    DataServiceResponse ret = _context.SaveChanges();
    if (ret != null)
    UpateResponseDump(ret);
    }

  • 8.-9.删除某个作者,以及包括他写的书

  • Author au9 = _context.Author.Where(b1 => b1.ID == 9).First();
    Console.WriteLine("Author ID:{0}, First-Last Name:{1},{2}", au9.ID, au9.FirstName, au9.LastName);

    _context.MergeOption = MergeOption.OverwriteChanges;

    _context.LoadProperty(au9, "Book");
    foreach (Book item in au9.Book)
    {
    Console.WriteLine("Book ID:{0}, Titel:{1}, Price:{2}", item.ID, item.Title, item.Price);
    _context.DeleteObject(item);
    }

    _context.DetachLink(au9, "Book", au9.Book);

    _context.DeleteObject(au9);

    try
    {
    DataServiceResponse ret = _context.SaveChanges(SaveChangesOptions.Batch);
    if (ret != null)
    UpateResponseDump(ret);
    }

  • 10. 错误处理:

  • if (ret.IsBatchResponse)
    {
    /*inspect HTTP artifacts associated with the entire batch: ret.BatchHeaders, ret.BatchStatusCode*/
    Console.WriteLine("The DataServiceResponse is BatchResponse");

    Console.WriteLine("BatchStatusCode:{0},BatchHeaders-Content-Type:{1}", ret.BatchStatusCode.ToString(), ret.BatchHeaders["Content-Type"]);
    }


    foreach (ChangeOperationResponse cr in ret)
    {
    if (cr.Error != null)
    Console.WriteLine("Error:", cr.Error.InnerException.ToString());
    else
    {
    if (cr.Descriptor is EntityDescriptor)
    {
    EntityDescriptor ed = (EntityDescriptor)cr.Descriptor;
    Console.WriteLine("Entity:{0} - Status:{1}", ed.Entity.ToString(), ed.State.ToString());
    }

    else if (cr.Descriptor is LinkDescriptor)
    {
    LinkDescriptor ld = (LinkDescriptor)cr.Descriptor;
    Console.WriteLine("Link Status:{0}", ld.State.ToString());
    Console.WriteLine("LinkSource:{0}- LinkTarget:{1} by Property:{2}", ld.Source.ToString(), ld.Target.ToString(), ld.SourceProperty);
    }
    }
    }
    }
    catch (DataServiceRequestException dre)
    {
    throw dre;
    }
结论:

1. Elisa Flasko 和 Mike Flasko的文章--在 Web 服务领域公开和使用数据,已成为经典文章,一定要看本篇文章。
我获得收益最大的是文章说的命令映射的概念--使用 HTTP Get 请求映射通过 ADO.NET 数据服务执行的查询后,如何执行 Create、Update 和 Delete 请求?四个 CRUD 操作(Create、Retrieve、Update 和 Delete)中的每个操作都映射到一个不同的 HTTP 动词:Retrieve 映射到 GET,Create 映射到 POST,Update 映射到 PUT,Delete 映射到 DELETE。

2. 这也几乎是我们进行数据操作最重要和关键的要领,无论开发和调试都要把握这个原则,这样REST开发和调试都比较简单,而最终你会喜欢这种方式。

3. 客户端的Context对象,你可以把它想像成离线版本的数据源Entity Framework Context ,它管理客户端所有实体的状态,以及实体之间的关系。也包括实体所有属性的管理(Get/Set),但记住,你在客户端Context的所有操作,真正的数据源并不知道。--真正的变化需要另外一个指令,你可以通过Fiddler这样工具很容易获知。

4. 客户端所有的CUR的操作,只有在调用SaveChanges(),才会将变化传送到真正的数据源。记住我上面说的命令映射概念,一个对象在客户端的一个CUD 操作如果想生效,也必须对应一个HTTP命令字才能生效。如果你想同时生效几个命令,就必须使用SaveChangesOptions.Batch 参数。比如上例中你想增加一个作者和他的一本书,以及你想删除某个作者和他下面的所有的书记录。对于服务端来说不知道作者和书的关系,它只知道命令映射,即一个Delete 操作映射到 HTTP DELETE上,一个Update操作.......,而通知它的方式只有一个,调用SaveChanges方法。
[*]5. SaveChanges 一般会返回一个复杂的结构体,基本上包括两类信息,一类是生效的实体的信息,一类是生效的实体之间的关系的信息。有关实体的信息在EntityDescriptor对象中,而关系信息在LinkDescriptor。其实除非是Batch操作,否则每次你只能获得其中一个信息,即要么是实体,要么是实体之间的关系。--记住命令映射的概念。(我上面的例子展示了这两个对象的用法)

6. SetLink, AddLink, ,DetachLink, 是进行实体关系管理的,你需要根据你的操作在客户端的Context中维护实体的关系,比如你新增实体的时候,可能需要调用AddLink来增加这个实体和其他实体的关联关系,修改某个实体的时候需调用SetLink来改变实体之间的关系,确保你对实体的修改也包括了对实体间关系修改的考虑(联系上面例子中修改书和作者的例子) ,DetachLink是在你要删除某个实体,你需要将有关联的两个实体之间的关联打断并告诉客户端的Context。而DeleteLink更多的是告诉客户端Context,你要将两个实体间的关联完全打断,这个方法有用,但我觉得它的实用性最低。所有所有的,我认为都是你自己玩,因为数据源根本不知道。但这是客户端非常非常重要的操作,如果你搞不懂,你会发现最后你SaveChanges的时候总是会失败。

7. 如果说,SetLink, AddLink, ,DetachLink是进行实体见关系管理的方法,那么AttachTo和Detach则是你用了处理实体状态的主要方法。在你的客户端,你基本上操作两类对象,一种是 POCO(或PONO),比如Book book1 =New Book(),这时候book1归CLR管理,它是对象,除了存活和垃圾收集之外,它不存在于一个业务领域(Domain)的环境中,你操作的就是.NET的对象。当你调用AttachTo作用于一个POCO对象的时候,这个对象变成了实体,开始有了某个业务领域的实体的状态,对象的属性变成了实体的属性,比如新增,修改,删除,冻结。。。,最重要的是这个实体将要被客户端的Context管理起来,包括实体的标识,实体的属性,实体之间的关系。Detach方法你可以将其理解成反操作,即将一个实体还原成POCO,你可以理解成它通知客户端的Context不要再理会这个对象了,这个实体被 Fired 了,被驱逐了,它回到了.NET CLR的世界。

8. 一般一个对象先AttachTo到客户端的Context中,SetLink, AddLink, ,DetachLink等方法才会生效,Detach时反之。不过我发现有时候AttachTo操作是隐形的,比如所有通过Context查询方法查询来的对象,其实都是实体,同样你将一个POCO对象赋值给一个实体对象时,似乎也默认会将这个POCO对象加入到客户端Context中。

9. AddObject ,UpdateObject ,DeleteObject 和AddToXXX(比如:AddToAuthor),其中AddToXXX是Entity Framework自动生成的简易方法,其实和调用AddObject方法等同。这是三个基本的操作是和上面说的命令映射一一对应的,但不用的是映射生效的标准是从你调用SaveChanges方法时开始。当你还没有调用SaveChanges的时候,这三个方式其实只能算是客户端Context提供给你修改Context中实体状态的方法。而且目前的设计下,AddObject ,UpdateObject ,DeleteObject 都是单一实体对象作为参数。

10. 当你明白和熟悉了这些Astoria Service的操作后,你会发现Entity Framework中有类似的操作,而且可能会更简单。记得很久以前我学习EF的时候经常搞不懂Attach和Detach方法,现在多增加一个客户端的Context,反而会变得容易理解。不过我认为EF的伟大之处是在于它建立E/R模型的方法论,以及它内置实现了IUpdatable,这样它和Astoria Service可以完美结合,而Astoria 象征的是一套REST的编程模型,这势必带来整个应用架构和开发模式的变化,这个论题以后的Weblog中我会详细论述。

11. System.Data.Services.Client (.NET Client) 会是一个很好的工具,我认为未来它非常适合应用在TDD中作为单元测试的一个途径,而且理解和掌握了,那么Silverlight ,AJAX,等客户端库就更容易理解了。

看完上面我总结的要素,再看Elisa Flasko 和 Mike Flasko文章中的代码,就容易了吧,比如下面的:
Quote
Categories newCategory = product.Categories;
context.AttachTo("Products", product);
context.AttachTo("Categories", newCategory);
context.UpdateObject(product);
context.SetLink(product, "Categories", newCategory);

context.SaveChanges();
哈哈,这算是你看本文最大收获,希望大家可以很快实际操作,体验Astoria Service的强大和REST编程的畅快。

最后,是我另外的一些思考,是我联系NHibernate的思考,
Astoria + Entity Framework 除了REST编程之外,特别是编程的逻辑上面,上面我讲到当使用Astoria + EF组合的时候,其实你不是直接操作的数据库完全映射的对象,你是在和一个客户端以及客户端的Context在打交道或进行编程,而且存在POCO对象,实体对象比较明显的两种概念和界限。这个Context存活在客户端(控制台,Silverlight, Javascript)中,代表的是客户关心的领域和视图,这可能就是EF所说的E/R模型的共享。

而NHibernate的编程场景中,这一点并不明显,比如我最近尝试的一个例子中:

using (ISession context = OpenSession())
{
using (ITransaction tran = context.BeginTransaction())
{
IQuery query = context.CreateQuery("from Employee where Name='ccBoy'");

Employee ccboy = query.List<Employee>()[0];
ccboy.Name = "ccBoy Qiang";

Employee Henry = new Employee();
Henry.Name = "Henry Qiang";
ccboy.Manager = Henry;

context.Flush();
tran.Commit();

Console.WriteLine("Updated ccBoy and added Henry");
}
}

你会发现,ISession,Context.BeginTransaction(),tran.Commit和 context.Flush() 这样的操作,基本上是必须的操作,这代表在NHibernate中,你不会有第二个视图和什么客户端的编程界面,很多时候你在客户端编程的时候,你还是需要考虑OPP和数据库之间的差异,这之间的gap还是靠你自己来填平。
你会发现在NHibernate中类似:AddObject ,UpdateObject ,DeleteObject ,SetLink, AddLink, 以及DetachLink这样的函数几乎没有,假想一下,你在AJAX,Silverlight甚至ASP.NET中要使用ISession,BeginTransaction这样的操作,首先对于DDD来说,这并不美观,另外在很大程度上,这限制了客户端,这是否可以可以说是NHibernate的局限或说一般ORM工具的宿命。当然这并不是完全不好,比如从性能角度来说,因为目前的应用架构中,将NHibernate封装和Hosting在ASP.NET中,比你将NHibernate先封装在Astoria中,然后在ASP.NET中调用Astoria服务性能显然要高。

Astoria将如何影响我们的编程模型和架构,还是我们需要继续讨论的。因为这不是NHibernate或Astoria+EF好或不好的问题,而是我们如何应用REST模型的问题,以及REST对我们架构的支持和影响又多深的问题。

各位也看到,在最后一个操作例子中,当客户端要删除一个作者以及它所有的书的记录时,我之前尝试了不下十种方式,都没有成功。这意味着我们平时要维护实体的状态和完整性,这是我们客户端的视图和模型,但一旦我们需要将这些变化更新数据源和存储中的时候,Entity Framwork运行引擎有会按一个和数据库视图贴近和一致的视图来处理。
我试图在客户端打断要删除的实体Author和他的书直接的关联,然后再调用SaveChanges更新数据源,即我试图破坏数据的完整性,即在存在书记录的情况下,删除书的作者。客户端的Context完全支持我这么做,但服务端Entity Framwork并不认为,它固执的保持数据源的完整和一致性,当然这也是我比较欣慰和赞赏的。我的忧虑是性能。

上面测试是要删除的作者下面只有一本书,但现实的情况中,可能一个父子实体模型中,一个父对应几十个甚至上百个子实体,即使使用目前的批操作(Batch),性能也是很低的。
目前我的感觉,大部分实体操作是合适的,但是一些特殊的删除操作,不应该完全依赖Astoria+EF的默认方式,而应该换回我们原来的方式,另外也许QueryInterceptor/ChangeInterceptor , 自定义的Service Operations,甚至EF中存储过程特性等等是否可以成为我们的选择,这就是下篇文章我们要讨论的。
[Last Modified By ccBoy, at 2008-08-17 19:51:29]
Tags: Astoria EDM
Comments Feed Comments Feed: http://www.dotnettools.org/Blog/feed.asp?q=comment&id=262

There is no comment on this article.

You can't post comment on this article.