ひっそりと生きるプログラマのブログ

日頃気になった事なりを書き留めるブログです。関心ごとは多くもう少し更新頻度を上げたいところです。

【Entity Framework】LINQ と Oracle の関数でマップされていないものを利用する(3)※固定値を関数に引数にとる場合

saboten-sakura.hatenablog.com

今回は前回より複雑なSQLを発行します。
TO_CHAR(DATE, 'D') で、指定した日付が何曜日かが取れます。
今回は上記の関数を投げて、特定の曜日のみ抽出する処理を目標とします。

流れとしては、途中まで1回目の流れです。
saboten-sakura.hatenablog.com

.net のメソッドを定義(Oracle 関数とマッピング用)

返す型は、 TO_CHAR なので string を返します。
引数は、TO_CHAR 2で二つなんですが、第1引数は任意の値で、第2引数は'D'を渡します。
なので、 .net では DateTime を引数として渡します。

public static string DayOfWeekString(this DateTime? value) => throw new NotImplementedException();

.net で定義したメソッドを Oracle 関数とマッピング

GetMethod で定義した MethodInfo を取得します。

var method = typeof(StringExtension).GetMethod(nameof(StringExtension.DayOfWeekString), new Type[] { typeof(DateTime) })!;

取得した MethodInfo と Oracle の関数を定義付けします。

modelBuilder.HasDbFunction(method).HasTranslation(translation =>
new SqlFunctionExpression(
    "TO_CHAR",
    new SqlExpression[]
    {
        new SqlConstantExpression(
            Expression.Constant("D"),
            new StringTypeMapping(
                "VARCHAR2",
                DbType.String)),
        translation.First()
    },
    true,
    new bool[]
    {
        false,
        false
    },
    typeof(string),
    new StringTypeMapping(
        "VARCHAR2",
        DbType.String)
    )
);

上記を定義した後に、以下のコードを実装。

var context = new SampleDbContext();
var items = (from m in context.SampleTables 
             where
                m.SampleDate.DayOfWeekString() == "3"
             select m);
foreach (var item in items)
{
    Console.WriteLine($"{item.Id} - {item.SampleDate?.ToString("ddd")}");
}

で、出力した SQL がこちら。

SELECT "s"."ID", "s"."SAMPLEDATE", "s"."SAMPLENUMBER"
FROM "SAMPLETABLE" "s"
WHERE TO_CHAR("s"."SAMPLEDATE", 'D') = '3' 

【Entity Framework】LINQ と Oracle の関数でマップされていないものを利用する(2)※Select句で利用する。

saboten-sakura.hatenablog.com

投稿のちょっとした続きです。
前回は Where句 に独自関数を追加しましたが、
今回は Select句 で Oracle の関数を呼んでみます。

var context = new SampleDbContext();
var items = (from m in context.SampleTables 
             where 
             m.SampleNumber.Sqrt() > 3 && 
             m.SampleNumber.Sqrt() < 7 
             select new { test1 = m.SampleNumber, test2 = m.SampleNumber.Sqrt() });
foreach (var item in items)
{
    // SampleNumber の表示と、 SampleNumber の平方根を表示。
    Console.WriteLine($"{item.test1} - {item.test2}");
}

で、出力した SQL がこちら。

SELECT "s"."ID", "s"."SAMPLENUMBER", "SQRT"("s"."SAMPLENUMBER") "test2"
FROM "SAMPLETABLE" "s"
WHERE (("SQRT"("s"."SAMPLENUMBER") > 3) AND ("SQRT"("s"."SAMPLENUMBER") < 7)) 

【Entity Framework】LINQ と Oracle の関数でマップされていないものを利用する(1)

LINQ 正規関数にマッピングされていない Oracle 関数の使い方。
幾つか実装方法はありますが、簡単な方法から。

環境

  • Visual Studio 2022
  • .net 6.0
  • EF Core 6.0.29
  • Oracle EF Core 6.21.1.40
  • NLog (ログを確認の為、出力)

実装

