ADO.NET Dataservice/WCF Data ServicesってそれぞれEntity Frameworkを使う場合とPOCOを使う場合と、それぞれを混在させたい場合とあるような気がするけどどうなんでしょう。Read Onlyなら普通に混在できていいんじゃないかと思うけど簡単に出来たりしないのかな~。
データ モデル (ADO.NET Data Services フレームワーク)
試しにNorthwindのProducts/Categories/Order/Order Detailsでやってみた。
まずはVS2008を使ってADO.NET Entity Data Modelを以下のように作成。
続いてADO.NET Data Serviceを作成。
public class DataEF : DataService<NorthwindEntities>
{
public static void InitializeService(IDataServiceConfiguration config)
{
config.UseVerboseErrors = true; // 追加
config.SetEntitySetAccessRule("*",
EntitySetRights.AllRead);
config.SetServiceOperationAccessRule("*",
ServiceOperationRights.AllRead);
}
}
普通ですね。
これに以下のようなPOCOクラスも混在させたい。
public class DailyOrderSummary
{
public int Year { get; set; }
public int Month { get; set; }
public int Day { get; set; }
public decimal Amount { get; set; }
}
このモデルを返すのは以下のようなクエリです。
from o in Context.Orders
where o.OrderDate.HasValue
group o by new
{
o.OrderDate.Value.Year,
o.OrderDate.Value.Month,
o.OrderDate.Value.Day
} into daily
select new DailyOrderSummary
{
Year = daily.Key.Year,
Month = daily.Key.Month,
Day = daily.Key.Day,
Amount = daily.Sum(d => d.Order_Details
.Sum(od => (od.UnitPrice *
od.Quantity)))
};
がしかし、これを利用するためにDataEfクラスにIQueryableなメソッドを追加してもどうにもこうにも機能してくれないんですよ。Object Service層にはそんなのいないからってことでしょうね。んじゃどうしましょうか。
An ADO.NET Data Services Primer - O'Reilly Answers
ここにあるように一旦POCOだけで動かせる状態にして、そのクラスをプロキシとしてDataService<T>を作ってみます。
public class EfDataModels
{
public NorthwindEntities Context { get; private set; }
public EfDataModels()
{
Context = new NorthwindEntities();
}
public IQueryable<Categories> Categories
{
get { return Context.Categories.AsQueryable(); }
}
public IQueryable<Products> Products
{
get { return Context.Products.AsQueryable(); }
}
public IQueryable<Orders> Orders
{
get { return Context.Orders.AsQueryable(); }
}
public IQueryable<Order_Details> OrderDetails
{
get { return Context.Order_Details.AsQueryable(); }
}
public IQueryable<DailyOrderSummary> DailyOrderSummaries
{
get
{
return
from o in Context.Orders
where o.OrderDate.HasValue
group o by new
{
o.OrderDate.Value.Year,
o.OrderDate.Value.Month,
o.OrderDate.Value.Day
}
into daily
select new DailyOrderSummary
{
Year = daily.Key.Year,
Month = daily.Key.Month,
Day = daily.Key.Day,
Amount = daily.Sum(d => d.Order_Details
.Sum(od => (od.UnitPrice *
od.Quantity)))
};
}
}
}
これを使ってDataEfを定義することにしてみたけど、これはこれでやっぱりエラー。
でもね、上記EfDataModelクラスからEntity Modelで定義したものを除外してDailyOrderSummariesだけにするとうまく行くわけですよ。
となると、違いはDataService<T>に指定するクラスじゃなくて、モデルの基底クラスなのかなとなりますね。なりませんかね。ソースかドキュメントでもあれば自信を持って基底クラスがEntityObjectのものがいるとPOCO混在出来ないと言えるところなんですが、そのへんよくわからないです。教えてエライ人!
で、それならそれでLINQ to SQLのDataContextを使うという方法もありますね。こっちは基底クラスがいない(objectな)わけで。最初に戻ってLINQ to SQLのDataContextの作成からやり直してみます。
EFと一緒です。あ、そうそうちなみにPOCOの場合、キー項目がわからないので、クラスにDataServiceKey属性を付けるのを忘れずに。
ADO.NET Data Services and LINQ-to-SQL - Sergey Zwezdin
なので、先のDailyOrderSummaryクラスは以下のように属性を付けておく必要あり。
[DataServiceKey("Year", "Month", "Day")]
public class DailyOrderSummary
{
public int Year { get; set; }
public int Month { get; set; }
public int Day { get; set; }
public decimal Amount { get; set; }
}
DataContextに追加したモデルたちもpartialなので同じように定義します。
[DataServiceKey("OrderID")]
partial class Order
{
}
[DataServiceKey("OrderID","ProductID")]
partial class Order_Detail
{
}
[DataServiceKey("ProductID")]
partial class Product
{
}
[DataServiceKey("CategoryID")]
partial class Category
{
}
これをプロキシとなるDataModelsにまるっといれこんで以下のようなクラスができました。
public class DataModels
{
public DataL2SDataContext Context { get; private set; }
public DataModels()
{
Context = new DataL2SDataContext();
}
public IQueryable<Category> Categories
{
get { return Context.Categories; }
}
public IQueryable<Product> Products
{
get { return Context.Products; }
}
public IQueryable<Order> Orders
{
get { return Context.Orders; }
}
public IQueryable<Order_Detail> OrderDetails
{
get { return Context.Order_Details; }
}
public IQueryable<DailyOrderSummary> DailyOrderSummaries
{
get
{
return 省略
}
}
}
なんか無駄にコードの量が増えてきて面倒になってきた...。
ナイス!
ここまで来たら、VS2010でADO.NET POCO Entity Generatorを使って同じようにやりたくなるってもんですよね。
ADO.NET C# POCO Entity Generator
このジェネレータはT4になってて、EDMXファイルを指定すると、ObjectContex派生のコンテキストクラスと、エンティティPOCOクラスを生成してくれます。
もちろんPOCOエンティティなのでDataServiceKey属性定義は必要だね。やってみよう!
おぉ~、いいじゃ~ん。と思ったのもつかの間。
ジェネレートしたエンティティを参照すると↑こんなの出てきてちゃんと動かない。なんでじゃ~。出力されたエラーを見てみると...。
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<feed xml:base="http://localhost:29890/Data.svc/" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
<title type="text">Categories</title>
<id>http://localhost:29890/Data.svc/Categories</id>
<updated>2010-07-25T06:29:58Z</updated>
<link rel="self" title="Categories" href="Categories" />
<m:error>
<m:code></m:code>
<m:message xml:lang="ja-JP">内部サーバー エラーです。型 'System.Data.Entity.DynamicProxies.Categories_549968D49334B0D9E524C2FC37C19156947873B86A46A78227AFE184CB25AA68' は複合型またはエンティティ型ではありません。</m:message>
</m:error>
なんじゃこれ。いろいろ調べてみたらDynamicProxiesを使わないようにすればいいという記述を発見。
ADO.NET C# POCO Entity Generator and Data Services
ObjectContextOptions プロパティ (System.Data.Objects)
というわけで、POCO GeneratorのTTファイルをいじることにしてみます。
VSに生成されてる~.Context.ttファイルを開いて、Constructorsリージョンで隠れてる部分を開いて以下のように変更。
#region Constructors
public <#=code.Escape(container)#>()
: base(ConnectionString, ContainerName)
{
ContextOptions.ProxyCreationEnabled = false;
<#
WriteLazyLoadingEnabled(container);
#>
}
public <#=code.Escape(container)#>(string connectionString)
: base(connectionString, ContainerName)
{
ContextOptions.ProxyCreationEnabled = false;
<#
WriteLazyLoadingEnabled(container);
#>
}
public <#=code.Escape(container)#>(EntityConnection connection)
: base(connection, ContainerName)
{
ContextOptions.ProxyCreationEnabled = false;
<#
WriteLazyLoadingEnabled(container);
#>
}
#endregion
この状態で動かしてみます。
今度は通常のエンティティクラスの参照もうまくいきました。ちなみにttファイルをいじってしまえば、DataServiceKey属性もハナからくっついた状態でエンティティクラスを生成させることも出来ますね。
using System.Data.Services.Common;
<#
BeginNamespace(namespaceName, code);
bool entityHasNullableFKs =
entity.NavigationProperties
.Any(np => np.GetDependentProperties()
.Any(p=>ef.IsNullable(p)));
#>
[DataServiceKey(<#= string.Join(",",entity.KeyMembers.Select(km=>"\""+km.Name+"\""))#>)]
<#=Accessibility.ForType(entity)#>
<#=code.SpaceAfter(code.AbstractOption(entity))#>
partial class <#=code.Escape(entity)#>
<#=code.StringBefore(" : ", code.Escape(entity.BaseType))#>
{
<#
region.Begin("Primitive Properties");
foreach (EdmProperty edmProperty in
entity.Properties
.Where(p => p.TypeUsage.EdmType is PrimitiveType &&
p.DeclaringType == entity))
太字の1行。
ようはPOCOのみで構成してしまえば、Read OnlyのData Servicesは比較的簡単に公開できるね、っていう話。
なんだけど、本質的にはData ServiceのレイヤでごちゃごちゃせずにDBにViewを定義して、そのViewをそのまま公開するのが本筋だと思うわけです。Viewが集計に必要なRaw状態になってれば、あとはPivotTableなり使ってExcelでよしなにやってしまえよ、というのがセルフBIのスタートラインなんじゃないのかね。賢くないことをしようとすると無駄なことをたくさんしないといけなくなるから、そこんとこちゃんとやりたまえよ。