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

2012/09/05

【Access】パラメータクエリにフォームのコントロール割り当てた時の問題点【VBA】

 今日も今日とて悩める後輩君。例によって頭を抱えていたので聞いてみた。
 以下、内容。
とある同一のクエリを使用し、フォーム(単票)とサブフォーム(データシート)を作成した。フォームとサブフォームの関係は親子ではなく対等であり、サブフォームをフォームのナビゲーションとして利用する。要はサブフォーム側のレコードを移動するとフォーム側のレコードも同じレコードに移動するというものである。
若干状況が分かりにくいのでここで一端サンプルとイメージを提示しときます。

テーブル
Books
Id
BookName
Price
Sales
Id
BookId
SaleDate
Num


クエリ
SaleList
Id
SaleDate
BookName
Num
Sale
(Num*Price)

フォーム
2012090501
ソース
'サブフォームのレコード移動時イベント
Private Sub Form_Current()
 
    '親フォームのレコードを自身のレコードのインデックスに移動する
    DoCmd.GoToRecord acDataForm, "SalesForm", acGoTo, Me.CurrentRecord
 
End Sub

 こんな感じです。こんな風にしたらって言ったのは自分ですが、面倒なことをさせてしまったもんです。…さて、後輩君もここまでは何も問題なく作成しておりました。

 問題はここからです。以下、再び内容。
フォームに使用したクエリに外部のフォームで確定した条件で抽出する必要が発生した。クエリをパラメータクエリに変更し、対象となるフォームのコントロールを参照するよう変更する。
これだけだと何が問題なのって感じですよね?とりあえずその通りにサンプルを追加・変更してレコード移動してみます。

フォーム(パラメータ設定用)
2012090502

クエリ
SaleList
Id
SaleDate
(Where: SaleDate = Forms![ParamForm]![SaleDateText])
BookName
Num
Sale
(Num*Price)

実行時
2012090503
 あれ?サブフォームのレコード移動するとこけるようになっちゃいましたってなわけです。う~ん、細かく見てみましたがぶっちゃけはっきりとした原因は分かりませんでした。流れ的にはサブフォーム側の移動が実行され、フォーム側の移動が実行されるんですが、ここで終了せず何でかサブフォームの先頭レコードにフォーカスが移っちゃって再度同じイベントが発生して…って感じです。自分の推測としては移動後にパラメータクエリのフォーム参照が再評価に走ってフォームのフォーカスが飛んじゃってるんじゃないかなって感じです。すごく不確かですが。こりゃ後輩君も悩むよねって話です。
 とりあえず後輩君にはこのやり方自体諦めて違う手段を考えようって言いました。後輩君には教えなかったんですが自分的な代替え案としてはパラメータクエリを諦めて、外部フォームを参照するのをクエリではなくフォームにしてフィルタで対応したらってとこですね。一応サンプル載せときます。当然、後輩君はこのブログを知りません。

ソース(メインフォーム側)
'メインフォームのオープン時イベント
'Loadで書くとフィルタの挙動がおかしくなったのでこちらにしました
Private Sub Form_Open(Cancel As Integer)
 
    '外部フォーム(ParamForm)の設定値を取得
    Dim param As String
    param = Forms!ParamForm!SaleDateText
    
    '自身のフィルタ設定
    Me.Filter = "SaleDate = #" & param & "#"
    Me.FilterOn = True
    
    'サブフォームのフィルタ設定
    Me.SubForm.Form.Filter = "SaleDate = #" & param & "#"
    Me.FilterOn = True
    
End Sub


HP Directplus オンラインストア

2012/08/29

【Access】複数のテーブルを1つのフォームで更新する方法を考える【VBA】

 後輩君が何やら頭を抱えて悩んでいたので聞いてみた。