主要部分のみで、内容に意味は無いです。
"こうすれば動くよ!"という事を抜粋して記載します。

流れとしては以下の通りです。

  1. .net のメソッドを定義(Oracle 関数とマッピング用)
  2. .net で定義したメソッドを Oracle 関数とマッピング
  3. .net のメソッドを利用し SQL を発行し結果を確認

.net のメソッドを定義(Oracle 関数とマッピング用)

.net のメソッドをサクッと定義
平方根を算出する "SQRT" とマッピング予定

public static class StringExtension
{
    public static int Sqrt(this int? value) => 
        throw new NotImplementedException();
}

.net で定義したメソッドを Oracle 関数とマッピング

DbContext を継承し OnModelCreating を override します。
.net で定義したメソッドの MethodInfo を取得します。
HasDbFunction の引数に渡して、HasName で何の関数にマッピングするか設定。
※.net のメソッドと Oracle 関数が 1:1 なのでこれで十分

シグネチャが同じであれば、"SQRT" 以外のOracle関数でもマッピング可能。
.net のメソッドと Oracle の関数は役割が近く名前も似ていた方が把握し易くなりますね。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var sqrt = typeof(StringExtension).GetMethod("Sqrt", new Type[] { typeof(int) })!;
    modelBuilder.HasDbFunction(sqrt).HasName("SQRT");
    base.OnModelCreating(modelBuilder);
}

.net のメソッドを利用し SQL を発行し結果を確認

エンティティのメンバとか全無視して期待した SQL が発行されるか確認。

// SampleNumber の値に、 2, 16, 40, 6423 が設定されたレコード有
// 平方根の結果が、3より大きく 7未満の場合
var context = new SampleDbContext();
var items = (from m in context.SampleTables 
             where 
             m.SampleNumber.Sqrt() > 3 && 
             m.SampleNumber.Sqrt() < 7 
             select m);
foreach (var item in items)
{
    // 16
    // 40
    Console.WriteLine(item.SampleNumber);
}

上記で発行された SQL をログ出力した結果

SELECT "s"."ID", "s"."SAMPLENUMBER"
FROM "SAMPLETABLE" "s"
WHERE (("SQRT"("s"."SAMPLENUMBER") > 3) AND ("SQRT"("s"."SAMPLENUMBER") < 7)) 

マッピングされ期待した結果が無事取得出来ました。
ユーザーが独自に定義した関数も可能だったり、
.net のメソッドと Oracle 関数を 1:n でマッピングだったり出来るので気分が向いたら追記します。

【Entity Framework】テーブルの主キーの値を渡して、レコードを取得する方法(ナビゲーションの値も含む)

以下のような実装で取得可能。

次の実装は呼び出し元

SampleTable というテーブルがあり、
主キーに相当するプロパティが SampleId になります。
SampleSubTable と SampleTable はナビゲーションプロパティが定義しています。

static void Main(string[] args)
{
    SampleTable data = null;
    using (var context = new Sample01Entities())
    {
        context.Configuration.ProxyCreationEnabled = false;
        data = GetSampleTableData(context, 2);
    }
    Console.WriteLine(data.SampleId);
    Console.WriteLine(data.SampleName);
    Console.WriteLine(data.SampleSubTable.Count);
    Console.ReadLine();
}

private static SampleTable GetSampleTableData(
    DbContext context, int key1)
{
    return DbContextUtility.GetData<SampleTable>(
        context,
        new SampleTable()
        {
            SampleId = key1
        });
}

次の実装は呼び出し先
リファクタリングの余地はあるが、
実現したいことはできていると思います。

public static class DbContextUtility
{
    #region EntityType
    private static Dictionary<Type, EntityType> entityTypes =
        new Dictionary<Type, EntityType>();

    private static EntityType GetEntityType(DbContext context, Type type)
    {
        EntityType result;
        if (entityTypes.TryGetValue(type, out result)) return result;
        var objectContext = ((IObjectContextAdapter)context).ObjectContext;
        var metadata = objectContext.MetadataWorkspace;
        result = metadata.GetItem<EntityType>(type.FullName, DataSpace.OSpace);
        entityTypes.Add(type, result);
        return result;
    }
    #endregion

