ラベル Access の投稿を表示しています。 すべての投稿を表示
ラベル Access の投稿を表示しています。 すべての投稿を表示

2010年1月23日土曜日

Access の VBA でスタックトレースを表示

メニューより 「表示 > ツールバー > デバッグ」にチェックを入れる。

img01-21-2010[7].png

VBA のコードを実行する前にブレークポイントを設置。実行して停止したところで、ツールバーの「呼び出し履歴」のアイコンをクリック。

img01-21-2010[8].png

ショートカットキーは、Ctrl + L 。表示メニューにある。

Access の VBA で複数行をコメントアウト

メニューより 「表示 > ツールバー > 編集」にチェックを入れる。

img01-21-2010[7] - コピー.png

コードを複数行選択し、ツールバーのコメントブロックのアイコンをクリック。

img01-21-2010[9].png

2010年1月11日月曜日

Access で参照整合性を設定したフィールドを含むレコードの入力をキャンセルする

参照整合性の設定

例えば、家計簿のようなものを作ろうと思い、以下のようなリレーションシップを持ったテーブルを作成した。ここでは、使った金額の 「支払い」 において、誰がどこへ支払い、その明細を管理したいもとする。 (抜粋)

CropperCapture2010[1].png

「支払」 テーブルの 「支払先ID」 フィールドは 「組織」 テーブルの ID を外部キーとして持ち、「支払人ID」 は 「支払人」 テーブルの ID を外部キーとして持ち、参照整合性を設定。

 

フォームの作成

次に、「支払」 テーブルを元にフォームを作成し、動作の確認をする。

CropperCapture2010[2].png

ここで、「支払先」 または 「支払人」 に値を入力をした後、フォームのデザインに戻りたいとする。もし、「支払先」 と 「支払人」 フィールドに参照整合性制約に抵触しない値を入力した場合、その値がデータベースに保存された後、フォームデザインの画面に切り替わる。しかし、参照整合性制約に抵触した値を入力した場合、フォームデザインに戻ろうと思っても、

バリアント型でない変数に Null 値を代入しようとしました。

とエラーダイアログが表示される。入力を取消したいと思い、入力した値を BackSpace キー消しても同じダイアログが表示。

 

Esc キーで入力のキャンセル

メニュー下の「レコードの削除」ボタンを押しても、全てのフィールドに正当な値が入力されてないとダメ。仕方がないので、とりあえず正当な値を入力しておき、その後レコードの削除するということで対応できる。しかし、これでは面倒。 (+_+)

[ACC97]: Access データベース内の関連テーブル内のレコードを編集します。 によると、

注: 入力ミスを修正、BackSpace キーを押します。 現在のフィールドに加えた変更をキャンセルするには、Esc キーを押します。 レコード全体に加えた変更をキャンセル、Esc キーを押します

(太字は引用者による)

あ~、Esc キーだったのか。基本的な操作すらわかってないな。。 ^^;

2009年5月14日木曜日

VBA のバリアント型

配列の走査

VBA で文字列の配列の初期化」のついでに配列の走査。変数 strAry が文字列の配列だとすると、

    Dim idx As Integer
    For idx = LBound(strAry) To UBound(strAry)
        Debug.Print strAry(idx)
    Next

LBound, UBound 関数が記憶の彼方にあって全く思い出せなかった。 (o_ _)o~†

 

For Each … Next

For Each を使う場合は、

    For Each elem In strAry
        Debug.Print elem
    Next

もし、上記に先立ち、

    Dim elem As String

と宣言すると、

090513-005

For Each の場合、要素を指す変数はバリアント型でないといけなかったかぁ。。 (@_@;)

ヘルプ > Microsoft Visual Basic Documentation > Visual Basic プログラミングのヒント の「For Each...Next ステートメントの使い方」によると、

For Each...Next ステートメントは、コレクションの各オブジェクトまたは配列の各要素に対して、一連のステートメント ブロックを繰り返し実行します。

また、「For Each...Next ステートメント」 には、要素が代入される変数について、

コレクションや配列の各要素を繰り返す変数を指定します。コレクションの場合、引数 element にはバリアント型 (Variant) 変数、総称オブジェクト型変数、または任意の固有オブジェクト型のオブジェクトの変数を指定できます。また、配列の場合は、引数 element にはバリアント型のみ指定できます。

 

クラスモジュール

For Each … Next と言えば、オブジェクトのコレクションを走査するときによく使った。例えば、プロパティに「名前」を持つ Person クラスを定義し、

Dim mstrName As String

Public Property Let Name(strName As String)
    mstrName = strName
End Property

Public Property Get Name() As String
    Name = mstrName
End Property

このクラスのオブジェクトをコレクションに追加し、For Each … Next で走査するには、

    Dim col As New Collection
    
    Dim p1 As New person
    p1.Name = "Tarou"
    Dim p2 As New person
    p2.Name = "Jirou"
    
    col.Add p1
    col.Add p2
    
    Dim person As person
    For Each person In col
        Debug.Print person.Name
    Next

 

バリアント型

ところで、バリアント型はあまり使ってなかったので、For Each で使えることを知らなかった。上記でも For Each で使っている要素を代入する変数 person の型宣言をせずにバリアント型としても使用できる。

ヘルプの「バリアント型 (Variant)」を見ると、

バリアント型は特殊なデータ型で、固定長の文字列型 (String) データとユーザー定義型を除く、あらゆる種類のデータを格納することができます。

(太字は引用者による)

バリアント型というと、文脈に応じて適用する演算子の意味が変化し、メモリを結構使うということしか頭に残っていない。 ^^;

バリアント型には、特殊な値 Empty 値Null 値、およびエラー値や Nothing なども格納することができます。

(同上より)

あぁ~、なんかややこしいなぁ。。 (+_+)

Empty 値 とはヘルプによると、

その変数が初期化されていないことを示し

Null 値とは、

その変数に有効なデータが格納されていないことを示します。