「現在とある業務で使用してるAccessのテーブル「A」と「B」と「C」は正規化の観点からするとひとつのテーブルに集約することができるが、フィールド数が多くなることと、業務の流れとして「A」が確定してから「B」と「C」を入力するということであえて分割してある。現在、それぞれのテーブルを更新・削除を行う場合、個別のフォームを用いているが、作業量が増えてきた関係で煩わしくなってきた。更新・削除を1つのフォームから行うことはできないだろうか?」

 …というのが悩みの種のようです。ちなみに後輩君のレベルは簡単なクエリとそれに基づいたフォームが作れるくらいです。確かにちょっと悩んじゃうかもしれません。個人的には追加部分を工夫するとして正規化しちゃえばいいじゃんってとこですが、まぁ、そうできないケースも今後発生するかもしれないのでとりあえずこのまま挑戦させてみようかなぁってことで、このブログでは後輩君をほっておいて自分的な考察やらコードをあげちゃいます。聞くだけ聞いてなんなんですが後輩君には自分で考えてもらいます。

 とりあえずサンプルデータです。「A」「B」「C」のテーブルは「A」を基点としてそれぞれ1対1で連結しています(フィールド数が少ないことは気にしない)。

A B C
Id AName Id AId BName Id AId CName
1 太郎 1 1 犬 1 1 オス
2 花子 2 2 猫 2 2 メス


 次に根本的な考え方ですが、フォームとテーブル(クエリ)の連結は最初からあきらめちゃいます。どうやら後輩君が1番引っかかってる部分のようですが、1つのフォームに複数のテーブルを連結することはできません(たぶん)。サブフォームを2つ使えばって考えもありますがその場合、更新のタイミングの問題も出てくると思います(例えばBを更新する際、AとCの更新は?みたいな)。イメージ的には下図のようになります。

フォームに表示する流れ
image

更新・削除をする流れ
image

 VBA上でSQL文を使用して「A」「B」「C」各テーブルと「A」「B」「C」を結合した独自型をつなげ、その独自型とフォームを連動させます。独自型を使うのははっきり言って趣味です。飛ばして直でフォームにいっちゃっても問題ないと思います。それでは流れに基づいてメソッドを作ってみます。

Option Compare Database
Option Explicit
 
'独自型
Public Type MyData
    Id As Integer  'このIdはAのId
    AName As String
    BName As String
    CName As String
End Type
 
'生成メソッド
Public Function GetMyData(targetId As Long) As MyData
 
On Error GoTo ErrProcess
 
    Dim cn As ADODB.Connection
    Set cn = CurrentProject.Connection
    
    Dim selectSql As String
    selectSql = "SELECT A.Id, A.AName, B.BName, C.CName " _
        & "FROM (A INNER JOIN B ON A.Id = B.AId) " _
        & "INNER JOIN C ON A.ID = C.AId WHERE A.Id = " & targetId
    
    Dim rs As ADODB.Recordset
    Set rs = New ADODB.Recordset
    rs.Open selectSql, cn, adOpenKeyset, adLockReadOnly
    
    Dim result As MyData
    
    '0件はエラー、2件以上については今回は考えません
    If (rs.RecordCount = 0) Then
        MsgBox "レコードを取得できませんでした。", vbExclamation, "エラー"
    Else
        result.Id = rs!Id
        result.AName = rs!AName
        result.BName = rs!BName
        result.CName = rs!CName
    End If
    
    rs.Close: Set rs = Nothing
    cn.Close: Set cn = Nothing
    
    GetMyData = result
    
Exit Function
 
ErrProcess:
 
    rs.Close: Set rs = Nothing
    cn.Close: Set cn = Nothing
    
    MsgBox Err.Description, vbCritical, "エラー"
    
End Function
 
'更新メソッド
Public Sub UpdateMyData(updateData As MyData)
 
On Error GoTo ErrProcess
 
    Dim cn As ADODB.Connection
    Set cn = CurrentProject.Connection
    
    Dim updateSqlA As String
    updateSqlA = "UPDATE A SET AName = '" & updateData.AName & "' " _ 
        & "WHERE Id = " & updateData.Id
 
    Dim updateSqlB As String
    updateSqlB = "UPDATE B SET BName = '" & updateData.BName & "' " _ 
        & "WHERE AId = " & updateData.Id
    
    Dim updateSqlC As String
    updateSqlC = "UPDATE C SET CName = '" & updateData.CName & "' " _ 
        & "WHERE AId = " & updateData.Id
    
    cn.Execute (updateSqlA)
    cn.Execute (updateSqlB)
    cn.Execute (updateSqlC)
    
    cn.Close: Set cn = Nothing
    
