MySQLやSQLiteでEFが使えるならば、F#からもEFを使えるだろう、ということで探していたのだけど、
F# で Entity Framework Coreる – pocketberserkerの爆走
http://pocketberserker.hatenablog.com/entry/2017/12/06/000405
確かに、Entity Classを生成するコードが F# にはない…というか T4 自体が C# だけしか使えないようになっているので、F# では手作業で Model クラスを作らねばいけないっぽい。
のだが、
EFのクラスをMVVMのINotifyPropertyChangedに対応させる裏技 | Moonmile Solutions Blog http://www.moonmile.net/blog/archives/9030
で試したように T4 のファイル(*.tt)を直接書き替えることによって、生成するコードを自前で作ることができるんですよ。となると、ここで C# の Model クラスを出力している部分を、F# に書き替えればよい。
- C# のプロジェクトに ADO.NET Entitiy Data Model を追加
- *.tt を F# 用に書き替える。
- 生成すると *.fs ファイルができる。
- 生成した F# のファイルを、F# のプロジェクトにコピーして使う
という手順でいけるだろう、ってことで、作ってみたのがこれ。
http://github.com/moonmile/sample-fsharp-ef/
C# プロジェクトの Model.tt と Model1.Context.tt を F# コードを出力するように書き替える。本来ならば、F# プロジェクトで TextTemplatingFileGenerator が使えればよいのだけど駄目なので、仕方がないので生成後のコード(Model1.Context.fs, projects.fs, issues.fs)を F# プロジェクトにリンクする。
まずは、普通にC#プロジェクトでADO.NET Entitiy Data Modelを追加する
C#プロジェクトを作って、MySQLのprojectsテーブルとissuesテーブルのエンティティクラスを作る
ここで、使われている T4のファイル(Model1.Context.tt, Model1.tt)を書き替える
Model1.Context.tt, Model1.ttを書き替えてF#のコードを出力
Model1.tt
ちょっと乱暴だが、CodeStringGenerator::PropertyとEntityClassOpeningを書き替えて、F#のレコード型が出力されるようにする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public class CodeStringGenerator { ... public string Property(EdmProperty edmProperty) { return string .Format( CultureInfo.InvariantCulture, // "{0} {1} {2} {{ {3}get; {4}set; }}", "{2} : {1} " , Accessibility.ForProperty(edmProperty), _typeMapper.GetTypeName(edmProperty.TypeUsage), _code.Escape(edmProperty), _code.SpaceAfter(Accessibility.ForGetter(edmProperty)), _code.SpaceAfter(Accessibility.ForSetter(edmProperty))); } ... public string EntityClassOpening(EntityType entity) { return string .Format( CultureInfo.InvariantCulture, "[<CLIMutable>]" + Environment.NewLine + "type {2}{3} =" , Accessibility.ForType(entity), _code.SpaceAfter(_code.AbstractOption(entity)), _code.Escape(entity), _code.StringBefore( " : " , _typeMapper.GetTypeName(entity.BaseType))); } |
Model1.Context.tt
DbContext を継承するクラスを作成する部分を書き替えてしまう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | type <#=code.Escape(container)#>() = inherit DbContext() override x.OnConfiguring( optionsBuilder: DbContextOptionsBuilder ) = base.OnConfiguring(optionsBuilder) optionsBuilder.UseSqlServer("connection-string") |> ignore <# foreach (var entitySet in container.BaseEntitySets.OfType< EntitySet >()) { #> <#=codeStringGenerator.DbSet(entitySet)#> <# } foreach (var edmFunction in container.FunctionImports) { WriteFunctionImport(typeMapper, codeStringGenerator, edmFunction, modelNamespace, includeMergeOption: false); } #> |
これらの *.tt ファイルは保存すると即実行されるので、試行錯誤しながらできる。コンバートされる F# コードを見てちまちまと *.tt ファイルを変更する。
そうすると、こんなコードが出力される。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | namespace SampleFSharpEF.Model open System open System.Collections.Generic [<CLIMutable>] type issues = { id : int tracker_id : int project_id : int subject : string description : string due_date : Nullable<System.DateTime> category_id : Nullable< int > status_id : int assigned_to_id : Nullable< int > priority_id : int fixed_version_id : Nullable< int > author_id : int lock_version : int created_on : Nullable<System.DateTime> updated_on : Nullable<System.DateTime> start_date : Nullable<System.DateTime> done_ratio : int estimated_hours : Nullable< float > parent_id : Nullable< int > root_id : Nullable< int > lft : Nullable< int > rgt : Nullable< int > is_private : bool closed_on : Nullable<System.DateTime> } |
DbContext を継承した redmineEntities クラス
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | namespace SampleFSharpEF.Model open System open Microsoft.EntityFrameworkCore type redmineEntities() = inherit DbContext() override x.OnConfiguring( optionsBuilder: DbContextOptionsBuilder ) = base .OnConfiguring(optionsBuilder) optionsBuilder.UseMySQL( "connection-string" ) |> ignore [<DefaultValue>] val mutable _issues : DbSet<issues> member this .issues with get () = this ._issues and set v = this ._issues <- v [<DefaultValue>] val mutable _projects : DbSet<projects> member this .projects with get () = this ._projects and set v = this ._projects <- v |
F# のほうは、MySQLとMicrosoft.EntityFrameworkCoreを使うので、OnConfiguringメソッドをオーバーライドして接続文字列を指定する形にしている。
F#プロジェクトにエンティティクラスを含める
F#のプロジェクトにリンクでエンティティクラスを含めておく。F#の場合、ファイルが前方参照にする必要があるので、順番を調節する。
ここでは、.NET Core な F# プロジェクトを作って試している。
エンティティクラスを使うコードを書く
手作業になると100行以上書かないといけないエンティティクラスを、T4 で吐き出すようにしたので、使うのは結構楽になる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | open System open System.Linq open SampleFSharpEF.Model [<EntryPoint>] let main argv = printfn "Hello World from F#!" let ent = new redmineEntities() for it in ent.projects.ToList() do Console.WriteLine(String.Format( "{0} : {1}" , it.id, it.name )) 0 // return an integer exit code |
NuGet で MySql.Data.EntityFrameworkCore をインストールした後に、普通にデータベースにアクセスコードを書けばいい。
dotnet run するとこんな感じになる。
C#から使ってみる
試しに、F#で出力されたエンティティクラスをC#で使ってみる。
コマンドライン版の SampleFSharpEF.FSharp プロジェクトを参照設定して、SampleFSharpEF.Modelの中が使えるようにしていまう。本来ならば、別途F#のモデルプロジェクトを用意すると思う。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using System; using System.Linq; using SampleFSharpEF.Model; namespace SampleFSharpEF.CSharp { class Program { static void Main( string [] args) { Console.WriteLine( "Hello C# World!" ); var ent = new redmineEntities(); foreach ( var it in ent.projects.ToList()) { Console.WriteLine(String.Format( "{0} : {1}" , it.id, it.name)); } } } } |
結果はF#と同じになる。
以前は、C#プロジェクトからF#プロジェクトを参照する場合、FSharp.Core を参照設定する必要があった(PCLの関係だと思う)のだが、.NET Standard化されているのか、.NET Coreなプロジェクトだと FSharp.Core への参照設定はいらない。
サンプルコード
http://github.com/moonmile/sample-fsharp-ef/
おまけ
F#では動かないT4なんだけど、C#プロジェクトでF#コードを出力するという裏技を使うと、なんとか使えるようになる。とは言え、いちいちC#プロジェクトを作るのもアレなんだが。
Templatus/README.md at master ・ kerams/Templatus
https://github.com/kerams/Templatus/blob/master/README.md
F#でT4みたいなのが動く Templatus というのがあるので、これを使ってみるとよいかもしれん…が、Model1.tt の中身を全て F# で書き替えなければいかんというのが結構な手間のような気もする。要はエンティティクラスのマイグレーションがやりたいだけなので、適当に MySQLやSQLiteに接続してテーブル構造を引っ張り出して、コードを出力するだけでいいんだが。コードファーストのマイグレーションのような積み重ね方式じゃなくて、単純なスナップショット版でよいし(データは消えるので、実際にあれこれやる場合は、積み重ね方式が必須になるだけど)。