くりーむわーかー

プログラムとか。作ってて ・試しててハマった事など。誰かのお役に立てば幸いかと。 その他、いろいろエトセトラ。。。

2016年12月

C#でバルクインサート(BulkCopy) 非同期

以前、非同期でBulkCopyするのを書いた。それのちょっと追記版。

MSDN見てたら「WriteToServerAsync」っていうのがあった。っていうか普通に非同期用のメソッドあるんじゃん・・・。とゆーことでこっちを使って書き直す。

//using System;
//using System.Data;
//using System.Data.SqlClient;

string ConnectionString = @"****";
DataTable templateDt = new DataTable();//BulkCopy用のDataTable定義テンプレ

public void sample()
{
    /*サンプルテーブル
    create table HogeTable(
        intval int,
        str1 varchar(100),
        str2 varchar(100),
        str3 varchar(100),
        str4 varchar(100),
    )
    */

    //最初にDataTableの定義を作るのメンドーなので対象テーブルを空SELECTしてSqlDataReaderから定義を作る
    using (SqlConnection cn_ = new SqlConnection(ConnectionString))
    {
        cn_.Open();
        SqlCommand command = new SqlCommand();
        command.CommandText = "select top 0 * from [HogeTable]";
        command.Connection = cn_;
        using (SqlDataReader sqlDr = command.ExecuteReader())
        {
            DataTable schemaDt = sqlDr.GetSchemaTable();
            foreach (DataRow schemaDr in schemaDt.Rows)
            {
                string columnName = schemaDr["ColumnName"].ToString().Trim();
                string dataType = schemaDr["DataType"].ToString().Trim();
                DataColumn dc = new DataColumn();
                dc.ColumnName = columnName;
                dc.DataType = System.Type.GetType(dataType);
                templateDt.Columns.Add(dc);
            }
        }
    }
    Console.WriteLine("同期実行=====");
    Console.WriteLine("Start:{0}", DateTime.Now);
    for (int i = 0; i < 3; i++)//3回やる
    {
        DataTable copyFromDataTable = CreateTestData(500000);//50万件のテストデータ
        using (SqlConnection cn_ = new SqlConnection(ConnectionString))
        {
            cn_.Open();
            using (SqlBulkCopy bulkCopy = new SqlBulkCopy(cn_))
            {
                bulkCopy.DestinationTableName = String.Format("dbo.[{0}]", "HogeTable");
                try
                {
                    bulkCopy.WriteToServer(copyFromDataTable);
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message);
                }
            }
        }
        Console.Write("*");//進捗的な
    }
    Console.WriteLine("");
    Console.WriteLine("End:{0}", DateTime.Now);

    Console.WriteLine("非同期実行=====");
    st = DateTime.Now;
    Console.WriteLine("Start:{0}", DateTime.Now);
    for (int i = 0; i < 3; i++)//3回やる
    {
        DataTable copyFromDataTable = CreateTestData(500000);//50万件のテストデータ
        AsyncSqlBulkCopy(copyFromDataTable, String.Format("dbo.[{0}]", "HogeTable"));
        Console.Write("*");//進捗的な
    }
    Console.WriteLine("");
    ed = DateTime.Now;
    Console.WriteLine("End:{0}", DateTime.Now);

}
//非同期実行用
public async void AsyncSqlBulkCopy(DataTable bulkFrom,string distTableName)
{
    using (SqlConnection conn = new SqlConnection(ConnectionString))
    {
        await conn.OpenAsync();
        using (SqlBulkCopy bcp = new SqlBulkCopy(conn))
        {
            bcp.DestinationTableName = distTableName;
            await bcp.WriteToServerAsync(bulkFrom);
        }
    }
}
//テストデータ作る
public DataTable CreateTestData(int count)
{
    DataTable copyFromDataTable = templateDt.Clone();
    string kyedatetime = DateTime.Now.ToString("yyyyMMddHHmmssfff");
    for (int i = 0; i < count; i++)
    {
        var addRow = copyFromDataTable.NewRow();
        addRow.BeginEdit();
        addRow[0] = i;
        addRow[1] = String.Format("str1_{0}", kyedatetime);
        addRow[2] = String.Format("str2_{0}", kyedatetime);
        addRow[3] = String.Format("str3_{0}", kyedatetime);
        addRow[4] = String.Format("str4_{0}", kyedatetime);
        addRow.EndEdit();
        copyFromDataTable.Rows.Add(addRow);
    }
    return copyFromDataTable;
}

