var SQL = $@"
select
予約.ID,
顧客.会社名,
予約.日付,
部屋.名称 as 会議室,
予約状況.開始時刻 as 開始,
予約状況.終了時刻 as 終了,
顧客sub.部署名 as 部署名,
顧客sub.担当者A as 担当者,
顧客sub.TEL as TEL,
顧客sub.FAX as FAX,
予約.予約 as 状態,
予約.契約日 as 契約日,
予約.確認票最終処理日 as 確認日,
請求.日付 as 請求日,
X.利用料,
isnull(入金.入金額,0) as 入金額,
予約.備考B as 注意事項,
予約.案内名称 as 案内板名称
from
(
select
予約.ID as ID,
cast(ISNULL(sum(予約sub.単価*予約sub.数量),0) as money) as 利用料
from 予約
left join 予約sub on 予約sub.予約ID = 予約.id
where 予約.顧客id = {顧客ID}
group by
予約.ID
) X
inner join 予約 on 予約.id = X.ID
inner join 顧客 on 顧客.ID = 予約.顧客ID
inner join 顧客sub on 顧客sub.ID = 予約.顧客SUBID
left join 請求 on 請求.id = 予約.請NO
left join 入金 on 入金.請求ID = 請求.ID
inner join 部屋 on 部屋.id = 予約.会議室
inner join 予約状況 on 予約状況.予約ID = 予約.ID
order by 予約.日付 desc
";
this.Items = App.ent.Database.SqlQuery<結果>(SQL).ToList();
なので、.NET Framework の EF では、以下のように「予約1」という名前が勝手に振られます。
public partial class 予約
{
public int ID { get; set; }
public int 顧客ID { get; set; }
public int 顧客SUBID { get; set; }
public Nullable<int> 予約者 { get; set; }
public string 予約会社 { get; set; }
public string 予約担当 { get; set; }
public string 予約TEL { get; set; }
public int 担当者ID { get; set; }
public Nullable<System.DateTime> 日付 { get; set; }
public Nullable<System.DateTime> 記録日 { get; set; }
public Nullable<System.DateTime> 契約日 { get; set; }
public Nullable<System.DateTime> M期限 { get; set; }
public Nullable<System.DateTime> K期限 { get; set; }
public Nullable<int> 予約1 { get; set; }
public Nullable<int> 会議室 { get; set; }
public Nullable<int> TF { get; set; }
public Nullable<int> TE { get; set; }
public partial class 単発アラーム
{
public int ID { get; set; }
public Nullable<System.DateTime> 日時 { get; set; }
public string 対象種別 { get; set; }
public string 対象名 { get; set; }
public string 表示内容 { get; set; }
public System.DateTime CreateAt { get; set; }
public System.DateTime UpdateAt { get; set; }
}
このような自動生成された Entity クラスとは別に、patial で拡張しておきます。
/// <summary>
/// 日時を年月日と時分に分ける拡張
/// </summary>
public partial class 単発アラーム
{
public Nullable<System.DateTime> 日時_年月日
{
get { return this.日時; }
set
{
if ( this.日時.HasValue )
{
DateTime dt = new DateTime(
value.Value.Year,
value.Value.Month,
value.Value.Day,
日時.Value.Hour,
日時.Value.Minute,
0);
this.日時 = dt;
}
}
}
public Nullable<System.DateTime> 日時_時分
{
get { return this.日時; }
set
{
if (this.日時.HasValue)
{
DateTime dt = new DateTime(
日時.Value.Year,
日時.Value.Month,
日時.Value.Day,
value.Value.Hour,
value.Value.Minute,
0);
this.日時 = dt;
}
}
}
}
ここで .NET6 への移植時に問題が発生します。
.NET Framework の EF では、テーブル構造が *.edmx ファイルに分離されているので、拡張した日時_年月日プロパティや日時_時分プロパティは更新時に無視されるのですが、.NET6 の EFCoreでは更新対象がEntityクラスの全プロパティとなるため、この拡張したプロパティを「無視」させるようにしなければなりません。
CREATE TABLE RPI (
Id INTEGER NOT NULL CONSTRAINT PK_RPI PRIMARY KEY AUTOINCREMENT,
Key BLOB NULL,
Metadata BLOB NULL,
StartTime TEXT NOT NULL,
EndTime TEXT NOT NULL,
RssiMin INTEGER NOT NULL,
RssiMax INTEGER NOT NULL,
MAC INTEGER NOT NULL
)
sqlplus sys/siosWOA6SKw=1@localhost:1521/ORCLCDB as sysdba
ログインユーザーと表領域の作成
ログインユーザーを作ってためしておきます。
あとで、.NET から接続確認したいので redmine ユーザーを作ります。
alter session set container=ORCLPDB1;
create tablespace "redminets" datafile '/opt/oracle/oradata/ORCLCDB/redminets.dbf' size 500M
AUTOEXTEND ON NEXT 100M MAXSIZE 1G LOGGING EXTENT MANAGEMENT
LOCAL SEGMENT SPACE MANAGEMENT AUTO;
create user redmine
identified by redmine
default tablespace "redminets"
account unlock ;
GRANT connect TO "REDMINE";
GRANT CREATE SESSION TO "REDMINE";
GRANT "RESOURCE" TO "REDMINE";
ALTER USER "REDMINE" DEFAULT ROLE ALL;
こんなに使われると、ホスト側の Windows が死んでしまうので、.wslconfig で制限をします。この加減がよくわからないのですが、まあ、Hyper-V の仮想環境で Windows Server + Oracle を作ったときと同じくらい喰うのはどうなの?って感じです。環境的には、可搬性があるからいいけど。
CREATE TABLESPACE REDMINETS DATAFILE
'C:\app\oracle\oradata\ORCL\DATAFILE\REDMINETS.dbf'
SIZE 1G AUTOEXTEND ON NEXT 100M MAXSIZE UNLIMITED;
CREATE USER redmine IDENTIFIED BY redmine
DEFAULT TABLESPACE REDMINETS
TEMPORARY TABLESPACE TEMP
ACCOUNT UNLOCK ;
GRANT UNLIMITED TABLESPACE TO redmine;
GRANT CREATE SESSION TO redmine;
GRANT CONNECT TO redmine;
GRANT RESOURCE TO redmine;
ALTER USER "REDMINE" DEFAULT ROLE ALL;
redmine という名前でユーザーを作成しておきます。
表領域 REDMINETS
ユーザー名 redmine(内部では自動的に大文字になるので、正確には REDMINE です)
パスワード redmine
権限が少し過剰ですが、ひとまずこれで Ok です。この使い方は、いまとなっては少しイリーガルなので、プラカブルデータベースを使った方式に直します。
一番手っ取り早いのは、手作業でエンティティクラスを作ることです。エンティティクラスは単純な値クラスなので、プロパティを並べれば ok.
MySQL Workbench の結果から、ちまちまと C# のクラスを作るか、スキーマを参照しながら手作業で作ります。
CREATE TABLE `projects` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '',
`description` text,
`homepage` varchar(255) DEFAULT '',
`is_public` tinyint(1) NOT NULL DEFAULT '1',
`parent_id` int DEFAULT NULL,
`created_on` timestamp NULL DEFAULT NULL,
`updated_on` timestamp NULL DEFAULT NULL,
`identifier` varchar(255) DEFAULT NULL,
`status` int NOT NULL DEFAULT '1',
`lft` int DEFAULT NULL,
`rgt` int DEFAULT NULL,
`inherit_members` tinyint(1) NOT NULL DEFAULT '0',
`default_version_id` int DEFAULT NULL,
`default_assigned_to_id` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_projects_on_lft` (`lft`),
KEY `index_projects_on_rgt` (`rgt`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 ;
NULL 許可の部分をチェック
tinyint(1) を bool に直す
timestamp を DateTime あるいは DateTimeOffset に直す
主キーに Key 属性をつける
部分を注意すれば比較的簡単に C# のエンティティクラスができます。
public class projects
{
[Key]
public int id { get; set; }
public string name { get; set; } = "";
public string? description { get; set; }
public string? homepage { get; set; }
public bool is_public { get; set; }
public int parent_id { get; set; }
public DateTime created_on { get; set; }
public DateTime updated_on { get; set; }
public string identifier { get; set; } = "";
public bool status { get; set; }
public int? lft { get; set; }
public int? rgt { get; set; }
public int inherit_members { get; set; }
public int? default_version_id { get; set; }
public int? default_assigned_to_id { get; set; }
}
using System.ComponentModel.DataAnnotations.Schema;
[Table("proejcts")]
public class Project
{
...
[Column("is_public")]
public bool IsPublic { get; set; }
...
}
Table 属性と Column 属性をちまちまと指定していけば、
MySQL 側のカラム名を Column で指定
プロパティ名は C# の命名規約に合わせる
ことができます。ちなみに MySQL の設定によってはテーブル名やカラム名の大文字小文字が区別されるため、環境にそろえようとすると(特に Linux上)、この属性は必須になります。
public class DescResult
{
[Key]
public string Field { get; set; }
public string Type { get; set; }
public string Null { get; set; }
public string? Key { get; set; }
public string? Default { get; set; }
public string? Extra { get ; set; }
}
public class RedmineDataContext : DbContext
{
...
public DbSet<DescResult> DescResult => Set<DescResult>();
}
var cnn = context.Database.GetDbConnection() as MySqlConnection;
var result = context.DescResult.FromSqlRaw("DESC projects");
Console.WriteLine("\nDESC projects");
Console.WriteLine("Field Type Null Key Default Extra");
foreach ( var it in result )
{
Console.WriteLine($"{it.Field}\t{it.Type}\t{it.Null}\t{it.Key}\t{it.Default}\t{it.Extra}");
}
public static class DataTableExtenstions
{
/// <summary>
/// DataTable.Rows を指定した List<T>に変換する
/// </summary>
public static List<T> ToList<T>(this DataTable src) where T : new()
{
var items = new List<T>();
var properties = typeof(T).GetProperties();
// TODO: Column 属性があれば、探索するカラム名を変更する
foreach ( DataRow row in src.Rows )
{
var item = new T();
foreach ( var pi in properties )
{
var value = row[pi.Name];
if ( value == System.DBNull.Value )
{
pi.SetValue(item, null);
}
else
{
pi.SetValue(item, row[pi.Name]);
}
}
items.Add(item);
}
return items;
}
}
public partial class T_顧客Sub
{
public int TIDa { get; set; }
public Nullable<int> TID { get; set; }
public string 部署名 { get; set; }
public string TEL { get; set; }
public string FAX { get; set; }
public string 携帯 { get; set; }
[Column("〒")]
public string ZIP { get; set; }
public string 住所1 { get; set; }
public string 住所2 { get; set; }
...
}
ここまでできあがると、通常の EF Core と同じように ACCESS にアクセスができます。あまり無茶をすると ACCESS ファイルが壊れそうな気がするのですが、まあ SELECT だけならば大丈夫でしょう。
付属するデータは、LINQ の場合は Include を使って取ってきてもよいのですが、外部キーの記述が結構面倒(標準にあっていないとうまくいかないことが多い)ので、あとから手作業でとってきています。多少検索スピードは落ちますが、ツール的にはこれで十分でしょう。まじめに作るときは SQL Server 等にデータを移行&カラム名を連携しやすいように直します。
public static class DataTableExtenstions
{
public static DataTable AsDataTable<T>(this DbSet<T> src) where T : class
{
return DataTableExtenstions.AsDataTable(src.Local);
}
public static DataTable AsDataTable<T>(this IEnumerable<T> src) where T : class
{
var properties = typeof(T).GetProperties();
var dest = new DataTable();
// テーブルレイアウトの作成
foreach (var prop in properties)
{
DataColumn dc = new DataColumn();
dc.ColumnName = prop.Name;
if (prop.PropertyType.IsGenericType &&
prop.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
dc.DataType = Nullable.GetUnderlyingType(prop.PropertyType);
dc.AllowDBNull = true;
} else
{
dc.DataType = prop.PropertyType;
}
dest.Columns.Add(dc);
}
// 値の投げ込み
foreach (var item in src)
{
var row = dest.NewRow();
foreach (var prop in properties)
{
var itemValue = prop.GetValue(item, new object[] { });
row[prop.Name] = itemValue ?? System.DBNull.Value;
}
dest.Rows.Add(row);
}
return dest;
}
}
データを投入するときに null から System.DBNull.Value にしておきます。
private void blukSave<T>(string tablename, IEnumerable<T> items, bool keepid = true ) where T : class
{
var cnstr = toEnt.Database.GetDbConnection().ConnectionString;
SqlBulkCopy bc;
if (keepid == true)
{
bc = new SqlBulkCopy(cnstr, SqlBulkCopyOptions.KeepIdentity);
}
else
{
bc = new SqlBulkCopy(cnstr);
}
bc.DestinationTableName = tablename;
var dt = items.AsDataTable();
bc.WriteToServer(dt);
}
確か、以前の BulkCopy は ID をインクリメントしなかったような気がするのですが、現在の SqlBulkCopy は ID を挿入時にインクリメントしてしまいます。大量データを投入するときは、ID はあらかじめ振ってあることが多い(他のデータから移行するため)ので、INSERT 時に ID の値が変わらないようにします。
オプションで SqlBulkCopyOptions.KeepIdentity をつけておきます。
実際の使い方はこんな感じ。予約テーブルは実は ACCESS から移行するデータなので大量に Nullable が入っています。いったん List にため込んでから、BlukCopy を行うので一時的にため込まれる List のメモリ量が心配ですが、まあ、大丈夫でしょう。最初の DropTable 関数は、内部で TRUNCATE TABLE を呼び出しています。
public bool To予約()
{
DropTable("予約");
var lst = new List<予約>();
foreach (var it in fromEnt.T_予約)
{
var t = new 予約();
t.ID = it.KID;
t.顧客ID = it.TID.Value;
t.顧客SUBID = it.TIDa ?? 0;
t.予約者 = it.予約者;
...
t.UpdateAt = DateTime.Now;
lst.Add(t);
}
this.blukSave("予約", lst);
return true;
}