ラブびあ

ビール。ときどきラブ

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ファイルを作ってください。

スクリプトでやっていることは、

  1. テンプレートファイルを読み込む(ReadText)
  2. 予約語を動的パラメータに置換する(Replace)
  3. ファイルに保存する(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

hoge.sql

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
)