非同期の方が同期実行時の半分くらいの時間だった。まぁデータが登録終わるまでの時間は変わんないけどネ。。。

ちなみにDestinationTableName にはちゃんと"[テーブル名]"の形でテーブル名を指定したほうが吉。[]で囲ってないとやられる場合がある故。

MSSQLServerからPostgreSQLへのデータ移行をC#で書く

タイトル通り。最近、SQLServerからPostgreSQLへの切り替えをやってる。CreateTableしてー、CSVにデータはいてー、Postgreにインポートしてー。みたいなのはググればいっぱい出てきますが、正直、
クッソだるい

何がだるいかとゆーと、ってか実際には上の感じの作業はしてないので、もしかしたらすんなり行くのかもしれませんが、十中八九、以下の点ではまるのが目に見えている。

  1. テキスト系で改行とかエスケープしないとダメなのあったらどーせ無理じゃない?
  2. 100を超えるテーブルのCreate文なんか流したくない
  3. 100を超えるテーブルのインポートなんかしたくない
  4. バイナリ型ってCSV吐いただけで行けるのかしら?
などなど。ハマルのが目に見えている。

とゆーわけで、PostgreSQLをC#でごにょごにょするのに慣れる意味も含めてプログラムでやった。

SQLServerへの接続とかはデフォでほぼ行けるからどーでもいいとして、PostgreSQLを扱う場合は「Npgsql」を使う。インストールはNugetから。

あと、一件づつインサートとかはありえないので、.netでいうBulkCopyもしたい。

プログラムの流れは↓の感じ。

①SQLServerで移行したいDBのテーブルの定義を丸ごと取得
②①の中でPostgre用のCreateTableを作っておく。
③Postgreに②のCreateTableを発行
④SQLServer側のテーブルをSelect *して、Postgreに順次BulkCopy
⑤Postgre側にIndexつける
⑥VACUUM実行

大したことはやってないけど、確実に手でやるより早い。

①のSQLServerの定義取得は↓の感じのクエリで丸ごととってくる

select 
X.object_id
,X.name as tablename
,cast(Y.column_id as int) as column_id
,Y.name as colname
,cast(Y.system_type_id as int) as system_type_id
,TYPE_NAME(Y.system_type_id) as typename
,cast(Y.max_length as int) as max_length
,cast(Y.precision as int) as precision
,cast(Y.scale as int) as scale
,cast(Y.is_nullable as int) as is_nullable
,cast(Y.is_identity as int) as is_identity 
,cast(isnull(Z.is_primary_key,0) as int) as is_primary_key
from (
	select * from sys.tables
	where type = 'U' 
	and name not like '%[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]%'
) as X
inner join (
	select * from sys.columns
) as Y
on X.object_id = Y.object_id
left join (
	select X.object_id,Y.column_id,Z.is_primary_key from sys.columns as X
	inner join sys.index_columns as Y
	on X.object_id = Y.object_id
	and X.column_id = Y.column_id
	inner join sys.indexes as Z
	on Y.object_id = Z.object_id
	and Y.index_id = Z.index_id
	and X.object_id = Z.object_id
	where Z.is_primary_key = 1
) as Z
on Y.object_id = Z.object_id
and Y.column_id = Z.column_id
order by X.object_id,Y.column_id

主キーの判定がちょっとメンドー。あと、テーブル名で西暦8ケタ付きのバックアップぽいテーブルは除くようにしてみたりしてる。あと、数値系の項目ってどれがbyte型か調べるのめんどーになったので、全部強制的にIntに変換。ランボー過ぎかな?

定義が取れれば、Create文はテキトーに作れるでしょう。

そしたら、Postgre側にテーブル作成を投げる。下の感じ。公式のままだ。

//EF使ってるけど、クエリを発行したいのでEFの接続文字列を無理やり取得
string constr= this.Database.Connection.ConnectionString;
using (var conn = new NpgsqlConnection(constr))
{
    conn.Open();
    using (var cmd = new NpgsqlCommand())
    {
        cmd.Connection = conn;
        try
        {
            //何回か流すようにテーブルドロップ。初回は落ちるのでtryしておく。これもランボー。
            cmd.CommandText = String.Format("DROP TABLE dbo.\"{0}\";",pDef.tablename);
            cmd.ExecuteNonQuery();
        }
        catch { }
        cmd.CommandText = pDef.createSql;
        cmd.ExecuteNonQuery();
    }
}

