DBとUIのデータ形式を変換する
データベースで'0'や'1'で保持している値を、ユーザーインターフェースではCheckBoxで表現したい、などというケースはよくあります。一般的にはDBEntityとUIEntityを別クラスで作ってDxo機能で変換するようですが、DBとUIが密接なEntity(画面表示用のSELECTと対になるEntity)に、変換機能を持たせることで一層減らす方法を考えました。
SeasarもListViewもEntityにプロパティ名でアクセスするので、まずいつも通りDB側の定義から生成したEntityに、UI用のプロパティを追加しています。例ではDB側は表示区分Varchar2のカラムをstringのプロパティで受けて、UIとはboolのプロパティでget/setできるようにしています。C#ではプロパティのoverloadができないので、苦肉でUI用のプロパティにはプレフィクスUIを付けています。
public string 表示区分 { get; set; } public bool UI表示区分 { get { return 表示区分 == 区分定数.表示区分.表示する; } set { 表示区分 = value ? 区分定数.表示区分.表示する : 区分定数.表示区分.表示しない; } }
static class 区分定数 { static class 表示区分 { public static string 表示しない = "0"; public static string 表示する = "1"; } }
Entityに値をコピーする
Entityクラスに、引数で渡されたEntityのプロパティ値をコピーするAssignメソッドを実装してみました。引数のEntityから自分のプロパティと同じ名前のプロパティを探して値をgetし、自分のプロパティにsetするだけのシンプルな機能です。
public void Assign(Entity e) { if (e == null) { return; } Type et = e.GetType(); foreach (var p in this.GetType().GetProperties()) { PropertyInfo ep = et.GetProperty(p.Name); if (ep == null) { continue; } object ev = ep.GetValue(e, null); if (ev != null) { ev = Convert.ChangeType(ev, Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType); } p.SetValue(this, ev , null); } }
ASP.NETでListViewを使っていて、ListView.ItemsからFindControlする処理で、同じ感じ?で、引数で渡されたListViewItemからコピーするAssignメソッドです。このメソッドでコピーできるようにするには、Entityのプロパティ名とaspのコントロール名を同じ名前にする必要があります。
つまり、Entityのhogeプロパティをバインドするaspコントロールはhoge。
<asp:Label ID="hoge" runat="Server" Text='<%# Eval(Container.DataItem, "hoge") %>' />
public void Assign(ListViewItem item) { foreach (var p in this.GetType().GetProperties()) { object o = item.FindControl(p.Name); if (o == null) { continue; } Type up = Nullable.GetUnderlyingType(p.PropertyType); if (o is ITextControl) { //""(入力なし)をstringプロパティまたはNullableプロパティにセットするときはnullをセットする if (string.IsNullOrEmpty((o as ITextControl).Text)) { if (p.PropertyType.Equals(typeof(string)) || up != null) { p.SetValue(this, null, null); continue; } } p.SetValue(this, Convert.ChangeType((o as ITextControl).Text, up ?? p.PropertyType), null); continue; } if (o is ICheckBoxControl) { p.SetValue(this, Convert.ChangeType((o as ICheckBoxControl).Checked, up ?? p.PropertyType), null); continue; } } }
S2Dao.NETのDaoファイルとEntityファイルを自動生成する
S2Dao.NET(Oracle用)のDaoファイルとEntityファイルを自動生成するスクリプトです。Dao用に作成したSQLファイルをスクリプトファイルにドロップして使います。SQLファイルを使わないDaoの場合は、空のtablename.sqlファイルを作ってください。
スクリプトでやっていることは、
- テンプレートファイルを読み込む(ReadText)
- 予約語を動的パラメータに置換する(Replace)
- ファイルに保存する(WriteText)
だけなので、そういう処理のテンプレにも。例によってwow64.vbsとcmd.vbsはS2Dao.vbsと同じフォルダに入れておきます。
S2Dao.vbs
option explicit dim wsh,fso const ForReading = 1 const ForWriting = 2 const ForAppending = 8 dim OraSession dim OraDatabase const ORADB_DEFAULT = &H0& const ORADYN_DEFAULT = &H0& const ORAPARM_INPUT = 1 const ORADB_INTEGER = 3 'Integer const ORADB_LONG = 4 'LongInteger const ORADB_SINGLE = 6 'Single const ORADB_DOUBLE = 7 'Double const ORADB_DATE = 8 'Date const ORADB_OBJECT = 9 'OraBFILE.. const ORADB_TEXT =10 'String const ORADB_LONGBINARY =11 'String const ORADB_MEMO =12 'String set wsh = CreateObject("Wscript.Shell") set fso = CreateObject("Scripting.FileSystemObject") include "cmd.vbs" include "wow64.vbs" call init call main call fini msgbox "ファイルの生成が完了しました。" set wsh = nothing set fso = nothing WScript.Quit '----------------------------------------------------------------- sub init() dim database dim username dim password database = "orcl" username = "scott" password = "tiger" set fso = CreateObject("Scripting.FileSystemObject") set OraSession = CreateObject("OracleInProcServer.XOraSession") set OraDatabase = OraSession.OpenDatabase( _ database, _ username & "/" & password, _ ORADB_DEFAULT) OraDatabase.Parameters.Add "column_name", 0, ORAPARM_INPUT OraDatabase.Parameters("column_name").AutoBindEnable end sub sub main() dim i for i = 0 to WScript.Arguments.Count - 1 call createDao(WScript.Arguments(i)) call createEntity(WScript.Arguments(i)) next end sub sub createDao(arg) dim s s = ReadText(fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), "IDao.cs")) s = Replace(s, "%tablename%", fso.GetBaseName(arg)) call WriteText(fso.BuildPath(fso.GetParentFolderName(arg), "I" + fso.GetBaseName(arg) + "Dao.cs"), s) end sub sub createEntity(arg) dim s s = ReadText(fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), "Entity.cs")) s = Replace(s, "%tablename%", fso.GetBaseName(arg)) s = Replace(s, "%fields%", getFields(arg)) call WriteText(fso.BuildPath(fso.GetParentFolderName(arg), fso.GetBaseName(arg) + ".cs"), s) end sub function ReadText(filename) with CreateObject("ADODB.Stream") .Charset = "UTF-8" .Open .LoadFromFile filename ReadText = .ReadText .Close end with end function sub WriteText(filename, text) with CreateObject("ADODB.Stream") .Charset = "UTF-8" .Open .WriteText text, 1 '最後に改行付き .SaveToFile filename, 2 '上書き .Close end with end sub function getFields(filename) dim i,t with OraDatabase.CreateDynaset(sql(filename), ORADYN_DEFAULT) t = "" for i = 0 to .Fields.Count - 1 t = t + "" + vbCrLf t = t + " /// <summary>" + vbCrLf t = t + " /// " + getComment(.Fields(i).Name) + vbCrLf t = t + " /// </summary>" + vbCrLf t = t + " public " + getType(.Fields(i)) + " " + .Fields(i).Name + " { get; set; }" + vbCrLf next .Close end with getFields = t end function function sql(filename) dim s s = ReadText(filename) with CreateObject("VBScript.RegExp") .Pattern = "/\*.*?\*/" .Global = True s = .Replace(s, "") end with if Len(s) > 0 then sql = "SELECT * FROM (" + s + ") WHERE 1=0" else sql = "SELECT * FROM " + fso.GetBaseName(filename) + " WHERE 1=0" end if end function function getComment(fieldname) OraDatabase.Parameters("column_name").Value = fieldname with OraDatabase.CreateDynaset("select max(comments) from user_col_comments where column_name = :column_name", ORADYN_DEFAULT) if IsNull(.Fields(0)) then getComment = fieldname else getComment = .Fields(0).Value end if end with end function function getType(field) select case field.Type case ORADB_INTEGER gettype = "int" case ORADB_LONG gettype = "long" case ORADB_SINGLE gettype = "short" case ORADB_DOUBLE gettype = "double" case ORADB_DATE gettype = "DateTime" case ORADB_OBJECT gettype = "object" case ORADB_TEXT gettype = "string" case ORADB_LONGBINARY gettype = "string" case ORADB_MEMO gettype = "string" case else gettype = "string" end select if gettype <> "string" and field.OraNullOK then gettype = gettype + "?" end if end function sub fini() set OraDatabase = Nothing set OraSession = Nothing end sub sub include(filename) filename = fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), filename) ExecuteGlobal fso.OpenTextFile(filename, ForReading, False).ReadAll() end sub
select a.*, b.* from a, b where a.id=b.id
IDao.cs
using System; using System.Collections; using System.Collections.Generic; using System.Data; using System.Diagnostics; using Seasar.Dao.Attrs; namespace Company.Product.Dao { /// <summary> /// I%tablename%Dao /// </summary> [Bean(typeof(%tablename%))] public interface I%tablename%Dao { IList<%tablename%> Select(); int Insert(%tablename% entity); int Update(%tablename% entity); int Delete(%tablename% entity); } }
Entity.cs
using System; using System.Collections.Generic; using Seasar.Dao.Attrs; namespace Company.Product.Entity { /// <summary> /// %tablename% /// </summary> public class %tablename% { %fields% } }
■追記
ALTER TABLE ADD columnで追加したカラムのField.Typeは、ORADB_TEXTになるみたいです。正しく取得できるようにするには、DROP & CREATE でテーブルを再作成する必要があります。
CSVファイルをsqlldrでロードする
WOWで動かすテンプレを使って、CSVファイルをsqlldrでロードするスクリプトを作ってみました。CSVファイルをsqlldr.vbsへドロップすると、CSVファイル名をテーブル名とみなしてORACLEデータベースの列情報からコントロールファイルを自動生成しsqlldrを起動します。CSVファイルとテーブルの列定義が異なる場合は、コントロールファイルの雛型作成ツールとして使えます。実行にあたっては、WOWで動かすテンプレのcmd.vbsとwow64.vbsが必要です。
sqlldr.vbs
option explicit dim wsh,fso const ForReading = 1 const ForWriting = 2 const ForAppending = 8 set wsh = CreateObject("Wscript.Shell") set fso = CreateObject("Scripting.FileSystemObject") include "cmd.vbs" include "wow64.vbs" dim OraSession dim OraDatabase dim OraDynaset dim database dim username dim password const ORADB_DEFAULT = &H0& const ORADYN_DEFAULT = &H0& const ORAPARM_INPUT = 1 dim [ctl],[tbl],[dat],[log],[bat] call init() call main() call fini() set wsh = Nothing set fso = Nothing WScript.Quit sub init() database = "orcl" username = "scott" password = "tiger" set OraSession = CreateObject("OracleInProcServer.XOraSession") set OraDatabase = OraSession.OpenDatabase( _ database, _ username & "/" & password, _ ORADB_DEFAULT) OraDatabase.Parameters.Add "table_name", 0, ORAPARM_INPUT OraDatabase.Parameters("table_name").AutoBindEnable end sub sub main() dim i for i = 0 to WScript.Arguments.Count - 1 [dat] = WScript.Arguments(i) if fso.FileExists([dat]) then '入力ファイルパスからパラメータを決定する [tbl] = UCase(fso.GetBaseName([dat])) [ctl] = fso.BuildPath(fso.GetParentFolderName([dat]), tbl & ".ctl") [log] = fso.BuildPath(fso.GetParentFolderName([dat]), tbl & ".log") [bat] = fso.BuildPath(fso.GetParentFolderName([dat]), tbl & ".bat") call create_ctl() 'コントロールファイルを作成する call run_sqlldr() 'sqlldrを実行する end if next end sub sub fini() set OraDatabase = Nothing set OraSession = Nothing end sub function create_ctl() 'コントロールファイルを作成する dim f, sql set f = fso.OpenTextFile([ctl], ForWriting, True) f.WriteLine "OPTIONS(" f.WriteLine "skip=1," f.WriteLine "errors=-1" f.WriteLine ")" f.WriteLine "LOAD DATA" f.WriteLine "TRUNCATE INTO TABLE " & [tbl] f.WriteLine "FIELDS TERMINATED BY ','" f.WriteLine "OPTIONALLY ENCLOSED BY '""'" f.WriteLine "TRAILING NULLCOLS" f.WriteLine "(" sql = "select column_name from user_tab_columns where table_name = :table_name order by column_id" OraDatabase.Parameters("table_name").Value = [tbl] set OraDynaset = OraDatabase.CreateDynaset(sql, ORADYN_DEFAULT) do until OraDynaset.EOF if OraDynaset.RowPosition > 1 then f.Write "," end if f.WriteLine OraDynaset.Fields(0).Value OraDynaset.MoveNext loop f.WriteLine ")" f.Close set f = Nothing set OraDynaset = Nothing end function function run_sqlldr() 'SQLLDRを実行する wsh.Run cmd("SQLLDR [username]/[password]@[database] control='[ctl]' data='[dat]' log='[log]'") 'SQLLDR実行コマンドをbatファイルへ出力する dim f set f = fso.OpenTextFile(bat, ForWriting, True) f.WriteLine cmd("SQLLDR [username]/[password]@[database] control='[ctl]' data='[dat]' log='[log]'") f.Close set f = Nothing end function sub include(filename) filename = fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), filename) ExecuteGlobal fso.OpenTextFile(filename, ForReading, False).ReadAll() end sub
WOWで動かすテンプレ
64bit環境で32bitCOMを使うためのテンプレ。64bit環境でWOWじゃなかったらWOWで自分自身を起動しなおします。template.vbsのinit、main、fini、に適宜処理を記述します。cmd.vbs、wow64.vbsはincludeするので同じフォルダに置いてください。
template.vbs
option explicit dim wsh,fso const ForReading = 1 const ForWriting = 2 const ForAppending = 8 set wsh = CreateObject("WScript.Shell") set fso = CreateObject("Scripting.FileSystemObject") include "cmd.vbs" include "wow64.vbs" call init call main call fini set wsh = nothing set fso = nothing WScript.Quit '----------------------------------------------------------------- sub init() end sub sub main() dim i for i = 0 to WScript.Arguments.Count - 1 call func(WScript.Arguments(i)) next end sub sub func(arg) end sub sub fini() end sub sub include(filename) filename = fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), filename) ExecuteGlobal fso.OpenTextFile(filename, ForReading, False).ReadAll() end sub
cmd.vbs
'パラメータを展開する ' コレクションの各要素を結合する ' msgbox cmd(WScript.Arguments) ' 文字列の各要素を展開する ※各要素はグローバルで定義する ' A = "hoge.exe" ' B = "arg1" ' C = "arg2" ' msgbox cmd("[A] [B] [C]") function cmd(v) if IsObject(v) then dim i, arr redim arr(v.Count-1) for i = 0 to v.Count - 1 arr(i) = quote(v(i)) next cmd = join(arr) else cmd = v with CreateObject("VBScript.RegExp") .Pattern = "(\[.*?\])" .Global = True do while .Test(cmd) dim par for each par in .Execute(cmd) Execute "cmd = Replace(cmd, par, quote(" & par.subMatches(0) & "))" next loop end with end if end function function quote(s) quote = """" & Replace(s, """", """""") & """" end function
wow64.vbs
call wow64 sub wow64 '64bit環境か? if strcomp("AMD64", wsh.ExpandEnvironmentStrings("%PROCESSOR_ARCHITECTURE%"), 1) <> 0 then exit sub end if dim wow64_engine ' C:\Windows\SysWOW64\xscript.exe wow64_engine = _ fso.BuildPath( _ fso.BuildPath( _ wsh.ExpandEnvironmentStrings("%windir%"), "SysWOW64"), _ fso.GetFileName(WScript.FullName)) 'wow64で実行しているか? if strcomp(wow64_engine, WScript.Fullname, 1) = 0 then exit sub end if 'wow64で実行できるか? if not fso.FileExists(wow64_engine) then exit sub end if 'wow64で起動しなおす wsh.Run quote(wow64_engine) & " " & quote(WScript.ScriptFullName) & " " & cmd(WScript.Arguments) WScript.Quit end sub
IDEで実行中か判定する
IDEで実行中か判定する関数を作ってみました。IsDebug関数は、IDEで実行中のときはTrueを、exeに固めたときはFalseを返します。
直感的だけど関数が二つに分かれるのと、
Public Function IsDebug() As Boolean Dim b As Boolean Debug.Assert IsDebug_(b) IsDebug = b End Function Private Function IsDebug_(b As Boolean) As Boolean b = True IsDebug_ = b End Function
直感的じゃないけど関数が一つの。
Public Function IsDebug(Optional b = True) As Boolean b = Not b If Not b Then Debug.Assert IsDebug(b) IsDebug = b End Function
SQLスクリプトまとめて適用バッチ
windowsクライアントで、SQLスクリプトを複数のORACLEデータベースにまとめて適用したいときに使うバッチファイルです。開発環境が何コもあると、同期をとろうと思って一環境ずつ手で適用しているとだいたい間違えます。そんなときはコレ一発で。送るに入れて使っています。
@echo off cd /d %~dp0 set db[1]=scott/tiger@db1 set db[2]=scott/tiger@db2 set db[3]=scott/tiger@db3 set count=3 for /l %%i in (1,1,%count%) do ( call echo -----------------------------------------------%%db[%%i]%%>>apply.log ( @echo off echo set sqlblanklines on echo set echo on for %%a in (%*) do echo @%%a ) | call sqlplus %%db[%%i]%%>>apply.log )