確認しておく。

    Dim hoge As Variant
    
    Debug.Print IsNull(hoge)   ' False
    Debug.Print IsEmpty(hoge)  ' True
    
    hoge = Null
    
    Debug.Print IsNull(hoge)   ' True
    Debug.Print IsEmpty(hoge)  ' False
    
    hoge = 0
    
    Debug.Print IsEmpty(hoge)  ' False
    
    Set hoge = Nothing
    
    Debug.Print IsNull(hoge)   ' False
    Debug.Print IsEmpty(hoge)  ' False

2009年5月13日水曜日

VBA で文字列の配列の初期化

1. 文字列の配列

VBA で文字列の配列を初期化しようと思い、次のコードを実行した。

dim strAry as String = {"hoge", "piyo", "fuga"}

しかし、エラーが表示された。(@_@;) VB6 ってこういうのできなかったかな?

VB 配列の初期化 によると、

VB6では配列を初期化することはできません。要素ごとに値をセットする必要があります。ただし、固定長の配列は既定値で初期化されています。

ちなみに .NET では上記のように書けるようだ。

仕方がないので、

    Dim strAry(2) As String
    strAry(0) = "hoge"
    strAry(1) = "piyo"
    strAry(2) = "fuga"

しかし、これは面倒だなぁ。。 (+_+)

 

2. split 関数で初期化

VB 文字列操作 に split 関数を使う方法が書かれていた。これを参考にして、

    Dim strAry() As String
    strAry = Split("hoge,piyo,fuga", ",")

こちらの方が楽。 ^^

2009年4月22日水曜日

Access のフォームにおいて、イベントが呼出されるタイミングを知りたい

1. イベントが発生するタイミングを把握する必要がある

Access が分かりにくい理由は、フォームがどういう仕組みの上で動いているかをちゃんと理解せず、場当たりに作ってしまうことにある。フォームに配置できる部品のプロパティを把握しておくことは重要。

フォームのデザインにおいて、

  1. フォームの左上隅をクリックし、
  2. プロパティを表示させると、

色々なイベント処理に対応していることがわかる。

090422-006

しかし、その数があまりにも多く、どういうタイミングで何が呼出されているか理解していないと、簡単にイベント処理の森に迷いこんでしまう。 (@_@;)

イベントの意味、発生する順序について、以下の説明がわかりやすかった。

 

2. Debug.Print で力技

どういうタイミングでイベントが呼出されているのか知りたい。

イベント処理に対して、横からキャッチできないか調べてけれど、分からなかった… (+_+)

仕方がないので、フォームのイベント全部に Debug.Print 書いて、イミディエイト ウィンドウに出力させた。

Private Sub Form_Activate()
    Debug.Print "Activate"
End Sub

Private Sub Form_AfterDelConfirm(Status As Integer)
    Debug.Print "AfterDelConfirm"
End Sub

Private Sub Form_AfterFinalRender(ByVal drawObject As Object)
    Debug.Print "AfterFinalRender"
End Sub

Private Sub Form_AfterInsert()
    Debug.Print "AfterInsert"
End Sub

Private Sub Form_AfterLayout(ByVal drawObject As Object)
    Debug.Print "AfterLayout"
End Sub

Private Sub Form_AfterRender(ByVal drawObject As Object, ByVal chartObject As Object)
    Debug.Print "AfterRender"
End Sub

Private Sub Form_AfterUpdate()
    Debug.Print "AfterUpdate"
End Sub

Private Sub Form_ApplyFilter(Cancel As Integer, ApplyType As Integer)
    Debug.Print "ApplyFilter"
End Sub

Private Sub Form_BeforeDelConfirm(Cancel As Integer, Response As Integer)
    Debug.Print "BeforeDelConfirm"
End Sub

Private Sub Form_BeforeInsert(Cancel As Integer)
    Debug.Print "BeforeInsert"
End Sub

Private Sub Form_BeforeQuery()
    Debug.Print "BeforeQuery"
End Sub

Private Sub Form_BeforeRender(ByVal drawObject As Object, ByVal chartObject As Object, ByVal Cancel As Object)
    Debug.Print "BeforeRender"
End Sub

Private Sub Form_BeforeScreenTip(ByVal ScreenTipText As Object, ByVal SourceObject As Object)
    Debug.Print "BeforeScreenTip"
End Sub

Private Sub Form_BeforeUpdate(Cancel As Integer)
    Debug.Print "BeforeUpdate"
End Sub

Private Sub Form_Click()
    Debug.Print "Click"
End Sub

Private Sub Form_Close()
    Debug.Print "Close"
End Sub

Private Sub Form_CommandBeforeExecute(ByVal Command As Variant, ByVal Cancel As Object)
    Debug.Print "CommandBeforeExecute"
End Sub

Private Sub Form_CommandChecked(ByVal Command As Variant, ByVal Checked As Object)
    Debug.Print "CommandChecked"
End Sub

Private Sub Form_CommandEnabled(ByVal Command As Variant, ByVal Enabled As Object)
    Debug.Print "CommandEnabled"
End Sub

Private Sub Form_CommandExecute(ByVal Command As Variant)
    Debug.Print "CommandExecute"
End Sub

Private Sub Form_Current()
    Debug.Print "Current"
End Sub

Private Sub Form_DataChange(ByVal Reason As Long)
    Debug.Print "DataChange"
End Sub

Private Sub Form_DataSetChange()
    Debug.Print "DataSetChange"
End Sub

Private Sub Form_DblClick(Cancel As Integer)
    Debug.Print "DblClick"
End Sub

Private Sub Form_Deactivate()
    Debug.Print "Deactivate"
End Sub

Private Sub Form_Delete(Cancel As Integer)
    Debug.Print "Delete"
End Sub

Private Sub Form_Dirty(Cancel As Integer)
    Debug.Print "Dirty"
End Sub

Private Sub Form_Error(DataErr As Integer, Response As Integer)
    Debug.Print "Error"
End Sub

Private Sub Form_Filter(Cancel As Integer, FilterType As Integer)
    Debug.Print "Filter"
End Sub

Private Sub Form_GotFocus()
    Debug.Print "GotFocus"
End Sub

Private Sub Form_KeyDown(KeyCode As Integer, Shift As Integer)
    Debug.Print "KeyDown"
End Sub

Private Sub Form_KeyPress(KeyAscii As Integer)
    Debug.Print "KeyPress"