そしたら次はBulkCopy。ロジックは抜粋で。

using (SqlConnection cn_ = new SqlConnection(ConnectionString))
{
    cn_.Open();

    SqlCommand command = new SqlCommand();
    command.CommandTimeout = 3600;//タイムアウトの設定
    command.CommandText = "select * from [テーブル名]";
    command.Connection = cn_;
    //SQLServerにSelect投げる
    using (SqlDataReader sqlDr = command.ExecuteReader())
    {
        DataTable schemaDt = sqlDr.GetSchemaTable();
        //Postgreにつなぐ
        using (var conn = new NpgsqlConnection(pgConStr))
        {
            conn.Open();
            //Postgreのbulk
            using (var writer = conn.BeginBinaryImport(String.Format("COPY dbo.\"{0}\" ({1}) FROM STDIN (FORMAT BINARY)", "テーブル名","テーブルの列のリスト")))
            {
                while (sqlDr.Read())//SQLServerのSelect結果
                {
                    writer.StartRow();//コピーする行ごとに必要っぽい
                    for(int i=0;i< schemaDt.Columns.Count; i++)
                    {
                        //列毎にカキカキ
                        writer.Write(sqlDr[i],getColDef(pDef, i));
                    }
                }
            }
        }
    }
}
//SQLServerのデータ型に応じてPostgre側の型定義を返す
public NpgsqlDbType getColDef(MSDBTableDef pDef, int i)
{

    switch (pDef.coldeflist[i].system_type_id)
    {
        case 56://int
            return NpgsqlDbType.Integer;
        case 127://bigint
            return NpgsqlDbType.Bigint;
        case 167://varchar
        case 231://nvarchar
            if(pDef.coldeflist[i].max_length < 0)
                return NpgsqlDbType.Text;
            else
                return NpgsqlDbType.Varchar;
        case 106://decimal
            return NpgsqlDbType.Numeric;
        case 61://datetime
            return NpgsqlDbType.Timestamp;
        case 165://varbinary
            return NpgsqlDbType.Bytea;
    }
    return NpgsqlDbType.Varchar;
}

Npgsqlのバルクコピーは↓の感じで、Objectの配列として渡してもOKな気がする。

object[] dtArray = new object[schemaDt.Columns.Count];
sqlDr.GetValues(dtArray);
writer.WriteRow(dtArray);

ただ、公式のこの辺に「NpgsqlDbTypeでデータの型をちゃんと指定する事を激しくお勧めする」って書いてあるのでそーした。データ型の指定はいつの時代になってもとっても重要。

あと、インデックスはデータ入れ終わってから、まとめてやった方が多分よいと思う。速度的な意味で。

最後に、対象のDBに対して「VACUUM」ってコマンドを投げてあげる。不要領域の削除とかなんかもろもろやるらし。テーブルロックとかされる場合もあるらしいので、誰か触ってる時にはやらない方がいい。

でだ、NpgsqlのCopyはいいんだけど、Writeのタイミングで書きに行ってるわけじゃないよね?なんだかいつ実行してるのかロジック見る感じでよくわからない。多分、usingしてるし、Disposeされるときにやってるのかなと愚考。個人的に明示してくれる方がわかりやすい。

C# ファイルとフォルダの一覧

タイトルまま。まー使いたいときにいっつもすぐ出てこないのでメモ。 ググればいっぱい出てくるけども、まとめ的に。

フルパス欲しいだけの時は、静的メンバの方が楽。DirectoryInfoとかFileInfo欲しいときはインスタンス化して使った方が楽。あと、ファイルの操作するときはFileInfoで取っておかないと後で困る。静的メンバ使って操作しても問題ないと思うけど、大量にやるときはインスタンス作った方が早いらしい。試したことは無い。。。

列挙だけならEnumerateの方がレスポンスは早っぽいですね。さすがに。