    public static T GetData<T>(
        DbContext context, T entity) where T : class
    {
        var type = typeof(T);
        var entityType = GetEntityType(context, type);
        var keyProperties = entityType.KeyMembers.Select(k => type.GetProperty(k.Name));
        var values = keyProperties.Select(m => m.GetValue(entity)).ToArray();
        var result = (T)context.Set<T>().Find(values);
        GetEntity(context, result, new HashSet<object>());
        return (T)result;
    }

    private static void GetEntity(
        DbContext context, object entity, HashSet<object> values)
    {
        if (values.Contains(entity)) return;
        values.Add(entity);
        var type = entity.GetType();
        var entityType = GetEntityType(context, type);
        var navproperties = entityType.NavigationProperties.Select(
            m => entity.GetType().GetProperty(m.Name));
        foreach (var nav in navproperties)
        {
            if (nav.PropertyType.IsGenericType)
            {
                context.Entry(entity).Collection(nav.Name).Load();
                var enumerable = (IEnumerable)nav.GetValue(entity);
                foreach(var e in enumerable)
                    GetEntity(context, e, values);
            }
            else
            {
                context.Entry(entity).Reference(nav.Name).Load();
                var value = nav.GetValue(entity);
                GetEntity(context, value, values);
            }
        }
    }
}

【Entity Framework】Set やら Find やら使って、ナビゲーションプロパティも含めてDBから取得する方法

調べた結果、できそうな雰囲気。
眠いので今週中位に記事を書く予定。

EntityType から、いろいろな情報が取得できるので、
面白い事ができそうです。

DB(SQL ServerOracle)に、
依存しない形で汎用的な部分を作りこめるのはうれしいですね。

【ASP.NET MVC】モデルのバインドに関して (2)

前回投稿した記事で、
モデルのバインドに関して調べました。
saboten-sakura.hatenablog.com

以下のように複数の Name が存在した場合。

@using (Html.BeginForm("Send", "Home", null, FormMethod.Post))
{
    <input type="text" name="a" />
    <input type="checkbox" name="a" value="true" />
    <input type="hidden" name="a" value="false" />
    <input type="submit" value="送信" />
}

以下のような実装をする事でフォーム上の値が変数にバインドされます。
※引数の型が配列になっています。

public class HomeController : Controller
{
    public ActionResult Send(bool[] a)
    {
        return this.Index();
    }
}

bool 型なので、 true or false のみですが、
string 型 や int 型などでもキャスト可能であれば利用可能です。

【ASP.NET MVC】モデルのバインドに関して

ポストする時に同じ name があった場合、
どのような挙動となるのか確認しました。
※自分自身のメモです。時間も遅いのでわかり辛かったらすみません。。。

次のような Controller と View があるとします。

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View("Index");
    }

    public ActionResult Send(bool a)
    {
        return this.Index();
    }
}

※ submit すると HomeController クラスの Send メソッドが呼ばれます。

@using (Html.BeginForm("Send", "Home", null, FormMethod.Post))
{
    <input type="checkbox" name="a" value="true" />
    <input type="hidden" name="a" value="false" />
    <input type="submit" value="送信" />
}

checkbox を チェック した状態で引数 a には true が設定されます。
また、
checkbox を チェック しない状態で引数 a には false が設定されます。

これは、name が複数同じ値が存在した場合、
先に読み込まれた値を元に解決しているようです。
その為、以下のような実装も可能となります。

@using (Html.BeginForm("Send", "Home", null, FormMethod.Post))
{
    <input type="text" name="a" />
    <input type="checkbox" name="a" value="true" />
    <input type="hidden" name="a" value="false" />
    <input type="submit" value="送信" />
}

type が text の入力値が bool型へ変換できない場合は実行時例外が発生します。
文字列そてい、 "true" および "false" と入力した場合は、
入力した値が bool 型へ変換され引数 a に設定されます。

この事から、上から下へ順に解決されている事が確認できました。