End Sub

Private Sub Form_KeyUp(KeyCode As Integer, Shift As Integer)
    Debug.Print "KeyUp"
End Sub

Private Sub Form_Load()
    Debug.Print "Load"
End Sub

Private Sub Form_LostFocus()
    Debug.Print "LostFocus"
End Sub

Private Sub Form_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)
    Debug.Print "MouseDown"
End Sub

Private Sub Form_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single)
    Debug.Print "MouseMove"
End Sub

Private Sub Form_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single)
    Debug.Print "MouseUp"
End Sub

Private Sub Form_MouseWheel(ByVal Page As Boolean, ByVal Count As Long)
    Debug.Print "MouseWheel"
End Sub

Private Sub Form_OnConnect()
    Debug.Print "OnConnect"
End Sub

Private Sub Form_OnDisconnect()
    Debug.Print "OnDisconnect"
End Sub

Private Sub Form_Open(Cancel As Integer)
    Debug.Print "Open"
End Sub

Private Sub Form_PivotTableChange(ByVal Reason As Long)
    Debug.Print "PivotTableChange"
End Sub

Private Sub Form_Query()
    Debug.Print "Query"
End Sub

Private Sub Form_Resize()
    Debug.Print "Resize"
End Sub

Private Sub Form_SelectionChange()
    Debug.Print "SelectionChange"
End Sub

Private Sub Form_Timer()
    Debug.Print "Timer"
End Sub

Private Sub Form_Undo(Cancel As Integer)
    Debug.Print "Undo"
End Sub

Private Sub Form_Unload(Cancel As Integer)
    Debug.Print "Unload"
End Sub

Private Sub Form_ViewChange(ByVal Reason As Long)
    Debug.Print "ViewChange"
End Sub

あぁ~面倒だった。パタッ(o_ _)o~†

 

3. 試してみる

以下の処理をしたときに、どのようなイベントが発生するか試してみた。

  1. フォームを開く
  2. フィールドを修正
  3. フォームを閉じる

イミディエイト ウィンドウに、以下のように表示された。

Open
Load
Resize
Activate
Current
Dirty
BeforeUpdate
AfterUpdate
Unload
Deactivate
Close

close イベントの前に Before/AfterUpdate イベントが処理されている。Close イベントのときに、Me.Dirty が True でなかったのはこれが理由だったのか。

 

関連記事

Access の VBA でエラー処理

例えば、フォームにレコード を削除するためのボタンがあり、クリックされたときの処理を次のように記述したとする。

Private Sub btnDelete_Click()
    DoCmd.RunCommand acCmdDeleteRecord
End Sub

エラー処理を記述してないので、削除ボタンを押したときに「いいえ」を選択すると、

090422-003

実行時エラーのエラーのダイアログが表示される。

090422-002

 

エラー処理

エラーをキャッチして、エラー番号とエラー内容を表示するように変更するなら、

Private Sub btnDelete_Click()
On Error GoTo Err_Handler
    DoCmd.RunCommand acCmdDeleteRecord
    Exit Sub
    
Err_Handler:
    MsgBox Err.Number & " : " & Err.Description
End Sub

正常に処理されたとき、エラー処理に突入しないように Exit Sub の記述を忘れずに。

先ほどと同じ操作をすると、同じ内容がメッセージボックスに表示される。

090422-004

 

エラー処理の分岐

ところで、ユーザに上記のメッセージを見せる必要はないので、エラーの内容によってエラー処理を変えたい。この場合、先ほどのエラー番号で処理を分ける。

Private Sub btnDelete_Click()
On Error GoTo Err_Hander
    DoCmd.RunCommand acCmdDeleteRecord
    Exit Sub
    
Err_Handler:
    If Err.Number = 2501 Then
        Exit Sub
    Else
        MsgBox Err.Number & "/" & Err.Description
    End If
End Sub

Java ばかりいじっていたとき、エラー番号でトラップするのを見たときびびったけど ^^;

ちなみに Chapter 09 例外処理 - @IT によると、

VB.NETでは、On Error Gotoは「非構造化例外処理」という名前で呼ばれる

 

後処理がある場合

もし、次のような要件がある場合は、

  • エラーの内容に応じて処理をしたい
  • 正常、エラーを問わず後処理が必要
Private Sub btnDelete_Click()
On Error GoTo Err_Handler
    DoCmd.RunCommand acCmdDeleteRecord

Finally:
    ' 後処理
    Exit Sub

Err_Handler:
    If Err.Number = 2501 Then
        ' このエラー独自の処理
    Else
        MsgBox Err.Number & "/" & Err.Description
    End If
    Resume Finally
End Sub

(cf. Access の VBA でトランザクション処理 – CSV ファイルからデータの登録)

処理がシンプルなときは Resume Next でもいいけれど、バグを誘発しそうなので Resume ラベル名 を使うことにしようかな。

2009年4月18日土曜日

時刻を扱う - 特定の日付に属さない時間

例えば、「いつも朝の 6:30 から 6:40 までラジオ体操をする」という事柄を表現したいとする。「いつも」とあるように、この表現は特定の日付と関連したものではない。過去のある期間においてそういう事実があったことを記録したいわけでもなく、未来の予定について述べているわけでもない。「日常」という言葉通り、典型的な「生活パターン」というような抽象的なレベルの事象をどのように表現して扱うかという問題。

 

Access の日付/時刻型

さて、Access でデータを管理しようと思うと、上記の「時刻」をどのデータ型で扱えばいいのだろうか?最初に思い付くが「日付/時刻型」。(cf.  Access で使用できるフィールドのデータ型 (MDB) - Access - Microsoft Office Online)

「日常生活」と命名したテーブルを以下のように定義し、

090417-001

データを入力したとする。

090417-002

 

これに対して、次のように「年月日」を表示させるクエリを実行すると、

SELECT 日常生活.id, 日常生活.内容, Format([開始時刻],"yyyy/mm/dd hh:nn") AS 式1, Format([終了時刻],"yyyy/mm/dd hh:nn") AS 式2
FROM 日常生活;