//using System.IO;
public void fileListSample()
{
    string TopDirectory = @"C:\*****";
    //Directory
    if (Directory.Exists(TopDirectory))
    {
        Console.WriteLine("DirectoryクラスのGetDirectories 戻りはフルパスのString");
        //サブディレクトリを丸ごと
        //カレント直下のみ欲しい場合は「SearchOption.TopDirectoryOnly」
        //⇒でも同じ foreach (string tmpDir in Directory.EnumerateDirectories(TopDirectory, "*", SearchOption.AllDirectories))
        foreach (string tmpDir in Directory.GetDirectories(TopDirectory, "*", SearchOption.AllDirectories))
        {
            Console.WriteLine("{0}", tmpDir);
        }

        Console.WriteLine("DirectoryInfoをインスタンスにしてGetDirectories 戻りはDirectoryInfo");
        DirectoryInfo di = new DirectoryInfo(TopDirectory);
        //サブディレクトリを丸ごと
        //カレント直下のみ欲しい場合は「SearchOption.TopDirectoryOnly」
        //⇒でも同じ foreach (DirectoryInfo tmpDir in di.EnumerateDirectories("*", SearchOption.AllDirectories))
        foreach (DirectoryInfo tmpDir in di.GetDirectories("*", SearchOption.AllDirectories))
        {
            Console.WriteLine("{0}", tmpDir.FullName);
        }
    }
    //File
    if (Directory.Exists(TopDirectory))
    {
        Console.WriteLine("DirectoryクラスのGetFiles 戻りはフルパスのString");
        //サブディレクトリを丸ごと
        //カレント直下のみ欲しい場合は「SearchOption.TopDirectoryOnly」
        //⇒でも同じ foreach (string tmpFile in Directory.EnumerateFiles(TopDirectory, "*", SearchOption.AllDirectories))
        foreach (string tmpFile in Directory.GetFiles(TopDirectory, "*", SearchOption.AllDirectories))
        {
            Console.WriteLine("{0}", tmpFile);
        }

        Console.WriteLine("DirectoryInfoをインスタンスにしてGetFiles 戻りはFileInfo");
        DirectoryInfo di = new DirectoryInfo(TopDirectory);
        //サブディレクトリを丸ごと
        //カレント直下のみ欲しい場合は「SearchOption.TopDirectoryOnly」
        //⇒でも同じ foreach (FileInfo tmpFile in di.EnumerateFiles("*", SearchOption.AllDirectories))
        foreach (FileInfo tmpFile in di.GetFiles("*", SearchOption.AllDirectories))
        {
            Console.WriteLine("{0}", tmpFile.FullName);
        }
    }
}

.net mvc レスポンスヘッダつける

別ドメインのURLにAjaxでリクエスト送りたかったんだけど、↓のアクセスエラーが返ってくる。

XMLHttpRequest cannot load http://***. Origin http://*** is not allowed by Access-Control-Allow-Origin.

やる前からだいたい予想してたけど、やっぱ別ドメインにAjaxで飛ばすのは大変だ。

Jsonpにすればいけるかと思ったけどなんかダメ。なので結果を返す側でレスポンスヘッダつけた。返す側は.netMVCで作ってるサイト。コントローラーで以下をつけてあげる。

Response.Headers.Add("Access-Control-Allow-Origin", "*");//呼び出せるようにクロスドメインをOKにしておく。

まーだめな対応の仕方だとは思う。自分が作ってるサイト間でのやり取りだからこその話。それでもだめだと思うけど。。。

Bitnami Redmineでプラグイン作成

Bitnamiのredmine上でプラグインを作るとき。OSはWindwos。

まず、作業するコンソールは必ず「use_redmine.bat」で起動する。これが一番はまる。

そしたら「apps\redmine\htdocs」に移動。でホントに最初は下をやる。

gem install bundler
bundle install --without postgresql mysql rmagick

SSL接続できーねーみたいなエラーが出た場合は下のコマンドでhttpの接続に変える。そしたらリトライ。

gem source -a http://rubygems.org/

んで、railsでスケルトンを作成。いちを、「apps\redmine\htdocs\plugins」に移動してからやった。

ruby script/rails generate redmine_plugin [プラグイン名]

もしかしたら、script/railsじゃなくて「bin/rails」じゃないとだめかも。

「Access denied for user 'root'@'localhost' (using password: NO)」みたいなエラーが出た場合は DBのrootのパスワードが指定されてないで落ちてる。 なので、「config\database.yml」のdevelopmentあたりにルートのパスワードを入れる。 rootのパスワードはBitnamiRedmineインストールしたときに指定したrootのパスワード。

「ActiveRecord::NoDatabaseError: Unknown database 'redmine_development'」で怒られた場合は 開発用のDBが作られていない。なのでmysqlにログインして「redmine_development」を作ってあげる。

作成出来たら、とりあえずRedmine再起動すると管理のプラグインのページに表示されてるはず。とりあえず、これでスケルトンの作成は終わり。

ロードバランサ cookieが消える その4(最終回)