Exit Sub
 
ErrProcess:
 
    cn.Close: Set cn = Nothing
    
    MsgBox Err.Description, vbCritical, "エラー"
    
End Sub
 
'削除メソッド
Public Sub DeleteMyData(deleteData As MyData)
 
On Error GoTo ErrProcess
 
    Dim cn As ADODB.Connection
    Set cn = CurrentProject.Connection
    
    Dim deleteSqlA As String
    deleteSqlA = "DELETE FROM A WHERE Id = " & deleteData.Id
    
    '連鎖削除設定してたら以下のBとCのDELETE文は不要
    Dim deleteSqlB As String
    deleteSqlB = "DELETE FROM B WHERE AId = " & deleteData.Id
    
    Dim deleteSqlC As String
    deleteSqlC = "DELETE FROM C WHERE AId = " & deleteData.Id
    
    cn.Execute (deleteSqlA)
    cn.Execute (deleteSqlB)
    cn.Execute (deleteSqlC)
    
    cn.Close: Set cn = Nothing
 
Exit Sub
 
ErrProcess:
 
    cn.Close: Set cn = Nothing
 
    MsgBox Err.Description, vbCritical, "エラー"
    
End Sub


 やってることはシンプルにしたつもりです。あとはリスト型のフォームと入力用のフォームを用意して各メソッドを呼び出してやれば完成です。さすがにフォームは省略させて下さい。


 以上が自分なりの後輩君の悩みへの解決方法です。いやぁ、後輩君はどんな方法で解決するか楽しみですね~。簡単にギブアップしないことを祈ってます。

2012/08/04

【Access】自作自演で課題をやってみる4

前回の続きです。フォームとレポートを簡単にやっつけちゃいます。

まずフォームから作ります。選出の際、年と月が必要になるのでそれを決定し、レポートを表示するってイメージで。



あっさりしてますがこんな感じでよいでしょう。決定ボタンのイベントはレポートが完成してから書きます。

次にレポートです。とりあえずレポート用のクエリを作ってやります。クエリのSQLビューをまんまコピーするとこんな感じです。

SELECT Employees.EmployeeNo, Employees.EmployeeName, Employees.ChoseDate
FROM Employees
WHERE (((Month([ChoseDate]))=[Forms]![MainForm]![MonthComboBox]));

月で抽出するため、WHERE区で先ほど作ったフォーム(MainForm)の月のコンボボックスを参照しています。

ではクエリを元にレポート作成します。



「ChoseDate」を週単位でグループ化しています。レポートビューにするとこんな感じ。



まぁ、サイズのことは気にしないで下さい。後はこれをフォームの決定ボタンで表示されるようにするだけです。

以下、決定ボタンのクリックイベントです。

'決定ボタンクリックイベント
Private Sub CommitButton_Click()

Dim targetYear As Integer
targetYear = Me.YearCombobox.Value

Dim targetMonth As Integer
targetMonth = Me.MonthComboBox.Value

Dim resultDay As Variant

For Each resultDay In Work.GetSunday(targetYear, targetMonth)
Work.ChoiceEmployee (CDate(resultDay))
Next

DoCmd.OpenReport "ChoiseReport", acViewPreview

End Sub

これで無事完成です。まぁ本当はもっといろんなケースを考えた方がよいでしょうけど(重複実行した場合とか)、とりあえずはこれにて第1回「自作自演で課題をやってみる」は終了です。


2012/08/03

【Access】自作自演で課題をやってみる3

前回の続き。

SQL文が出来たのでそれを使ってVBAを組んでみます。個人的な趣味で変数の使い回しやレコードセットの使い回しは禁止してますので若干長くなりがちですが気にしません。

'targetDate = 抽出日
Sub ChoiceEmployee(targetDate As Date)

'選出人数管理用
Dim choiceNum As Integer
choiceNum = 0

Dim cn As ADODB.Connection
Set cn = CurrentProject.Connection

'最初の選出SQL文
Dim selectSql1 As String
selectSql1 = "SELECT TOP 5 * FROM Employees WHERE ChoseDate = #9999/12/31# ORDER BY Rnd([Id])"