時刻の前に `1899/12/30’ が表示される。これでは表現したいデータを素直に表現しているとは言い難い。 (+_+)

090417-003

ちなみにこの日付となる理由は、「フィールドのデータ型の設定を修正または変更する - Access - Microsoft Office Online」によると、

Access では、1899 年 12 月 30 日が日付 0 として使用されます。

 

時間について

プログラマのためのSQL 第2版 (P64) によると、

時間に関する重要な問題の 1 つに、時間には 3 種あるというのがあります。固定された出来事「彼は、13:00 に到着した」、期間「その旅は 3 時間かかる」、時間間隔「その列車は 10:00 に出発して 13:00 に到着する」は、それぞれ依存関係を持っています。 SQL-92 は、 INTERVAL データ型を導入していますが、ほとんどの現実の実装 (…) では、明白に持っていません。

(太文字は引用者による)

SQL – Wikipedia を見ると、SQL-92 で INTERVAL が追加されているのがわかる。

データ型の拡張 (可変長文字列、ビット、文字集合、日付・時刻・時間間隔 (DATE, TIME, TIMESTAMP, INTERVAL))

どうやらこの「時間間隔」というのが考える手がかりになりそうだ。

ちなみに Access と SQL のデータ型を比較した表を見ると、Access では INTERVAL 型を直接表現した型がないことがわかる。

 

ところで、Access ではユーザ定義型を作成することができない。(型とその型に適用できる演算子の定義がまともにできる RDB ってどれくらいあるのだろう?) よって、上記の要求を満たすドメインの値を素直に保持することはできない。そのため、結果として機能し得る型で代用することになる。

 

MySQL の Time 型

MySQL :: MySQL 5.1 Reference Manual :: 10.3.2 The TIME Type によると、

MySQL retrieves and displays TIME values in 'HH:MM:SS' format (or 'HHH:MM:SS' format for large hours values). TIME values may range from '-838:59:59' to '838:59:59'. The hours part may be so large because the TIME type can be used not only to represent a time of day (which must be less than 24 hours), but also elapsed time or a time interval between two events (which may be much greater than 24 hours, or even negative).

MySQL 5.1 では「時間間隔」と「一日の時刻」を同じ Time 型で表わすようだ。というより、「一日の時刻」を「時間間隔」で代用しているということかな。

 

Postgres の interval 型と time 型

Postgres は、「時間間隔」 (Interval) と「その日の時刻」(time) が分かれている。

 

Python の time オブジェクト

ところで、以前に Python で指定した日数後の日付を得るのに timedelta オブジェクト を使ったことがある。timedelta 周りを見ると time オブジェクト があるのに気がついた。(@_@)

time クラスは、

理想化された時刻表現で、あらゆる特定の日における影響から独立しており、毎日厳密に 24*60*60 秒であると仮定します ("うるう秒: leap seconds" の概念はありません)。 属性: hourminutesecondmicrosecond、 および tzinfo

(5.1.1 利用可能なデータ型 より)

まさにこれ。ただし、

time オブジェクト間の四則演算はサポートされていないので注意してください。

(time オブジェクト より)

「年月日」がなく「時・分・秒」だけので、上記の例を表現するには概念的にちょうどいい。

 

Visual Basic の日付

VB にも、似たような見かけの TimeSerial 関数 があったが、

    Debug.Print TimeSerial(6, 30, 0)
    Debug.Print Format(TimeSerial(6, 30, 0), "yyyy/mm/dd hh:nn")

実行すると、

6:30:00 
1899/12/30 06:30

あ~、内部では日付型かぁ…

@IT:連載:プロフェッショナルVB.NETプログラミング 第5回 日付時刻の取得とフォーマット によると、

日付と時刻の扱いは、Microsoft系BASICの歴史に限ってもすでに激変が起きている。初期のBASICでは、日付と時刻はそれぞれ文字列として扱われており、日付と時刻は別個の情報として扱われていた。(…)

Timeの場合日付は1899年12月30日となっているが、これはVB 6の日付を扱う起点となる日付である。(…)

VB.NETでは日付時刻の起点は0001/01/01 12:00:00である

うーむ… (@_@;)

 

Haskell の TimeOfDay 型

では、Haskell ではどうかな?と思い調べてみると、Time of day に、

data TimeOfDay

Time of day as represented in hour, minute and second (with picoseconds), typically used to express local time of day.

「時・分・秒」のみを値に持つ型がある。

先ほどの Python の Time オブジェクトと同様に、TimeOfDay 型は Num クラスのインスタンスではないので四則演算はできない。ただし、DiffTime 型という「時間間隔」を表わす型を利用すると計算を行うことができる。

最初に timeOfDayToTime 関数で TimeOfDay 型を DiffTime 型に変換。この関数は午前 0 時からの時間間隔を返してくれる。

timeOfDayToTime :: TimeOfDay -> DiffTime

090417-001

DiffTime 型は Num クラスのインスタンスなので、この型の値を足したり引いたりできる。それを上記の関数とは逆の timeToTimeOfDay で元の型に戻す。例えば、

import Data.Time
main = do
  let s = TimeOfDay 6 30 0
  print s
  let ds = timeOfDayToTime s
  print ds
  print $ timeToTimeOfDay $ ds + (secondsToDiffTime (10*60))

結果は、

06:30:00
23400s
06:40:00

 

対応する型がない場合の妥協

さて、話を元に戻して Access で時刻を扱うにはどうしよう。

一つは、年月日の情報は無視して、つまり ‘1899/12/30’ はないものとして時刻だけを「日付/時刻型」で扱う。とりあえず楽な方法。

自前でどうにかしたいなら、「時・分・秒」をそれぞれ別のフィールドで数値として扱い、それを一塊のものとして自分で思い込み、「時・分・秒」間での計算が必要なら関数を定義。ちょっと面倒だし、それに見合ういいことがあるのかわからない。

また別の方法として、NULL撲滅委員会 にあるように、

日付の列は文字型で宣言しておくべきです。(…) ほとんどの DBMS が日付型を用意していますが、私は個人的に使うメリットを感じません。暦日計算の場合など、必要なときだけキャストしています。

時刻を文字列として保持し、必要なときだけキャストする。ただし、年月日の情報は無視して日付型として計算を行う。この場合、最低でも文字列として時刻を保存するとき、時刻へとキャスト可能な形式であるか確認する必要となる。

2009年4月16日木曜日

Access の VBA でトランザクション処理 – CSV ファイルからデータの登録

トランザクションと言えば、やはりこの本。

未だ買ってすらいない。 パタッ(o_ _)o~†

昔からデータベースのこの辺がチンプンカンプン。入門レベルを読んでもトランザクション、排他制御になるとついていけない。いつかはちゃんと頭の中を整理したいのだけれど… (+_+)

 

クライアントから見たトランザクション

トランザクションの大枠については以下の図を参照。

異常なければコミット、異常があればロールバック。これだけ聞くとシンプルなのだけれど…。とりあえず、クライアントアプリがトランザクションを利用するときのテンプレートを書いておくことに。参考にしたのは以下の二つの記事。

Access の ADO については、「ActiveX Data Object : ADO入門講座」を。

 

テンプレート

以下、ADO でテーブルにレコードを追加するときのテンプレート。Java のように try ~ catch がないのでエラーハンドラと Resume を利用。

ポイントは、トランザクションが開始された後に異常が置きたらロールバック。正常異常を問わず、最後にデータベースへのコネクションを閉じる。ただし、その際コネクションが存在していることを確認すること。

Sub tmplADOTran()
On Error GoTo Err_Handler
    Dim cn As ADODB.Connection
    Dim rs As ADODB.Recordset
    Dim blnTran As Boolean
    
    blnTran = False
    
    Set cn = CurrentProject.Connection
    Set rs = New ADODB.Recordset
    
    ' トランザクションの開始
    cn.BeginTrans                   '
    blnTran = True

    rs.Open "テーブル名", cn, adOpenKeyset, adLockOptimistic
    
    ' データを列ごとに追加する
    rs.addnew
    rs!列名 = "値"
    rs.Update
    
    ' コミット
    cn.CommitTrans
    blnTran = False
    
Finally:
    If Not rs Is Nothing Then rs.Close
    If Not rs Is Nothing Then cn.Close
    
    Exit Sub
    
Err_Handler:
    ' ロールバック
    If blnTran Then cn.RollbackTrans
    MsgBox "エラーが発生しました: " & Err.Description, vbOKOnly
    
    Resume Finally
End Sub

あぁ~、何かややこしいなぁ。 (+_+)

 

例. CSV ファイルのデータを登録

上記のテンプレートを元に、CSV ファイル (test.csv) のデータをテーブルに登録するコードを書いてみる。ただし、対象のテーブルは一つ。追加する先のテーブルの列は文字列型とする。

まず、データを追加するテーブルの名称と列の名前を保持する Table クラスを作成。(挿入 > クラスモジュール) 値を保持するだけなのでインスタンス変数は Public に。

Public name As String               ' テーブルの名称
Public cols As New Collection       ' 列の名前のコレクション

上記のテーブル情報を持ったオブジェクトと、データ元となるファイルへのパスを渡すとレコードを追加する関数は以下のようになる。

Sub insert(objTbl As table, strFilePath As String)
On Error GoTo Err_Handler
    Dim cn As ADODB.Connection
    Dim rs As ADODB.Recordset
    Dim blnTran As Boolean
    
    Dim fileNo As Integer
    Dim idx As Integer
    
    blnTran = False
    
    Set cn = CurrentProject.Connection
    Set rs = New ADODB.Recordset
    
    ' トランザクションの開始
    cn.BeginTrans                   '
    blnTran = True

    rs.Open objTbl.name, cn, adOpenKeyset, adLockOptimistic
    
    ' CSV ファイルからデータを一行ずつ読み込む
    fileNo = FreeFile()
    Open strFilePath For Input As #fileNo
    Do Until EOF(fileNo)
        Line Input #fileNo, strLine
        strAry = Split(strLine, ",")
        
        ' データを列ごとに追加する
        rs.addnew
        
        idx = 0
        For Each col In objTbl.cols
            rs(col) = strAry(idx)
            idx = idx + 1
        Next
        
        rs.Update
    Loop
    Close #intFileNo
    
    ' コミット
    cn.CommitTrans
    blnTran = False
    
Finally:
    If Not rs Is Nothing Then rs.Close
    If Not rs Is Nothing Then cn.Close
    
    Exit Sub
    
Err_Handler:
    ' ロールバック
    If blnTran Then cn.RollbackTrans
    MsgBox "エラーが発生しました: " & Err.Description, vbOKOnly
    
    Resume Finally
End Sub

上記を例えば次のように呼ぶ。

Sub test()
    Dim objTbl As New table
    Dim filePath As String
    
    ' CSV ファイルのパス
    filePath = Application.CurrentProject.Path & "\test.csv"
    
    objTbl.name = "テーブル名"        ' データを追加するテーブルの名前
    objTbl.cols.Add "列名"      ' データを追加するテーブルの列の名前
    
    insert objTbl, filePath
End Sub

 

参考

2009年4月14日火曜日

ACCESS で生年月日から年齢を求める

1. 「人」テーブルの定義

以下のような、「人」テーブルがあるとする。

  1. id : 主キー
  2. 名前
  3. 性別id : 01 男、02 女
  4. 生年月日

090413-001

各々の「人」に対して、次のことを知りたい。

  • 現在の年齢
  • 今年度末に何歳になるか?

 

2. 年齢を求めるための関数を定義

現在の年齢を求める関数

生年月日から年齢を求めるには、

「今年の誕生日が来ているかどうか?」

がわかればよい。

そのため、

  1. 「指定された日付に、何歳になっているか求める」関数を定義し、
  2. 現在の日付を渡すことによって、
  3. 今の年齢を求める。

関数の定義は、

  • 「データベース > モジュール」

において、新規モジュールを作成する。

日付の関数については

を参考にした。

' ある誕生日の人が、指定した日付に何歳になっているか返す。
Function Age(birthday As Date, theDate As Date) As Integer
    If AfterThisYearsBirthday(birthday, theDate) Then
        Age = Year(theDate) - Year(birthday)
    Else
        Age = Year(theDate) - Year(birthday) - 1
    End If
End Function

' 指定された日付に今年の誕生日が来ているか?
Function AfterThisYearsBirthday(birthday As Date, theDate As Date) As Boolean
    If Month(birthday) < Month(theDate) Or _
        (Month(birthday) = Month(theDate) And Day(birthday) <= Day(theDate)) Then
        AfterThisYearsBirthday = True
    Else
        AfterThisYearsBirthday = False
    End If
End Function

 

年度末における年齢を求める関数

次に、年度末に何歳になっているか知りたい。

上記と同様に「指定された日付の年度末を返す」関数を書く。

' 指定された日付の年度末の日付を返す
Function Nendomatu(theDate As Date) As Date
    Const DATE_331 = "/3/31"
    If 4 <= Month(theDate) And Month(theDate) <= 12 Then
        Nendomatu = CDate(Year(theDate) + 1 & DATE_331)
    Else
        Nendomatu = CDate(Year(theDate) & DATE_331)
    End If
End Function

 

3. クエリから関数を呼び出す

上記の関数を、クエリから呼出すようにする。

  1. 「人」テーブルを対象にしたクエリを作成し、
  2. 「年齢、年度末の年齢」を求めたい列で、右クリック > ビルド... を選択し、
  3. 式ビルダを呼出す。

左下を見ると、「関数」フォルダに、先ほど作成したモジュール名が表示され、定義した関数が属しているのがわかる。

090413-003

それぞれ以下のように定義する。

年齢: Age([生年月日],Now())
年度末の年齢: Age([生年月日],Nendomatu(Now()))

クエリの結果。今日の日付は 2009.4.13 。

090413-002

 追記(2009.4.25) : 学校の学年においては、 4/1 生まれは早生まれになるので、年度末の翌日における年齢を求める。

年度末の年齢: Age([生年月日],nendomatu(Now())+1)

(参考: 法制執務コラム集「4月1日生まれの子どもは早生まれ?」)

 

4. クエリの中だけで計算するには

生年月日から年齢を計算する方法」には、次のようなクエリが書かれていた

年齢: IIf(Right(Format([誕生日],"yyyy/mm/dd"),5)>Right(Format(Now(),
"yyyy/mm/dd"),5),DateDiff("yyyy",[誕生日],Now())-1,DateDiff("yyyy",
[誕生日],Now()))

(via 生年月日の入力によって年齢を出したい。 --Access Club 超初心者 FORUM—)

うへぇ~、一行だと読みにくい… (+_+)

そもそも、これは何だろう?

Right(Format([誕生日],"yyyy/mm/dd"),5)

関数をテストしてみる。

    Debug.Print CDate("1/1")    ‘ 2009/01/01  
    Debug.Print CDate("1")      ‘ 1899/12/31 
    Debug.Print CDate("366")    ‘ 1900/12/31 
    Debug.Print CDate("0")      ‘ 0:00:00 

CDate のヘルプを見ると、

CDate    日付型 (Date)    任意の有効な日付式

「日付式」とは、

日付として解釈可能な式。… 日付と解釈できる文字列、

結果を見ると、そこまで無理に解釈しなくても…と言いたくなるような仕様。 ^^;

日付の内、「年」を省略すると、「現在の年」と解釈されるようだ。

 

5. シンプルな計算方法

生年月日から年齢を計算する簡単な計算式:ITpro」には、次のような計算式が書かれている。

(今日の日付-誕生日)/10000の小数点以下切捨て。

なるほど、「年月日」をわざと一連の数値として扱う方法のようだ。確かに、年月だけ見れば、年月を各々二桁で表現しているなら、誕生日が来ていれば、今日の日付よりも誕生日の数値が小さくなる。

先ほどの方法でパフォーマンスが悪ければ、こちらに変更しようかな。

ただし、次の注意点が述べられている。

「誕生日の前日が終了する瞬間(すなわち誕生日をむかえる午前0時00分の直前)に1歳を加えることになる」の部分の解釈には前日に1日加えると解釈される場合と当日に1日加えると解釈される場合の2種類があるようです。特に自治体においては前者で解釈される場合が多いとのことです。

(同上より)

ん?これは自治体の特殊ルールで、「年齢」と「年齢をどう解釈するか」とは別の話だよねぇ。

2009年4月1日水曜日

Access で起動時の設定を元に戻す - 隠したデータベースウィンドウを表示したい

1. ユーザにデータベース ウィンドウを見せないための設定

データベースを利用するユーザに、余計なメニューを見せたり、データベース ウィンドウを見せたくない。そのために、メニューより

  • ツール  > 起動時の設定

において、チェックをいくつか外した。

090401-001

その結果、データベースを起動すると、以下のようにすっきりとしたメニューになる。

090401-002

 

2. データベースの表示を初期状態に戻す

上記の設定をした後、カスタマイズしたくなったとき、データベース ウィンドウをどうやって表示するのだろう ?

起動時の設定を初期状態に戻すには」によると、

「起動時の設定」 を初期状態に戻したいファイルを、[Shift] キーを押しながら開きます

エクスプローラにおいて、初期状態に戻したいデータベースを開き、セキュリティ警告のダイアログが表示されたら、シフトキーを押しながら「開く」ボタンを押せばよい。

090401-003

2009年3月31日火曜日

Access でデータベースを分割する

1. データベースの分割

Access では、入力フォームとテーブルを、別々のデータベースに分割することができる。

Microsoft Access – Wikipedia によると、

1つの設計技術はAccessのアプリケーションをデータとプログラムに分割することである。1つのデータベースはテーブルとリレーションシップのみを含むべきであり、一方他のデータベースはすべてのプログラム、フォーム、レポート、及びクエリを含み、最初のデータベースのテーブルにリンクする。

自動的に分割する

データベースの分割を行うには、メニューより

  • ツール > データベース ユーティリティ > データベース分割ツール

これにより、データを含むバックエンドのデータベースを自動で作成してくれる。

 

手動で分割

これに対して、

には、手作業でデータベースを分割する方法が説明されている。

簡単に方法をまとめると、

  1. 分割前のデータベースから、テーブルだけを別のデータベースにインポートする。(バックエンド)
  2. 分割前のデータベースから、フォームとクエリを別のデータベースにインポートする。(フロントエンド)
  3. リンクテーブルの機能により、フロントエンドから、バックエンドのデータベースにあるテーブルを指定する。

3399710217_747a9bbee1

データベースを分割するメリットは、次のように述べられている。

データベースを分割する最も一般的な理由は、ネットワーク上で複数のユーザーがデータベースを共有していることです。ネットワーク共有に単純にデータベースを保存している場合は、ユーザーがフォーム、クエリ、マクロ、モジュール、またはレポートを開くと、これらのオブジェクトがネットワークを経由して、データベースを使用する各ユーザーに送信されます。データベースを分割すると、各ユーザーに、フォーム、クエリ、マクロ、モジュール、およびレポートの自分用のコピーを与えられます。このため、ネットワークを経由して送信する必要があるデータは、テーブルのデータのみとなります。

(Access 2002 または Access 2003 で Microsoft Access データベースを手作業で分割する方法 より)

 

2. リンクしたテーブルの更新

バックエンドデータベースに一度リンクした後、フォームを別のデータベースに接続したい場合は、メニューより

  • ツール > データベース ユーティリティ > リンク テーブルマネージャ

において、「リンク先を更新するためのプロンプトを毎回表示する」にチェックを入れ、更新が必要なリンクを選択して OK ボタンを押す。

090331-009

Access でコンボボックスの「リスト外入力時」のメッセージを変更

テーブルの構造とルックアップの設定

「人」テーブルと「都道府県」テーブルがあるとする。

「人」テーブルは「都道府県」を参照。 id は主キーで、CD は候補キー。 id はシステムがタプルを一意に識別するためのもので、CD も一意に識別することができるが人間の都合で変更の可能性がある。

090331-003

 

「都道府県」を参照しているフィールドは、「ルックアップ」で「コンボボックス」が表示されるようにする。

090331-004

 

クエリからフォームを作成

上記のテーブルから下記のクエリを作成。

090331-005

 

これを元にフォームを作る。

このとき、フォームにおける「都道府県」の入力は CD (コード) でしようと考えていたので、「あれ? id を入力しなくていけないかな?どうしよう… (@_@;) 」と思ったけれど、上記の通り「人」テーブルの「都道府県id」フィールドには「ルックアップ」が設定されているので、id の入力をあたかも CD を入力してるかのようにフォームのコンボボックスで入力できる。

 

リスト外入力時のイベント

さて、ここで「都道府県」テーブルにない CD を入力しようとすると、

指定した項目はリストにありません。

とメッセージが表示される。

090331-007

 

このメッセージではユーザにとってわかりにくいので変更したい。

コンボボックスのプロパティより「イベント > リスト外入力時」の VBA を記述する。

Private Sub 都道府県id_NotInList(NewData As String, Response As Integer)
    Response = acDataErrContinue
    MsgBox "存在しない都道府県コードです"
End Sub

引数 Response の設定で先ほどのメッセージを抑制して、新たにメッセージを表示する。

 

参考

2009年3月24日火曜日

Access の VBA で関数をちょっと試すには

1. VBA で関数の動作を調べたい

試したい関数があるとき、

  1. フォームでコマンドボタン作り
  2. 関数をボタンを押したのイベントに記述し、
  3. MsgBox で出力する

という方法は手間がかかる。

 

2. イミディエイトウィンドウを使う

データベースウィンドウで右クリック > Visual Basic Editor を選択。

090324-007

表示 > イミディエイトウィンドゥ」 を選択しておく。

標準モジュール」の中でプロシージャを作成し、

  • F5 で実行。
  • F8 でデバッグ。

これにより、イミディエイトウィンドウに結果が表示される。

090324-005

関数の場合は「表示 > ローカルウィンドウ」 を表示しておくと良い。

2009年3月22日日曜日

Access のコンボボックスにおける Value と Text プロパティの違い

以下のように、前回と同じテーブルを使う。

090322-003

 

「人」テーブルの「性別id」は、前回最後に作ったクエリを元にルックアップの設定をしてある。

データの値だけちょっと変えておく。性別コードを二桁にした。

090322-001

 

ここで例えば、「人」テーブルを元に以下のようなコンボボックスを含むフォームを作成したとする。コンボボックスのコントロールソースは「性別id」。

090322-002

 

繰り返すが「性別id」は、クエリを元にルックアップの設定がしてある。

test ボタンを押すと VB のイミディエイトウィンドウにコンボボックスの値が出力されるようにした。コードは次の通り。

Private Sub btnTest_Click()
    ' Value プロパティ
    Debug.Print "性別id.Value: " & 性別id.Value
    
    ' Text プロパティ
    性別id.SetFocus
    Debug.Print "性別id.Text: " & 性別id.Text
End Sub

結果、次のような出力が得られた。

性別id.Value: 4
性別id.Text: 09

Text プロパティを取得する前にフォーカスを与えているのは、

コントロールの Text プロパティを取得または設定するには、コントロールにフォーカスを与えておく必要があります。フォーカスを与えないと、エラーが発生します。

(ヘルプの「Text プロパティ」より)

上記の結果を見ると、Text プロパティはコンボボックスに表示された値そのものを取得している。それに対して、Value プロパティは「データ > コントロールソース」である「性別id」の値が表示された。このような違いが生じたのは、コンボボックスの「データ > 集合ソース」にルックアップの設定で使っているクエリが設定されているためということか。

Access でルックアップの設定 - テーブル定義とフォームにおけるコンボボックスにおいて

1. テーブル定義とリレーションシップの設定

「人」と「性別」テーブルがある。

「人」テーブルの「性別id」が、「性別」テーブルの「id」を参照する。

090322-003

「性別」テーブルの内容は、以下の通り。

090322-002

id はシステムが自動的に割り当てる数値。CD は性別コード。 id は不変とし、性別コードはユーザが変更可能な値とする。

ルックアップを利用しないで、「人」テーブルにデータを入力する場合、「人」テーブルの「性別id」に、「性別」テーブルの「id」に存在する値を、直接入力しなければならない。

090322-004

 

2. テーブルにおけるルックアップの設定

ルックアップの機能を利用しないと、性別を入力しにくい。

「人」テーブルにおいて、「性別id」フィールドの「ルックアップ」を設定する。

  1. 「表示コントロール」を「コンボボックス」に変更。
  2. 値集合ソース」をリレーションを設定した「性別」テーブルにする。
  3. 連結列」を 1 に設定。理由は、「性別」テーブルの id が、1 列目に存在するため。
  4. 「列数」を 3 に設定。。この値が、「人」テーブルにおいて「性別id」を入力するときのコンボボックスの列数に対応する。
  5. 「列幅」は、上記の「列数」で表示した列を表示する長さ。
  6. 「リスト幅」は、上記の「列幅」の合計。

よく忘れるのは「連結列」の意味。本来、RDB の列に順序はない。

090322-006-2

ルックアップの設定をした場合、「性別コード」を入力するとき、「性別の名称」を確認しながら入力できる。ただし、「人」テーブルのデータを一覧するとき、性別id には性別コードではなくて id が表示されるのでわかりずらい。

そこで、「列幅」の先頭を 0 cm に設定し、全体の「リスト幅」を 4 cm に変更する。

090322-008-2

これにより id が表示されなくなり、性別コードが先頭に見える。テーブルの内容を一覧したとき、性別コードで表示されるため、データの内容を把握しやすい。

 

3. クエリをルックアップ

上記の例では、ルックアップする先が「テーブル」だった。(「値集合タイプ」がテーブル)。

ルックアップ先は、「連結列」、つまり順序で指定している。このため、ルックアップ先のテーブルの列の順序を変更すると、影響を与える。

そのため、「性別」に対するクエリ (q_性別) を作成しておき、「値集合タイプ」でクエリを参照する方が良い。

090322-011

「性別id」のルックアップの設定は、

  1. 「値集合ソース」をテーブルからクエリに変更
  2. 「連結列」を 3 に設定。 (クエリにおいて、id を 3 列目にしたため。)
  3. 「列幅」を 1cm, 3cm に変更。(3 列目の id の設定は不要。)

090322-012

これにより、元のテーブルを変更しても、ルックアップの設定に影響が及ばなくなる。

 

4. フォームのコンボボックスにおけるルックアップの設定

追記 (2010.1.16) : フォームのコンボボックスにおけるルックアップの設定も、テーブルのときと同様に考えればよい。

コンボボックスにしたいコントロールを

  • 「右クリック > コントロールの種類の変更 > コンボボックス

を選択。

 

ルックアップするソースの設定
  1. コントロールの「プロパティ > データ」で 値集合タイプを「テーブル/クエリ
  2. 値集合ソースで、ルックアップしたいテーブル、または、クエリを選択。
  3. ルックアップしたいテーブル、または、クエリの主キーが1 行目に設定されている場合、連結列を `1’ とする。