その1その2その3 の最終回。

前回までのあらすじ。

  1. IE11+Windows8.1の環境下でロードバランサ使ってると、つながってるサーバが唐突に変わる事象が起きる
  2. 調べてみるとCookieを使用したロードバランシングをしていると、ロードバランサが採番したセッションCookieのキーが唐突に変わることがある
  3. Win8.1以降のOSの場合、ピン留め機能のために定期的にサイトのルートへ「Get browserconfig.xml」というHTTPリクエストを発行している模様
  4. このリクエストにロードバランサのCookieがついてない
  5. ロードバランサが初回のアクセスと勘違いしてバランシングして別サーバに飛ばされるっていう原因
  6. ブラウザの保護モードを有りにするとこのリクエストにCookieをつけるようになる ←イマココ

で、保護モードを有効にするためにはUACが有効になってる必要があるようなのでUACを有効にする。

そしたら今度はIE11のエンタープライズモードが有効にならない事がわかった。あーあと、こんな古臭いことやってる人はいないと思いますが、IEのDLLホスティング使ってると、おおむね動くんだけど、保護モードで動かすと、特定のライブラリ読み込むときに、クライアント側で動的コンパイルしてるらしく、そのDLLの出力先の整合性が取れてない感じがして、変な例外が裏で発生する。IEのバグなのかしら?

とゆーことで保護モードを有効にして回避することはできませんでした。あ、自分が使うときはIE11のエンタープライズモードで動かさないとダメなもんで。

もーどうしよーもなし。IPでのバランシングに変えるしかない。NATとか使ってたら詰むね。。。

サイトのHTML直せるなら、METAタグで一個追加すればいけそうな気がする。MSDN的に。

<meta name="msapplication-config" content="none"/>

↑を入れれば、問題のリクエストをやめてくれるだろうか。

エクセルの表をRedmineのWikiテーブルにしたい

--追記2017/5/21
こっちにRedmineのプラグイン的な何かを置きました。

RedmineのWikiで表的なまとめを作ろうとするとtextile形式で作ることになるんだけど、ちょっと見た目にこりだしたりすると、とたんにえらいもの書かないといけなくなる。なので、エクセルでまとめた表をぱっと貼り付けられると便利。なんかいいプラグインないかなーと探してみたけど、グッとくるものがいないので、とりあえず作った。

まずはエクセルの表をコピーしてテキストエリアあたりに貼り付け⇒セルのフォーマット情報も含めて内容を取得する。Webサイトで。これにはClipboradApiを使用。下の感じ。

document.querySelector('#pasteArea').addEventListener('paste', function (e) {
    console.log(e.clipboardData.getData('text/html'));
});

クリップボードのデータを取得するとき、だいたい”text/plain”で指定するんだけど、htmlで指定すると、セルのフォーマット情報も含んだHTML形式で取得できる。↓の感じ。

sample01

で、あとはこのHTMLを解析してTextile形式に変換すれば良いという寸法です。ホントはこの流れをRedmineのプラグインで作りたかったんだけど、Ruby触るの久しぶり過ぎて、すごい時間かかりそうだったので、.netに逃げました。.netでやる場合、HTMLの解析は「HtmlAgilityPack」を使用する。Nugetから取得。

とりあえずで作ったのは、TextAreaにエクセルの表をコピペすると、サーバ側にそのHTMLをテキストで投げて、サーバ側で解析して結果を返す的なAPI。

で困ったことにHTMLをテキストで投げると、.netのセキュリティのチェックにデフォルトで引っかかる。なので、アノテーションでそのチェックを外してあげる。↓の感じ。

[HttpPost]
[ValidateInput(false)]//コレ
public ActionResult Excel2Textile(int id , FormCollection cols)
{
    string resultStr = Conv(cols);
    return Json(resultStr, JsonRequestBehavior.AllowGet);
}

そしたら、HTMLを解析する。まず、HTMLのテキストをAgilityPackに食わせる。

    StringBuilder wkSb = new StringBuilder();
    HtmlDocument htmlDoc = new HtmlDocument();
    htmlDoc.LoadHtml(excelTableHTML);