Dim rs1 As ADODB.Recordset
Set rs1 = New ADODB.Recordset
rs1.Open selectSql1, cn, adOpenKeyset, adLockOptimistic

'もしレコードが0件なら1巡してるので初期化
If rs1.RecordCount = 0 Then
Dim updateSql1 As String
updateSql1 = "UPDATE Employees SET ChoseDate = #9999/12/31#"
cn.Execute updateSql1
Else
Do Until rs1.EOF
'選出日をtargetDateで更新
Dim updateSql2 As String
updateSql2 = "UPDATE Employees SET ChoseDate = #" & targetDate & "# WHERE Id = " & rs1![Id]
cn.Execute updateSql2
choiceNum = choiceNum + 1
rs1.MoveNext
Loop
End If

rs1.Close
Set rs1 = Nothing

'選出人数が足りない場合
If choiceNum < 5 Then
'2回目の選出SQL文
Dim selectSql2 As String
selectSql2 = "SELECT TOP " & 5 - choiceNum & " * FROM Employees WHERE ChoseDate <= #" & DateAdd("m", -3, targetDate) & "# ORDER BY Rnd([Id])"

Dim rs2 As ADODB.Recordset
Set rs2 = New ADODB.Recordset
rs2.Open selectSql2, cn, adOpenKeyset, adLockOptimistic

Do Until rs2.EOF
'選出日をtargetDateで更新
Dim updateSql3 As String
updateSql3 = "UPDATE Employees SET ChoseDate = #" & targetDate & "# WHERE Id = " & rs2![Id]
cn.Execute updateSql3
Loop

rs2.Close
Set rs2 = Nothing
End If

cn.Close
Set cn = Nothing

End Sub

またまた個人的な趣味ですがレコードセットを直接更新かけるのが嫌いなのでUPDATEのSQL文を書いてます。別にレコードセットを直接「rs![ChoseDate] = 日付」&「rs.Update」とかやっちゃってもかまいません。

次にレポート作成の前にもう1つレポート印刷時に実行する処理をついでに組んでしまいましょう。作成条件の「毎月、月初めにその月の掃除当番表を作成し、出力する。」ってやつで「選出の実行はこれが出力されるタイミングでやっちゃえば良いでしょう」なんて考えてました。なので流れは…月を入力→各週の当番選出→印刷って感じかなぁ。

選出メソッドは出来てるので、対象月の各週毎にそれを実行すること考えます。まず基準日がいりますね。これは特に指定してませんので各週の日曜日にしちゃいます。なので対象月の全ての日曜日の日付を取得する必要があります。

'targetYear = 対象年
'targetMonth = 対象月
Function GetSunday(targetYear As Integer, targetMonth As Integer) As Date()

'その月の1日の曜日番号を取得
Dim targetWeekNum As Integer
targetWeekNum = Weekday(DateSerial(targetYear, targetMonth, 1))

'最初の日曜日を取得
Dim firstDay As Date

If 1 = targetWeekNum Then
firstDay = DateSerial(targetYear, targetMonth, 1)
Else
firstDay = DateSerial(targetYear, targetMonth, 1 - targetWeekNum + 8)
End If

Dim tempDay As Date
tempDay = firstDay

Dim result() As Date
Dim arrayNum As Integer
arrayNum = 0

'月が変わるまでループ
Do While targetMonth = Month(tempDay)
ReDim Preserve result(arrayNum)
result(arrayNum) = tempDay
tempDay = tempDay + 7
arrayNum = arrayNum + 1
Loop

GetSunday = result

End Function

こんな感じでどうでしょう。最初の日曜を求める部分の「DateSerial(targetYear, targetMonth, 1 - targetWeekNum + 8)」は分かりづらいかもしれませんが計算したらそうなったって感じです。

だいぶ完成が見えてきました。次回はフォームとレポートをちゃっちゃっと作ってしまいましょう。


【Access】自作自演で課題をやってみる2

さて前回の続きです。今回はとりあえずクエリかSQL文の作成から始めます。クエリとSQL文どっちにしようか迷いましたが、個人的な趣味でSQL文を使用することにします。こう書くとクエリとSQL文が別物のように受け止められそうですが根本的にはクエリもSQL文です。アプリ上で定義するかしないかの違いだけです。個人的にパラメータクエリが面倒なのでSQL文の方が好きなだけです。