img01-16-2010[2].png

 

コンボボックスの表示の設定

表示されるコンボボックスの列を、以下のようにしたいとする。

ID, CD, 名称

ただし、主キーである ID はユーザに見えないようにしたい。

先ほどのコンボボックスのプロパティにおいて、書式タブを選択。

  1. 列数を 3
  2. 列幅で 3 列それぞれの長さを設定。ただし、1 列名の ID が表示されないように 0 とする。ここでは一々単位である cm を入力する必要はない。
  3. リスト幅は上記で設定した値の合計。

img01-16-2010[1].png

 

参考

 

関連記事

2009年3月20日金曜日

コンボボックス、セレクトボックスのメニューを開くためのショートカットキー

1. コンボックスを開くためのショートカットキー

Access でテーブルを定義するとき、データ型はコンボボックスで選択する。

一々マウス使い、メニューから選択するのは面倒。

090320-001

「はじめて作るAccessアプリケーション」(p57) によると、

コンボボックスからの選択操作にマウスと Alt + ↓ または F4 キーしか使えないならば、…

(太字は引用者による)

 

2. ブラウザのセレクトボックスを開くためのショートカットキー

セレクトボックスを表示するために、

にあるサンプルを用いた。

Firefox
Firefox では、
  • Alt + ↓
  • F4