で、あとはTableタグ拾って⇒TRタグ拾って⇒TDタグ拾ってを繰り返し。

    foreach (var rootnode in htmlDoc.DocumentNode.SelectNodes(@"//table"))
    {
        //テーブルのTRで回す
        foreach (var tmpnode in rootnode.SelectNodes(@"tr"))
        {
            //TDそれぞれで定義を付ける
            foreach (var tmpTdNode in tmpnode.SelectNodes(@"td"))
            {

"//table/tr"って書けば外側のループはいらないんだけど理由は後で。これでとりえあず、中身の内容は拾えるんだけど、スタイルはAgilityPackでは解析できないっぽい?なので別途、スタイルシートを解析するライブラリを使う。今回使ったのは「ExCss」。取得はNugetから。これを使ってClass定義とstyle属性の内容を解析する。

今回はそこまで厳密にテーブルを再現したいわけではないので、背景色・文字色・文字寄せ・セル幅くらいを再現する。スタイルの解析は下の感じで書く。

var parser = new Parser();
var stylesheet = parser.Parse(htmlDoc.DocumentNode.SelectSingleNode(@"//style").InnerHtml);

//Class名は.付きにしないと当たらない。ex: .HogeClass
var styleElm = stylesheet.StyleRules.Where(n => n.Value == wkclass).FirstOrDefault();
if (styleElm != null)
{
    foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "background"))
    {
        if (!wkDic.ContainsKey("background")) wkDic.Add("background", sTmp.Term.ToString());
        break;
    }
}

Class定義はこれでいいんだけど、タグ内のStyle属性やる場合、ExCssはクラス定義の形式じゃないと解析できない模様なので、ちょっと文字列足してあげる。↓の感じ。

//クラス名はなんでもよい
var tmppparse = parser.Parse(".hoge{" + tmpTdNode.Attributes["style"].Value.Trim() + "}");

これでだいたい出来上がりなんだけど、問題が一つ。画像とか図形が表にかかってると、マイクロソフトのオレオレ定義が入り込んでくる。↓の感じ。

sample02

あと、この定義が入ると、通常のtdタグの中にさらにtableタグが入り込んでくる。てんやわんや。なので、一番表のTableだけで処理したいので、最初のループで最初のtableタグのみ処理するように仕込んでる。

超絶やっつけで作ったやつの全体像は下の感じ。見せられたもんではないのですが、とりあえず。同じロジック2回書いてるし・・・あとで整理しないと。


//ExcelをRedmineのWiki用に変換する
public string ExcelTable2WikiTable(string excelTableDef)
{
    StringBuilder wkSb = new StringBuilder();
    HtmlDocument htmlDoc = new HtmlDocument();
    htmlDoc.LoadHtml(excelTableDef);
    string wkInnerText = "";

    var parser = new Parser();
    var stylesheet = parser.Parse(htmlDoc.DocumentNode.SelectSingleNode(@"//style").InnerHtml);

    foreach (var rootnode in htmlDoc.DocumentNode.SelectNodes(@"//table"))
    {
        //テーブルのTRで回す
        foreach (var tmpnode in rootnode.SelectNodes(@"tr"))
        {
            //TDそれぞれで定義を付ける
            foreach (var tmpTdNode in tmpnode.SelectNodes(@"td"))
            {
                if (tmpTdNode.InnerText.Contains("<!--[if gte vml 1]>"))
                {
                    HtmlDocument innerHtmlDoc = new HtmlDocument();
                    innerHtmlDoc.LoadHtml(tmpTdNode.OuterHtml);
                    foreach (var innerNode in innerHtmlDoc.DocumentNode.SelectNodes(@"//table/tr/td"))
                    {
                        wkInnerText = innerNode.InnerText;
                        break;
                    }
                }
                else
                {
                    wkInnerText = tmpTdNode.InnerText;
                }

                Dictionary<string, string> wkDic = new Dictionary<string, string>();
                string classsep = "";
                wkSb.AppendFormat("|");
                //列結合
                if (tmpTdNode.Attributes.Where(n => n.Name == "colspan").Count() > 0)
                {
                    wkSb.AppendFormat("\\{0}", tmpTdNode.Attributes["colspan"].Value.Trim());
                    classsep = ".";
                }
                //行結合
                if (tmpTdNode.Attributes.Where(n => n.Name == "rowspan").Count() > 0)
                {
                    wkSb.AppendFormat("/{0}", tmpTdNode.Attributes["rowspan"].Value.Trim());
                    classsep = ".";
                }
                //最初にスタイル属性に入ってる奴を処理
                if (tmpTdNode.Attributes.Where(n => n.Name == "style").Count() > 0)
                {
                    var tmppparse = parser.Parse(".hoge{" + tmpTdNode.Attributes["style"].Value.Trim() + "}");
                    var styleElm = tmppparse.StyleRules.Where(n => n.Value == ".hoge").FirstOrDefault();
                    if (styleElm != null)
                    {
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "background"))
                        {
                            if (!wkDic.ContainsKey("background")) wkDic.Add("background", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "color"))
                        {
                            if (!wkDic.ContainsKey("color")) wkDic.Add("color", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "width"))
                        {
                            if (!wkDic.ContainsKey("width")) wkDic.Add("width", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "text-align"))
                        {
                            if (!wkDic.ContainsKey("text-align")) wkDic.Add("text-align", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                    }
                }
                //CLASSで指定されてる内容を処理
                if (tmpTdNode.Attributes.Where(n => n.Name == "class").Count() > 0)
                {
                    string wkclass = "." + tmpTdNode.Attributes["class"].Value.Trim();
                    var styleElm = stylesheet.StyleRules.Where(n => n.Value == wkclass).FirstOrDefault();
                    if (styleElm != null)
                    {
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "background"))
                        {
                            if (!wkDic.ContainsKey("background")) wkDic.Add("background", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "color"))
                        {
                            if (!wkDic.ContainsKey("color")) wkDic.Add("color", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "width"))
                        {
                            if (!wkDic.ContainsKey("width")) wkDic.Add("width", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                        foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "text-align"))
                        {
                            if (!wkDic.ContainsKey("text-align")) wkDic.Add("text-align", sTmp.Term.ToString());
                            classsep = ".";
                            break;
                        }
                    }
                }
                StringBuilder wkStyleStr = new StringBuilder();
                foreach (var tmpkey in wkDic.Keys)
                {
                    wkStyleStr.Append(tmpkey + ":" + wkDic[tmpkey] + ";");
                }
                if (wkDic.Count > 0) wkSb.Append("{" + wkStyleStr.ToString() + "}");
                wkSb.AppendFormat("{0}{1}", classsep, wkInnerText);
            }//td
            wkSb.Append("|" + Environment.NewLine);
        }//tr
        break;
    }//table
    
    return wkSb.ToString();
}