では早速1つめのSQL文(VBAに書くことを前提にしてます)です。社員テーブル(Employees)からまだ掃除当番に選出されていない人を5名ランダムに抽出します。

=“SELECT TOP 5 * FROM Employees WHERE ChoseDate=#9999/12/31# ORDER BY Rnd([Id])”

前回、載せ忘れてましたが、未選出の人は9999/12/31にしちゃいます。要はこれが選出・未選出のフラグです。SQL文のポイント最後のORDER BY区です。ここにAccessの持つ「Rnd関数」に自身の「Id」を割り当て、レコード毎に乱数を発生させ、それを並べ替えちゃってます。「Id」をseed値として扱うため基本的に乱数が重複することはありません。若干分かりにくい気もしますがRnd関数についての詳しい解説はヘルプを参照してみてください。

次に2つめ。上記SQL文で5名決定できなかった場合、つまり未選出が5名以下になった状態での抽出です。

=“SELECT TOP “ & <必要選出数> & “ FROM Employees WHERE ChoseDate <= #“ & <3ヶ月前> & “# ORDER BY Rnd([Id])”

<>部分がパラメータです。TOPとWHERE区に対してVBA上でパラメータを当てます。ちなみにクエリのデザイナじゃTOPに対してうまいことパラメータが当てられないかもしれません(他の方法を考えた方が早そうな気がします)。

うーん。ループを使わなくても意外とシンプルに解決しちゃっいました。もうちょっと問題捻ったようが良かったかなぁと思ったり…。まぁ、後輩は十中八九クエリでやろうとするからちょっと見物かも。もし、自分がクエリを定義してやるとしたら2つめのクエリはTOPの条件を1にしてしまって、WHERE区に「DateAdd関数」でも使って5名抽出できるまでループにかけちゃうかなぁ。その場合、抽出されるたびにそのレコードを抽出済みにする必要がある気がします。

ここからVBAまで書いちゃうとまた長くなっちゃうので次回で。


2012/08/01

【Access】自作自演で課題をやってみる

何のことやら分からん題名になってしまったけど、以前の反省を踏まえ、後輩に出題する課題をとりあえず自分でやってみておこうということです。

では今回の課題はこちら。


ストーリー
あなたの勤める会社(従業員数200名)では毎週5名ずつ掃除当番を選出することになっている。現在は総務のAさんが社員名簿とにらめっこして極力連続して当たったりしないように苦心して選出しているが、いい加減面倒くさくなりAccessを勉強中のあなたに「何とかして!!」と泣きついてきた。

条件
  • 毎週、社員名簿から5名を選出する。1度選出されたら全社員が1巡するまで再度選出されることはない。また、再度選出される際でも前回の選出から最低3ヶ月は間をおかなければならない。
  • 毎月、月初めにその月の掃除当番表を作成し、出力する。


以上。さて、早速ですがこの話題のキーワードをまとめておきましょう。「ランダムな選出」「期間込みのフラグ」「月区切りのレポート」といったところでしょうか。軽く順番に考えてみます。

「ランダムな選出」
社員テーブル(仮)を作成し、そこからランダムに5名選出する。まぁ、ループとクエリかSQL文を組み合わせれば済みそうな気がします。

「期間込みのフラグ」
選出時に2つ条件があります。「1巡するまで再選出されないこと」と「再選出は前回の選出から3ヶ月以上経過していること」です。これを実現しようと思った場合、社員テーブル(仮)に日付を持ったフィールドを作ってやるのが無難かぁと思いました。もしくは別のフラグ管理用テーブルを作るかってとこかなぁ。

「月区切りのレポート」
最終的にこれが結果として出力されるものですね。まぁ月を指定するパラメータを持ったクエリでレポート作成するのが無難そうです。週の区切りはレポートのグルーピング機能で出来ないかしら?って感じです。ちなみに選出の実行はこれが出力されるタイミングでやっちゃえば良いでしょう(選出タイミングは条件にないので)。