でセレクトボックスが開いた。Access のコンボボックスのショートカットキーと同じ。

 

Google Chrome
Google Chrome では、
  • Alt + ↓

F4 を押しても反応なし。カーソルキーで、セレクトボックスのメニューのフォーカスを切り替えることはできる。

 

Opera
Opera では
  • Alt + ↓

F4 は、別の操作のショートカットキーが割り当てられている。

 

Internet Explorer

IE は、Opera と同様
  • Alt + ↓

 

Alt + ↓  のショートカットキーはどのブラウザでも使える

比較してみると、 Alt + ↓ が多くのアプリで使えるようなので、こちらで手を馴染ませておこうか。

しかし、Firefox は、F4 でセレクトボックスを開くことができるのは楽。

 

3. コンボボックスとセレクトボックスの違い

「リストから一つを選択する」

UI の名称は、「コンボボックス」や「セレクトボックス」と呼ばれる。

どちらが一般的な名称なのだろう?

VB では「コンボボックス」と呼ばれ、HTML のフォームだと「セレクトボックス」言う。

Gallery lumber-mill - コンボボックス?セレクトボックス? によると、

コンボボックスは(ユーザの自由な入力を受け付ける点で)機能が大きく異なるため、混同しないようにすべきでしょう。

コンボボックス - Wikipedia

ドロップダウンリストまたはリストボックスを1行のテキストボックスと組み合わせたもので、ユーザーは値を直接入力することもできるし、既存のオプションから選択することもできる。グラフィカルなウェブブラウザのアドレス入力バーは、一般にコンボボックスになっている。

ユーザが入力できるリストがコンボボックス。

List box - Wikipedia

A list box is a GUI widget that allows the user to select one or more items from a list contained within a static, multiple line text box.  …

  • Drop down list - Like a list box, but not permanently expanded to show the elements of the list.
  • Combo box - Like a drop down list, but users also can make entries not on the list.