ちなみに、変換した結果をWikiに貼った結果は下。

sample03

けっこーちゃんと出来てる気がする。あー、ちゃんと列・行の結合にも対応出来てたりする。

.net MVCでEntity Framework使用でDBをPostgreSQLにしてみる

いつもはSQLServer使ってたんだけど、たまにはPostgreSQLも使ってみようかと思いまして、やってみた。

MVCはVisualStudioでテキトーにテンプレを使う。メンドーなので認証なし。そしたらNugetで下の3つのパッケージをインストール。

sample01

EFはEFのために必要。EFでPostgre使うためにNpgsql.EntityFrameworkが必要。これのためにNpgsqlが必要。みたいな感じらしい。EFは6。

そしたら次はWeb.configの設定

<!--ここはインストールすると勝手に入る-->
<entityFramework>
  <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
  <providers>
    <provider invariantName="Npgsql" type="Npgsql.NpgsqlServices, Npgsql.EntityFramework" />
    <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
  </providers>
</entityFramework>
<!--ここを手で追記-->
<system.data>
  <DbProviderFactories>
    <add name="Npgsql Data Provider" invariant="Npgsql" description="Data Provider for PostgreSQL" type="Npgsql.NpgsqlFactory, Npgsql" />
  </DbProviderFactories>
</system.data>
<!--接続文字列の指定-->
<connectionStrings>
  <add name="DefaultConnectionPGSQL" connectionString="Server=localhost;Port=5432;User Id=postgres;Password=*****;Database=hogedb" providerName="Npgsql" />
</connectionStrings>

手で追記する部分があるっぽい。ここ入れないと動かなかった。あとは接続文字列の指定。

ためしに作ったテーブルは下の二つ。日本語列名がいけるか試したいので日本語で。

--主キーがserial(SQLServerで言うIdentity)じゃないやつ
create table dbo.ホゲテーブル
(
ホゲ varchar(100) 
,数値 int
,CONSTRAINT wkpk PRIMARY KEY (ホゲ)
)
--主キーにserial(SQLServerで言うIdentity)
create table dbo.フガテーブル
(
フガid SERIAL
,テスト varchar(100) 
,数値 int
)

で、ワナが一つ。SQLServerはデフォでスキーマ名が「dbo」になる。なので、EFも何もしないとSQLに「dbo.table」みたいに書かれる。なので、Postgreのスキーマはdboで作っておいた方が吉。スキーマを変える場合は後述。