自分的に何となく出来そうな気がしてきたのでテーブルを設計してみます。とりあえず、テーブルは社員テーブル1個で済ます方向でいこうと思います。社員テーブルのフィールドは以下の通り。


テーブル
論理名社員テーブル
物理名Employees
フィールド
論理名物理名型
IDIdlong(increment)
社員番号EmployeeNostring
社員氏名EmployeeNamestring
選出日ChoseDatedate


選出日は日付/時刻型でしちゃってます。文字列型にすべきかとも思いましたがそんなに日付比較に精度を求める必要もないのでこのままでいきます。SQLServerのように単独で日付型、時間型を持っていればそちらを使いたいとこなんですが…。

長くなっちゃったので今回はここまで。次回はさくっとクエリかSQL文とVBAを書いちゃいます。


2012/07/31

【Access】サブフォームからレコードを選択する方法

「同一のレコードソースを持つメインフォーム(帳票)とサブフォーム(データシート)を用意し、サブフォームのあるレコードを選択したら、メインフォームも同一レコードへ移動するように。」と部下に課題を出してみたところどうやら煮詰まってる模様。

こっちは解答も用意せず何気なしに出題したわけだけどそんな難しいことかいな?と自分でも作ってみると、意外とやっかいというか…。一般的なサンプルはメインフォームとサブフォームの関係は親子の関係であり、サブフォームは詳細表示に使いましょう的なものが多く、今回のように対等に扱うケースはあんまり見あたりませんでした。すまん、すまん。これはちょっとうっかり。そりゃ初心者じゃ煮詰まるわ。

せっかくなので要点だけのサンプルを載せときます。サブフォームのレコード移動時イベントで割と強引にメインフォームのレコードを移動させてます。

Option Compare Database
Option Explicit

Private Sub Form_Current()

DoCmd.GoToRecord acDataForm, "メインフォーム名", acGoTo, Me.CurrentRecord

End Sub

何でも屋の「DoCmd」ですが、こーゆー別フォームの操作ができちゃうのが良いとこなんだか悪いとこなんだか…。個人的にはもうちょいちゃんとした手続きが間に欲しかったりするとこだけど、そこまでやるんだったらVBAじゃなくてもいいだろって話になっちゃうので…。

後は追加・削除時に互いにRequeryかけるとか、細かいとこ見てやれば大丈夫なはず。次からはちゃんと解答用意してから出題しようと思いましたとさ。


2012/07/21

VBAのTypeステートメント

後輩指導してたらTypeステートメントなんてのに初めて気がついたのでメモ。

Typeステートメントはユーザー定義型が作れる模様。.Netでよくデータモデルを扱う人間には使い勝手が良さそうです。定義してあればタイプミスしてもコンパイルで引っかかってくれます。テキストデータやレコードの読み込み時なんかで使えそうです。

以下、サンプル。テキストデータから「店舗レコード」型配列を作成するイメージです。
'店舗レコード
Type StoreRecord
StoreCd As String
Name As String
OpenDate As String
CloseDate As String
End Type

'店舗レコード作成
Function CreateStoreRecords(lines() As String) As StoreRecord()
Dim fso As FileSystemObject
Set fso = New FileSystemObject
Dim stream As TextStream
Set stream = fso.OpenTextFile(fileName, ForReading)
Dim results() As StoreRecord
Dim count As Integer
count = 0
Do Until stream.AtEndOfLine
Dim elements() As String
elements = Split(stream.readLine, ",")
Dim store As StoreRecord
store.StoreCd = Format(elements(0), "000000")
store.Name = elements(1)
store.OpenDate = CDate(elements(2))
store.CloseDate = CDate(elements(3))
ReDim Preserve results(count)
results(count) = store
count = count + 1
Loop
Set stream = Nothing
Set fso = Nothing
CreateStoreRecords = results
End Function

普段はあまりVBAを書くことがありませんが、たまにこういう発見も悪くないなぁって感じです。まぁ昔作った物を異常に修正したくなったりしますが…。

ちなみに久しぶりにSyntax Highlighter使ったらまったくもって使い方を忘れた上に、なんだかうまいこと投稿できず、削除と投稿を繰り返したりしました。

pagetop