あとは、ためし実装。Model側。

public class HomeModels : DbContext
{
    public HomeModels() : base("DefaultConnectionPGSQL")
    {

    }
    public DbSet<HogeTable> HogeTables { get; set; }
    public DbSet<FugaTable> FugaTables { get; set; }

    public HomeViewModel getTable()
    {
        HomeViewModel resultModel = new HomeViewModel();
        resultModel.hogeT = this.HogeTables.Where(n => n.hogeval > 0).ToList();
        resultModel.fugaT = this.FugaTables.Where(n => n.数値 > 0).ToList();

        return resultModel;
    }
    public void modelAdd()
    {
        HogeTable addM = new HogeTable();
        addM.hogekey = DateTime.Now.ToString("yyyyMMddhhmmssfff");
        addM.hogeval = 123;
        this.HogeTables.Add(addM);


        FugaTable addM2 = new FugaTable();
        addM2.テスト = DateTime.Now.ToString("yyyyMMddhhmmssfff");
        addM2.数値 = 123;
        this.FugaTables.Add(addM2);
        this.SaveChanges();
    }

}
public class HomeViewModel
{
    public List<HogeTable> hogeT { get; set; }
    public List<FugaTable> fugaT { get; set; }
}

[Table("ホゲテーブル")] //テーブル名指定
public class HogeTable
{
    [Key] //主キーの設定
    [Column("ホゲ")] //列名指定
    public string hogekey { get; set; }
    [Column("数値")]//列名指定
    public int hogeval { get; set; }
}

[Table("フガテーブル", Schema = "dbo")] //スキーマ変えたいとき
public class FugaTable
{
    [Key] //主キーの設定
    public int フガid { get; set; }
    public string テスト { get; set; }
    public int 数値 { get; set; }
}

コントローラ側。HomeのIndexだけで、あと、データ追加用のAction。

HomeModels db = new HomeModels();
public ActionResult Index()
{
    HomeViewModel result = db.getTable();

    return View(result);
}

public ActionResult AddAction()
{
    db.modelAdd();
    return RedirectToAction("Index");
}

View側はこれ。

@model mvcdemo.Models.HomeViewModel
@{
    ViewBag.Title = "Home Page";
}

<div>
    @Html.ActionLink("Add","AddAction")
</div>

<h1>ホゲテーブル</h1>
<div>
    @foreach(var tmp in Model.hogeT)
    {
        <table>
            <tr>
                <td>
                    @tmp.hogekey
                </td>
                <td>
                    @tmp.hogeval
                </td>
            </tr>
            
        </table>
    }
</div>

<h1>フガテーブル</h1>
<div>
    <table>
        @foreach (var tmp in Model.fugaT)
        {
            <tr>
                <td>
                    @tmp.フガid
                </td>
                <td>
                    @tmp.テスト
                </td>
            </tr>
        }    
    </table>
</div>

実行結果で、何回か追加した後。

sample02

ちゃんとIDも振られてるし問題なし。。。フツーに使える。よかたよかた。

スキーマ変えたいときはテーブルのModel作るときに属性で「[Table("フガテーブル", Schema = "dbo")] 」って感じにする。全体通してやる場合はDbContextのクラスの中に下を入れておく。

public class HomeModels : DbContext
{
    public HomeModels() : base("DefaultConnectionSQLServer")//DefaultConnectionPGSQL
    {

    }
    //これを入れる
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema("dbo");
    }
    public DbSet<HogeTable> HogeTables { get; set; }
    public DbSet<FugaTable> FugaTables { get; set; }
    ・・・

Modelのインスタンス作った時に呼ばれるやつではないかと。。。

ちなみにこの状態のまま、SQLServer側に同じテーブル作って、接続文字列の指定でSQLServer用のものを書けばそのままDB切り替えられました。ためしに色々やるのに便利。。。

Postgerはフツーに使えていい感じなんだけど、なんつーか、Postgreの管理ツールがびみょー。なんかいいの無いのかしら。

pgAdminなるものを使用してみたけど。微妙じゃない?なんつーか自分がMicrosoftに染められてるんだろーけど、インテリセンス弱すぎじゃない?あと、エラーとか警告とか結果の出力とかもろもろもろもろもろ。SQLServerのManegementStudioはやっぱりさすがにさすがにって感じなんだろーか。。。

問合せ