Visual Basic 中学校 掲示板 投稿内容
タグのない投稿を抽出 統計 RSS

Visual Basic 中学校 > 投稿一覧 >

DataTableから新たなDataTableを生成し表示 解決済み

Access ADO.NET DataTable タグの編集...

投稿者 さすが   (社会人)   投稿日時 2019/8/8 18:56:06
よろしくお願いします。
VisualBasic2005を使用し、データベース操作がどうなるか試しているところです。
そのためデータベースはAccessです。

やりたいことは、

まず、「TableA」と「TableB」がある。
この「TableA」と「TableB」のクロス集計クエリを行う。このクエリを「クエリ1」と名付ける。
次に、この「クエリ1」のうち、列Cの文字列と列Dの文字列を結合するという変化を加えたクエリを生成する。このクエリを仮に「クエリ2」と名付ける。
そして、「クエリ2」の全てをDataGridViewに表示させるのがゴール。

というものです。
とはいえ、ブレークダウンして、「クエリ1」の全てをDataGridViewに表示させるところから始め、表示できました。
しかし次の「クエリ2」を行うにあたり、DataSetの中にある「クエリ1」という名のDataTableから行ってほしいのですが、そのようにする方法が分かりません。

Accessなら、CurrentDB.CreateQueryDefというコードを使って一時的にでもAccessファイル内に「クエリ1」という名クエリを保存させ、これを見に行かせることができますのに。
VB2005でCurrentDBと書いたら、最初のAccessファイルですよね?

ちなみに、デザイン以外はSQL文含め基本的に全てコードを書き、
データソースエクスプローラーからデータソースを該当のAccessファイルに指定するような処理は全く行っておりませんが、
データソースを指定した方が簡単というのでしたら改めたいと思います。

改めまして、ご回答よろしくお願いします。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/8 20:25:15
> ブレークダウンして、「クエリ1」の全てをDataGridViewに表示させるところから始め、表示できました。
表示させるためのデータが、DataSet/DataTable の
 ds.Tables("クエリ1")
に既に読み込まれていると仮定しておきます。


> この「クエリ1」のうち、列Cの文字列と列Dの文字列を結合するという変化を加えたクエリを生成する。

列C も 列D も文字列型のようですので、
 ds.Tables("クエリ1").Columns.Add("NewColumn", GetType(String), "ISNULL(C,'')+ISNULL(D,'')")
のように『式列』を追加してみるのはどうでしょう。

投稿者 さすが   (社会人)   投稿日時 2019/8/9 12:03:31
魔界の仮面弁士さま

ご回答誠にありがとうございます。
ご回答のとおりにすると、見事にDataGridViewに加えたい列が表示されました。

しかし、この質問の事例は、私にとってデータベース操作のほんの初歩的なものです。
これからやろうと考えているものには、タイトルにある
「DataTableから新たなDataTableを生成」
する方法、もしくは大元のデータベースなどに一時保存する方法でも使わなければ、
到底できそうにありません。

どういうことを考えているか、については、Accessで実現できたのではありますが、
記載すると理解していただくだけで何時間もかかると思いますので、省略させていただきます。

ということで、本質問のタイトル、「DataTableから新たなDataTableを生成し表示」する方法を、教えていただけないでしょうか?

どうぞよろしくお願いします。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/9 14:37:42
Access VBA においても、Recordset オブジェクトの内容に対して
「SELECT * FROM  {Recordset1}」のような再問い合わせはできませんでしたよね。
(Recordset 二対して、Filter や Seek や Find などといったカーソル制御に関する機構があるだけ)


最初の例に挙がった DAO の CreateQueryDef や ADOX の Views.Append といったものは、結局のところ、 JET や ACE にとっての CREATE VIEW クエリに相当するものに過ぎません。
(DAO の CreateQueryDef は無名クエリの生成にも使えますが、今回の話とは無関係)

VB.NET でも、CREATE VIEW や CREATE TABLE の SQL を実行することはできますので、
ワークテーブルや VIEW を用いて、そこから再問い合わせを行うことはできるでしょう。
ですがそれはあくまでも Access 側が持つ、ACE / JET の機能に過ぎません。
VB.NET における ADO.NET や DataSet 、VBA における ADODB や DAO とは別物です。



なので Access 側の SQL で処理させるなら、ワークテーブルや VIEW を用いる必要がある、というのが回答になります。

SQL は使わず、VB 側で処理するなら、DataSet のリレーション機能を使ったり、
DataView のフィルターやソート機能を使ったり、LINQ による集計や再加工を行ったりします。

この 2 つはそれぞれ独立したものです。Access にとっての DAO や ADODB がそうであったように。


そもそも DataAdapter や DataSet というのは、Access でいえば、切断型の ADODB.Recordset にあたります。切断型というのは、ActiveConnection が Nothing だが Close されていない、オフライン向けの Recordset のことです。

ADODB において、Recordset の結果を使って別の処理をするとなると、Shape Provider によるリシェイプを利用する必要がありました。(この機能は DAO の Recordset にはありません)
https://docs.microsoft.com/ja-jp/office/client-developer/access/desktop-database-reference/reshape-name-property-dynamic-ado
http://hanatyan.sakura.ne.jp/patio/read.cgi?mode=view2&f=36&no=1

これに相当することで良ければ、DataSet 単体で行えます。
リシェイプ時と同様、JET 側の SQL は使えませんけれどね。


また、DataSet には、複数のテーブルを格納できますが、これはデータベースに繋がなくても利用できます。
ループ処理や LINQ で DataTable の各行を取り出して、新たな DataTable を DataSet に追加したり、
複数のテーブル間でリレーションシップを貼ったり、前回の回答のように「式列」を設けたり、
Compute 句による集計を行うことも出来ます。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/9 14:45:14
"クエリ1" の DataTable が、どういう列定義と行データを持っていて、
それを "クエリ2" でどういう形の DataTable へと再加工したいのか、
具体的な例を挙げて頂く事はできますか?

具体例があれば、それを実現するための参考コードを提示できるかと思います。

投稿者 shu   (社会人)   投稿日時 2019/8/9 16:27:55
「DataTableから新たなDataTableを生成し表示」方法のうち
生成までなら以下のようにSrcTblからCopyを作成することが可能です。

        Dim SrcTbl As New DataTable()

        With SrcTbl.Columns
            .Add("Column1"GetType(Integer))
            .Add("Column2"GetType(String))
        End With

        With SrcTbl.Rows
            .Add(1, "Data1")
            .Add(2, "Data2")
            .Add(3, "Data3")
            .Add(4, "Data4")
        End With

        Dim DestTbl1 = SrcTbl.Copy
        Dim DestTbl2 = SrcTbl.Clone
        Dim DestTbl3 = (From r In SrcTbl.AsEnumerable
                        Where r.Field(Of Integer)("Column1") < 3).CopyToDataTable





投稿者 さすが   (社会人)   投稿日時 2019/8/9 16:50:05
魔界の仮面弁士様

続けてご回答誠にありがとうございます。

具体的に、となると業種が非常に限定されてしまうのですが、

「クエリ1」は、クロス集計の結果、

フィールド名(データ型)
契約ID(オートナンバー型)│顧問先コード(Integer型)│決算日(DateTime型)│消費税課税(Integer型)│摘要(String型)│法人税申告料(Currency型)│消費税申告料(Currency型)│月次報酬(Currency型)│

と並んでいます。これを、

契約ID(オートナンバー型)│顧問先コード(Integer型)│決算日(DateTime型)│月次報酬(Currency型)│法人税申告料(Currency型)│消費税課税と消費税申告料の結合(String型)│摘要(String型)│

に並び替えてDataGridViewに表示させたいと考えています。

文字列と文字列を結合したい、と最初に申し上げたのは、「消費税課税」と「消費税申告料」の2列のデータであり、
実はどちらもString型ではありません。

「消費税課税」フィールドには、-1、0、1の数値を入れることとし、それぞれ、
-1 : 免税
0 : 原則課税
1 : 簡易課税
という意味を持たせています。
(まだ試しにやっている段階なので、Accessファイルに登録した「消費税課税」フィールドデータは0しかありません)

「消費税課税と消費税申告料の結合」フィールドに表示させたいのは、
この数値と金額をそのまま結合した文字列ではなく、
「消費税課税」の数値に対応する文字列と「消費税申告料」の金額を結合した文字列であり、
(免)0 とか、(原)30,000 のような文字列です。
>ご回答のとおりにすると、見事にDataGridViewに加えたい列が表示されました。
と申し上げたのは、「030,000」のように表示された、という意味です。
最終ゴールは、「(原)30,000」と表示させることです。

もし、「消費税課税」の数値からこれに対応する文字列に変換する処理がDataSetに格納した後となっては困難、ということでしたら、
それはそれで考えます。

以上の具体例から、参考コードを提示していただけますでしょうか?

再度申し伝えますと、VisualBasic2005ですので、LINQは使えないと思われます。

どうぞよろしくお願いします。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/9 20:36:42
DataSet の「式列」で書いてみた場合。

Public Class Form1
    Private ds As DataSet
    Enum 消費税課税 As Integer
        免税 = -1
        原則課税 = 0
        簡易課税 = 1
    End Enum

    Private Sub Form1_Load(ByVal sender As System.ObjectByVal e As System.EventArgs) Handles MyBase.Load
        ds = New DataSet("さんぷる")
        ds.CaseSensitive = True
        ds.EnforceConstraints = False

        Dim dt1 As DataTable = Createクエリ1()
        Dim dt2 As DataTable = Create消費税課税()

        ds.Tables.Add(dt1)
        ds.Tables.Add(dt2)

        'リレーションと式列を追加 
        ds.Relations.Add("Rel消費税課税", dt2.Columns("Key"), dt1.Columns("消費税課税"), True)
        dt1.Columns.Add("消費税課税と消費税申告料の結合"GetType(String), _
                "Parent(Rel消費税課税).DisplayText+消費税申告料")

        ds.EnforceConstraints = True

        'これはデザイン時に行っても OK 
        InitGrid()
    End Sub

    Private Sub InitGrid()
        DataGridView1.EditMode = DataGridViewEditMode.EditOnEnter
        DataGridView1.Columns.Clear()
        DataGridView1.AutoGenerateColumns = True
        DataGridView1.AllowUserToAddRows = False
        DataGridView1.ColumnHeadersDefaultCellStyle.WrapMode = DataGridViewTriState.False
        DataGridView1.RowHeadersVisible = False
        DataGridView1.AllowUserToResizeRows = False
        DataGridView1.DataSource = ds
        DataGridView1.DataMember = "クエリ1"
        DataGridView1.Columns("契約ID").Frozen = True
        DataGridView1.Columns("契約ID").DividerWidth = 3
        DataGridView1.Columns("決算日").DefaultCellStyle.Format = "yyyy年MM月dd日"
        For Each col As String In "法人税申告料@消費税申告料@月次報酬".Split("@"c)
            With DataGridView1.Columns(col).DefaultCellStyle
                .Format = "N0"
                .Alignment = DataGridViewContentAlignment.MiddleRight
            End With
        Next
        DataGridView1.Columns("消費税課税").Visible = False
        DataGridView1.Columns("消費税申告料").Visible = False
        DataGridView1.Columns("月次報酬").DisplayIndex = 3
        DataGridView1.Columns("摘要").DisplayIndex = 8

        DataGridView1.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.DisplayedCells)
    End Sub


    Private Function Createクエリ1() As DataTable
        Dim dt As New DataTable("クエリ1")

        Dim pkey As DataColumn = dt.Columns.Add("契約ID"GetType(Long))
        pkey.ReadOnly = True
        pkey.Unique = True
        pkey.AutoIncrement = True
        pkey.AutoIncrementSeed = 10000
        pkey.AutoIncrementStep = 1
        dt.PrimaryKey = New DataColumn() {pkey}

        dt.Columns.Add("顧問先コード"GetType(Integer))
        dt.Columns.Add("決算日"GetType(Date)).DateTimeMode = DataSetDateTime.Local
        dt.Columns.Add("消費税課税"GetType(消費税課税))
        dt.Columns.Add("摘要"GetType(String)).AllowDBNull = True
        dt.Columns.Add("法人税申告料"GetType(Decimal))
        dt.Columns.Add("消費税申告料"GetType(Decimal))
        dt.Columns.Add("月次報酬"GetType(Decimal))

        dt.Rows.Add(10000, 1200, #7/31/2018#, 消費税課税.原則課税, DBNull.Value, 98765D, 0D, 111111D)
        dt.Rows.Add(10001, 1201, #8/31/2018#, 消費税課税.原則課税, "適用", 98765D, 30000D, 222222D)
        dt.Rows.Add(10002, 1371, #9/30/2018#, 消費税課税.原則課税, "適当", 1000000, 20000D, 333333D)
        dt.Rows.Add(10003, 1390, #9/30/2018#, 消費税課税.免税, "敵よ", 1300000, 21000D, 43210D)
        dt.AcceptChanges()

        Return dt
    End Function

    Private Function Create消費税課税() As DataTable
        Dim dt As New DataTable("消費税課税")
        dt.PrimaryKey = New DataColumn() {dt.Columns.Add("Key"GetType(消費税課税))}
        dt.Columns.Add("DisplayText"GetType(String))
        dt.Rows.Add(消費税課税.免税, "(免)")
        dt.Rows.Add(消費税課税.原則課税, "(原)")
        dt.Rows.Add(消費税課税.簡易課税, "(簡)")
        dt.AcceptChanges()
        Return dt
    End Function
End Class


投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/9 20:45:14
「式列」を用いず、自前で列を追加する場合。

Private Sub Form1_Load(ByVal sender As System.ObjectByVal e As System.EventArgs) Handles MyBase.Load
    ds = New DataSet("さんぷる")
    ds.CaseSensitive = True
    ds.EnforceConstraints = False

    Dim dt1 As DataTable = Createクエリ1()

    ds.Tables.Add(dt1)

    '自前で結合列を用意 
    Dim col As DataColumn = dt1.Columns.Add("消費税課税と消費税申告料の結合"GetType(String))
    For Each row As DataRow In dt1.Rows
        Dim s As String
        Select Case row("消費税課税")
            Case 消費税課税.免税
                s = "(免)"
            Case 消費税課税.原則課税
                s = "(原)"
            Case 消費税課税.簡易課税
                s = "(簡)"
            Case Else
                s = ""
        End Select
        row(col) = s & String.Format("{0:N0}", row("消費税申告料"))
        row.AcceptChanges()
    Next

    ds.EnforceConstraints = True

    'これはデザイン時に行っても OK 
    InitGrid()
End Sub


投稿者 さすが   (社会人)   投稿日時 2019/8/12 20:59:40
魔界の仮面弁士様

コードを考えて記載していただき、誠にありがとうございます。

記載いただいたものは、「クエリ1」をまるで、コードに直接記載したもので生成するかのようです。

しかし実際は、データベースとしてAccessファイルを使用しており、
Accessに不便を感じたらSQLServerを使用する予定で、
いずれにせよデータベースを使用することに変わりはなく、
「クエリ1」はこのデータベースとコネクトし、SQL文にてデータを抽出し生成させます。

>ブレークダウンして、「クエリ1」の全てをDataGridViewに表示させるところから始め、表示できました。

と最初に申し伝えておりますが、Accessとコネクトし表示させることができた、という意味です。

ということは、

>Dim dt1 As DataTable = Createクエリ1()

は、「= Createクエリ1()」のコードを除去し、

>Private Function Createクエリ1() As DataTable
・・・
>End Function

を、データベースとコネクトしSQL文にて生成するコードにそっくりそのまま変えても
問題ないのでしょうか?

どうぞよろしくお願いします。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/13 01:09:48
> 記載いただいたものは、「クエリ1」をまるで、コードに直接記載したもので生成するかのようです。

はい、その通りです。

データベースとの懸け橋となる物として、OleDbDataAdapter や TableAdapter などを
挙げることができますが、それに対して DataSet や DataTable といったものは、
そもそもデータベースとはまったく無関係の代物です。


データベースのデータを操作しやすいように設計されてはいますが、
DataSet に格納される元データが、今回のように手動で生成されたものであろうと、
Access から取り出されたものであろうと、CSV や XML から構築されたものであろうと、
DataSet としては何ら変わりませんし、それらを区別する方法すら存在しません。


> データベースとコネクトしSQL文にて生成するコードにそっくりそのまま変えても
> 問題ないのでしょうか?

はい、その通りです。
後で中身を差し替えやすくするために、個別の Function にしただけなので、
適宜置き換えてみてください。

投稿者 さすが   (社会人)   投稿日時 2019/8/13 17:27:23
魔界の仮面弁士様

何度もご回答誠にありがとうございます。

実際にコードを書き込み、疑問点が出てきました。



Ds.Relations.Add("Rel消費税課税", dt2.Columns("Key"), dt1.Columns("消費税課税"), True)

のところでどうしてもエラーが発生します。

Accessファイルから「クエリ1」を生成し、DataSetに格納するのですが、その方法を、

(1)
  Private Function Createクエリ1() As DataTable

    dt1 = New DataTable("クエリ1")

    Cn = New OleDbConnection
    Rs = New ADODB.Recordset

    Dim SQL As OleDbCommand = Cn1.CreateCommand

    SQL.CommandText = (省略)

    Cn.ConnectionString = "Provider = Microsoft.Jet.OLEDB.4.0; Data Source = kanridata.mdb"
    Cn.Open()

    Da = New OleDb.OleDbDataAdapter(SQL_CLTFee1)

    Da.Fill(dt1)
    Ds.Tables.Add(dt1)

    Return dt1

  End Function

というコードでdt1というDataTableを、Fillし、DataSetにAddし、Returnしてやると、

たくさんの文章がイミディエイトウィンドウに現れて、
「親列および子列に型が一致する列が含まれていません。」
というエラーメッセージ」が現れます。

(2)
    Cn = New ADODB.Connection
    Rs = New ADODB.Recordset

    Cn.CursorLocation = ADODB.CursorLocationEnum.adUseClient
    Cn.ConnectionString = "Provider = Microsoft.Jet.OLEDB.4.0; Data Source = kanridata.mdb"
    Cn.Open()

    dt1 = New DataTable
    dt1.TableName = "クエリ1"

    Dim SQL As String

    SQL = (省略)

    Rs.Open(SQL, Cn)

    Da = New OleDb.OleDbDataAdapter()
    Ds = New DataSet()

    Da.Fill(Ds, Rs, dt1.TableName)

というコードで、"クエリ1"という名のDataTableをDataSetにFillしてやると、

「'column' 引数を Null にすることはできません。」

というエラーメッセージが現れます。

("クエリ1"という名のDataTableがあるのであり、dt1というDataTableがあるわけではないので、
 Nullとかいうエラーになるのは当然ではありますが。)

どこを、どのように直せばいいのでしょうか?

②今回の質問の条件下では、質問のタイトルである

「DataTableから新たなDataTableを生成」

する必要は、どうやらなさそうであることが分かりました。

しかし、あまり書きたくなかったのですが、仮にTableAというテーブルがあるとして、フィールドは、

契約ID|担当者名|取引先名|業務完了日|ナンバリング用フィールド|

とあるとします。

このテーブルから、
〇「行見出し」 = 担当者名
〇「列見出し」 = 業務完了日(8/13とか8/31とか)
〇「値」 = 取引先名をすべて結合した文字列
というクロス集計クエリを生成しようとする場合、

Step1 TableAに、1人の担当者が1日に業務完了させた契約にナンバリングを行う。

Step2 
〇「行見出し」 = 担当者名および業務完了日
〇「列見出し」 = ナンバリングした番号
〇「値」 = 取引先名
というクロス集計クエリ1-1を生成する。

Step3 クエリ1-1の取引先名の文字列全てを結合した「文字列集計列」を追加する。
追加するのではなくこれをクエリで行うならば、そのクエリを仮にクエリ1-2と名付ける。

Step4 クエリ1-1もしくはクエリ1-2から
〇「行見出し」 = 担当者名
〇「列見出し」 = 業務完了日
〇「値」 = 取引先名をすべて結合した文字列
というクロス集計クエリ1-3を生成する。

というステップを踏む必要があると思われ、過去に実際にこれをAccessで行ったことがあります。

これをVBで行う場合、

「DataTableから新たなDataTableを生成」

することは避けて通れないと思います。
しかし、ご教示いただいたコードは未だ、
DataTableから新たなクエリを生成するためのコードとなっていないように感じられました。

再度お願いいたします。

こちらがお示しした条件下で
「DataTableから新たなDataTableを生成」する方法を教えてください。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/13 18:20:15
> Ds.Relations.Add("Rel消費税課税", dt2.Columns("Key"), dt1.Columns("消費税課税"), True)
> 「親列および子列に型が一致する列が含まれていません。」
> というエラーメッセージ」が現れます。

dt2 の Key 列と、dt1 の 消費税課税 列の型が一致しているかを確認して下さい。
たとえば、Integer の「100」と Decimal の「100」は別物となります。
データベース側の列の型ではなく、DataTable 上の列の型を確認するという点に注意して下さい。

Dim ds As New DataSet()
Dim dt1 = ds.Tables.Add()
Dim dt2 = ds.Tables.Add()
dt2.PrimaryKey = New DataColumn() { dt2.Columns.Add("Key"GetType(Integer)) }
dt1.Columns.Add("消費税課税"GetType(Decimal))  '違う型にしてみる 

' System.Data.InvalidConstraintException: 
' 「親列および子列に型が一致する列が含まれていません。」 
ds.Relations.Add("Rel消費税課税", dt2.Columns("Key"), dt1.Columns("消費税課税"), True)


なお上記では、単一列を主キーとしたリレーションを貼っていますが、複合キーとなる場合には、
列指定の部分を、DataColumn の配列として渡すことも出来ます。


> dt1.TableName = "クエリ1"
TableName は後付でも構いません。実際の環境に沿った名前をつけましょう。
(特に指定しなければ、DataSet に割り当てられる際に自動的に命名されます)


> Da.Fill(Ds, Rs, dt1.TableName)

OleDbDataAdapter.Fill( DataSet, Recordset, TableName ) のかわりに
OleDbDataAdapter.Fill( DataTable, Recordset ) を使うこともできます。
後者は、階層型 Recordset の展開にも使えます。

なお、既にデータの入っている DataTable に追加格納する場合には、
実行前に OleDbDataAdapte.MissingSchemaAction プロパティの値を調整しておいてください。


> Rs = New ADODB.Recordset

可能であれば、ADO.NET のみでの操作に揃える事をお奨めします。
.NET 環境における ADO の併用は推奨されていません。


.NET での ADO の使用に関するロードマップ
http://j.mp/KB308044JP
》 ADO および ADO MD は、Microsoft .NET Framework 環境で十分にはテストされていません。
》 特に、サービスベースのアプリケーションまたはマルチスレッド アプリケーションでは、
》 断続的に問題が発生することがあります。


[FIX] 高負荷下で ADO を .NET COM interop または Java で使用するとアクセス違反が発生する
http://j.mp/KB321415JP
》  以下の 2 つのコーディング例を使用することで、この問題が発生する可能性を低く抑えることができます。
》 ・ COM オブジェクトの使用後、明示的に COM オブジェクトへの参照を解放する。 (Marshal.ReleaseComObject)
》 ・ ADO プライマリ相互運用アセンブリ (primary interop assembly) を使用する。 


上記で、プライマリ相互運用アセンブリ (PIA) を使うように案内されていますが、
ADO の場合、IA と PIA とでオブジェクト解放の手順が異なることに注意して下さい。

たとえば、普段、Recordset の Fields オブジェクトの存在を意識することは少ないかもしれませんが、
IA の場合は、これを 「ReleaseComObject しなければいけない」とされていますし、
PIA の場合は逆に、 「ReleaseComObject してはいけない(できない)」という違いがあります。
※ Recordset や Connection については、ReleaseComObject する必要あり。

投稿者 さすが   (社会人)   投稿日時 2019/8/13 19:12:44
すみませんが、

投稿日時 2019/8/13 18:20:15

のご回答は、私の質問のどこからどこまでのご回答なのでしょうか?

また、

>dt2 の Key 列と、dt1 の 消費税課税 列の型が一致しているかを確認して下さい。

と言われましても、そもそも魔界の仮面弁士様が書いてくださったコードには、

>Enum 消費税課税 As Integer

>dt.Columns.Add("消費税課税", GetType(消費税課税))

>dt.PrimaryKey = New DataColumn() {dt.Columns.Add("Key", GetType(消費税課税))}

とありますから、なるほど型は一致しています。

しかし、私の作ろうとしているものは、"消費税課税"列はデータベースからSQL文で生成するものであり、

コードにて型を指定してAddするようなものではありません。

>dt2.PrimaryKey = New DataColumn() { dt2.Columns.Add("Key", GetType(Integer)) }
>dt1.Columns.Add("消費税課税", GetType(Decimal))  '違う型にしてみる 

悪い見本を、相変わらず"消費税課税"列をAddするコードで示されても、
型をどのようにして一致させればよいのか私には分かりかねます。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/13 19:40:26
細かいことですが:

> 〇「行見出し」 = 担当者名

このような場面で
〇 [U+3007]:漢数字のゼロ(IDEOGRAPHIC NUMBER ZERO)
を使うことに違和感があります。


以下、類似の文字として。

○ [U+FFEE]:半角白丸(HALFWIDTH WHITE CIRCLE)
○ [U+25CB]:白丸(WHITE CIRCLE)
● [U+25CF]:黒丸(BLACK CIRCLE)
◯ [U+25EF]:大白丸(LARGE CIRCLE)
🔴 [U+1F534]:大赤丸(LARGE RED CIRCLE)
🔵 [U+1F535]:大青丸(LARGE BLUE CIRCLE)



> 仮にTableAというテーブルがあるとして、フィールドは、
> 契約ID|担当者名|取引先名|業務完了日|ナンバリング用フィールド|
> とあるとします。

同じ日に同じ取引先との契約が複数回発生した場合は、
別の契約 ID が付与されるのでしょうか?

また、取引先のナンバリング順序は定義されているでしょうか?
(契約ID 順なのか、取引日時順なのか、取引先コード順なのか)



> Step1 TableAに、1人の担当者が1日に業務完了させた契約にナンバリングを行う。

このナンバリングは、Access 側で行われるのでしょうか?
VB 側で行うのであれば、DataTable をループ処理すれば採番できますね。



> Step2 
> 〇「行見出し」 = 担当者名および業務完了日
> 〇「列見出し」 = ナンバリングした番号
> 〇「値」 = 取引先名
> というクロス集計クエリ1-1を生成する。

Dim dt As New DataTable("クロス集計")
dt.PrimaryKey = New DataColumn() { _
  dt.Columns.Add("担当者名"GetType(String)), _
  dt.Columns.Add("業務完了日"GetType(Date)) _
}
For n = 1 To 最大ナンバリング値
    dt.Columns.Add("取引先" & CStr(n), GetType(String))
Next

とでもしておいて、ここに .Rows.Add していけば良さそうです。
業務完了日を String 型にするかどうかはお好みで。


> Step3 クエリ1-1の取引先名の文字列全てを結合した「文字列集計列」を追加する。
Step2 の DataTable に
 dt.Columns.Add("文字列集計列", GetType(String))
を追加しておき、自前で連結文字列をセットしていった方が良いでしょう。


> Step4 クエリ1-1もしくはクエリ1-2から
> 〇「行見出し」 = 担当者名
> 〇「列見出し」 = 業務完了日
> 〇「値」 = 取引先名をすべて結合した文字列
> というクロス集計クエリ1-3を生成する。

Step3 の結果に対して、不要な 取引先# 列を dt.Columns.Remove() すれば完成。



とはいえ、Step 3 までの集計のために、DataTable に蓄えるのは非効率的なので、
適当なコレクション(Dictionary とか List など)を用意しておいて、
TableA の内容を For Each して詰みこんでいった方が手っ取り早そうです。
案件的に、ナンバリングする必要も無さそうですし。

いったんコレクション化してしまえば、そこから Step 4 の DataTable を作るのは、そう難しくは無いですよね。


投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/13 19:50:02
> しかし、私の作ろうとしているものは、"消費税課税"列はデータベースからSQL文で生成するものであり、
> コードにて型を指定してAddするようなものではありません。

先にも述べましたが、System.Data.DataTable 型のインスタンス に格納された情報は、
それがデータベースから取得されたものであろうと、自前のコードで作ったものであろうと、
取り扱い方には、何の違いもありません。生成手順が異なるだけです。

Access から取得した取引先名と、
TextBox から入力させた取引先名とでは、
いずれも String 型のインスタンスに過ぎませんし、
VB 側での取り扱いに差異はないですよね。取得手順が異なるというだけで。


ですから今回の話においては、変換元となる DataTable が、
OleDbDataAdapter 経由で生成されたものであるかどうかは、一切関係ありません。

DataAdapter から取得された結果の DataTable の列の型が何になっているかを、
ローカルウィンドウ等を用いて、自前でチェックしてもらうだけで OK です。


あるいは、OleDbDataAdapter にフィールドを自動生成させるのではなく、
自前で名前と型をしていしておき、そこに対して Fill させる手法もありますが、
そのような場合には、「型付DataSet」を使った方が手っ取り早いでしょう。
(今は、型付DataSet を使っているわけではないのですよね?)

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/13 21:39:08
> 契約ID|担当者名|取引先名|業務完了日|ナンバリング用フィールド|
> 〇「行見出し」 = 担当者名
> 〇「列見出し」 = 業務完了日
> 〇「値」 = 取引先名をすべて結合した文字列

とりあえずイメージコード。NULL 判定などは省いていますので、適宜置き換えを。


元データは DataTable でも Recordset でも構いませんが、とりあえず DataTable を想定しています。

データは予め、担当者名、業務完了日、取引先名を繋げる為のナンバリングフィールド の順で
ソートされた状態にしておいてください。

データベース側でソートしておくことが難しければ、
Recordset や DataView の Sort プロパティを使って並び替えても OK。


'処理結果を蓄えるための DataTable 
Dim dt As New DataTable()
dt.PrimaryKey = New DataColumn() {dt.Columns.Add("担当者名")}

'加工結果を蓄えるための Dictionary 
Dim dicGyou As New Dictionary(Of String, Dictionary(Of StringString))()

'DataTable の各行を列挙する 
' Recordset の場合は For Each ではなく、MoveNext 呼び出しに置き換えてください 
For Each row As DataRow In 元データのテーブル.Rows
    Dim dicRetu As Dictionary(Of StringString)

    Dim sStaff As String = CStr(row("担当者名"))
    Dim gDay As String = CStr(row("業務完了日"))   '元データが日付型の場合は Format して取得すること 
    Dim sClient As String = CStr(row("取引先名"))

    '行見出しの捜索 
    If dicGyou.ContainsKey(sStaff) Then
        dicRetu = dicGyou(sStaff)
    Else
        dicRetu = New Dictionary(Of StringString)()
        dicGyou.Add(sStaff, dicRetu)
    End If

    '列見出しの捜索 
    If dicRetu.ContainsKey(gDay) Then
        dicRetu(gDay) &= "、" & sClient   '取引先名を連結 
    Else
        dicRetu.Add(gDay, sClient)   '最初の取引先 
        If Not dt.Columns.Contains(gDay) Then
            '未登録の列見出しを追加 
            dt.Columns.Add(gDay, GetType(String)).AllowDBNull = True
        End If
    End If
Next

'Dictionary を DataTable に詰めなおす 
For Each entry1 As KeyValuePair(Of KeyValuePair(Of StringString), StringIn dic
    Dim newRow As DataRow = dt.NewRow()
    newRow("担当者名") = entry1.Key
    dt.Rows.Add(newRow)
    For Each entry2 As KeyValuePair(Of StringStringIn entry1.Value
        newRow(entry2.Key) = entry2.Value
    Next
Next
dt.AcceptChanges()


投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/14 09:38:11
訂正と追記

> For Each entry1 As KeyValuePair(Of KeyValuePair(Of String, String), String) In dic

上記は
 For Each entry1 As KeyValuePair(Of String, Dictionary(Of String, String)) In dicGyou
に差し替えておいてください。投稿時に直し漏れてました。


それから
> '未登録の列見出しを追加
> dt.Columns.Add(gDay, GetType(String)).AllowDBNull = True
のところでは、列見出しをそのまま出現順に追加しています。

見出しの並び順も制御する必要がある場合には、下記のような方法が使えます。
(後者の方が手間が少ないかな…?)


(案1) 末尾に Add した後、DataColumn の SetOrdinal メソッドを使って、列の並び順を入れ替える。

(案2) 列挙中に Add するのではなく、一旦すべて Dictionary なり List なりに詰め込んでから、
 そのコレクションから列見出しの一覧を取得しなおし、Array.Sort 等で並べなおしてから
 Columns.Add するようにする。


実際に変換してみた結果がこんな感じ。


投稿者 さすが   (社会人)   投稿日時 2019/8/16 19:59:36
魔界の仮面弁士様

たくさんのご回答をくださり、誠にありがとうございます。
ただ、私の知識の及ばないところがたくさんあり、十分に理解できていないところもあります。

取り急ぎ、現時点でお答えできるところから。

【投稿日時 2019/8/13 18:20:15 】

>可能であれば、ADO.NET のみでの操作に揃える事をお奨めします。
>.NET 環境における ADO の併用は推奨されていません。

余計なご心配をおかけしたようで申し訳ございませんでした。
私の使用しているのはVB2005であり、.NET環境ではありません。

【投稿日時 2019/8/13 19:40:26】

>このような場面で
>〇 [U+3007]:漢数字のゼロ(IDEOGRAPHIC NUMBER ZERO)
>を使うことに違和感があります。

知識がなく、申し訳ございませんでした。
では、どのような記号を頭に付けるのがこういう場合のルールなのでしょうか?

>同じ日に同じ取引先との契約が複数回発生した場合は、
> 別の契約 ID が付与されるのでしょうか?

ご指摘の内容は確かに制作上致命的とも言うべきものです。
業種を明らかにしてしまうようなものですが、これは、
毎年2月~3月に行う確定申告業務のことです。
契約IDというのは、「所得税申告」「消費税申告」「贈与税申告」、
うち、同日に提出(業務完了)する「所得税」と「消費税」を同一の契約IDでくくり、
「贈与税」は別の日に提出(業務完了)する場合に備え、別の契約IDを付与する、
という設計をいたしました。
したがいまして、
期限内に同日または別の日に申告書を再提出するということであれば現実的にありますが、
本質的に、ご心配の、同じ日に同じ取引先との契約が複数回発生することはございません(別の日に発生することもありません)。

【投稿日時 2019/8/13 19:50:02】

>DataAdapter から取得された結果の DataTable の列の型が何になっているかを、
>ローカルウィンドウ等を用いて、自前でチェックしてもらうだけで OK です。

本来、教わったとおりローカルウィンドウ等でチェックできた後に申し上げることかもしれませんが、

親フィールドと子フィールドの型を一致させる、とのことですが、
親フィールド("key")が、コード上で生み出した「消費税課税」という型、すなわち
免税 = -1 (あるいは、消費税課税.免税)
原則課税 = 0 (あるいは、消費税課税.原則課税)
簡易課税 = 1 (あるいは、消費税課税.簡易課税)
という、オリジナルの型であるのに対し、
子フィールド("消費税課税")が、データベースから取り出した、
-1
0
1
というInteger型である、
このため、コード上で生み出す「消費税課税」という型を諦めなければ一致させられない、
しかし、代替案が分からず、示してもいただけない、
という意味で申し上げております。

また、書いてくださったコードを何度読み直しましても、
“データベースからデータを取得するコード”と思しきものが見当たりません。
“単なる箱を作るコード”としか私には読めません。

以上、どうぞよろしくお願いします。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/17 03:31:06
>> .NET 環境における ADO の併用は推奨されていません。
> 私の使用しているのはVB2005であり、.NET環境ではありません。

いえ。VB2005 も .NET 環境とされていますよ。
実際、VB2005 で開発されたアプリケーションは、.NET 環境の上でしか動作できません。


ここ(VB中学校)の[準備講座]でも、この点について触れられていますね。
http://rucio.o.oo7.jp/main/VBdotNet/AboutVB/WhatsVBdotNet.htm
>> 「Visual Basic.NET」(VB.NET)とは、VB.NET2002以降の全バージョンのVBを
>> ひとまとめにして呼ぶ言い方です。VB6はVB.NETではありませんが、VB2013はVB.NETです。


Wikipedia には、このような解説もありました。
https://ja.wikipedia.org/wiki/Microsoft_Visual_Basic_.NET
>> なおVisual Studio 2005以降では、「Visual Basic .NET」や「VB.NET」という呼称ではなく、
>> 従来のように「Visual Basic」という呼称が用いられるようになっているが、
>> 6.0以前との互換性はなく、また.NETベースであることには変わりない。



…という事で、「VB.NET」という言い方をした場合、VB.NET 2002/2003 のみならず、
VB2005 やそれ以降もすべて含めた言葉と見るのが一般的です。(VB6 以下は含まれません)

一方、単に「VB」や「Visual Basic」という呼び方をした場合は、その時の文意によって
どのバージョンを指すのか、あまりはっきりしません。

①サポート期間中の現行製品を指す(VB6 等は考慮されない)
②VB6 以下や VB2005 なども含めた、すべてのバージョンの Visual Basic を指す
③VB.NET 2002 以降との比較のため、VB6 以下開発製品のことを指す(.NET 系は含まれない)
④VB6 以下 および VBA7.x 以下の言語仕様を指す(.NET 系は含まれない)
⑤VB.NET / VBA / VBScript まで含めたものを指す(この使い方は稀)


=== 以下蛇足 ===

実は ".NET" という言葉の意味は、少しずつ変化しています。
https://en.wikipedia.org/wiki/Microsoft_.NET_strategy


当初は、2000年にビルゲイツによって発表された、Web サービスによるシステム連携を軸とした
『Microsoft.NET 構想』および、それに纏わる営業的ブランディングとして使われていました。
https://en.wikipedia.org/wiki/Microsoft_.NET_strategy

これに伴い、"Windows .NET Server" や ".NET Passport" などの製品やサービスも登場しました。
https://www.atmarkit.co.jp/ait/articles/0205/11/news002.html


ただ、Web サービスの技術転換が重なったなど、様々な理由から、
営業的な側面でのブランディングは失敗したようで、Microsoft.NET 構想を指す言葉としての
当初の意味での .NET の名の使用は、数年で廃止されていったという経緯があります。

"Windows .NET Server" は、製品版では "Windows Server 2003" と名を変えてリリースされましたし、
".NET Passport" もその後、"Microsoft Passport" と名を変え、さらに後年には
"Live ID"、"Microsoft Account" などと、その名を変えていくことになります。


Visual Studio .NET 2003 や Visual Basic .NET 2003 という製品の後継が、
Visual Studio 2005 や Visual Basic 2005 という名になった理由は定かでは無いですが、
他の製品でも .NET の名を外していた時期でもあるので、恐らくはその流れなのでしょう。


ですがそれは、あくまで『製品名』の話。製品名から .NET の名が外されはしましたが、
VB2005 以降のバージョンが.NET Framework 非対応になった訳ではありませんし、
VB2005 以降もまた、.NET Framework が無ければ、動かないわけですから、
『開発技術基盤』としての .NET まで消えてしまったわけではありません。


そして現在、.NET と言えば、開発基盤の方の意味で使われる事が多いかと思います。
用語としては、.NET Framework を筆頭に、2002 年当時でも、ASP.NET であるとか、
.NET Data Provider、 ADO.NET などの用語が挙げられますね。

そして近年も、".NET Compiler Platform"、".NET Foundation"、".NET Standard"、
".NET Core"、"ASP.NET Core" などの名が登場していますね。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/17 05:20:34
>>このような場面で
>>〇 [U+3007]:漢数字のゼロ(IDEOGRAPHIC NUMBER ZERO)
>>を使うことに違和感があります。
> では、どのような記号を頭に付けるのがこういう場合のルールなのでしょうか?

ルールという程の物ではないでしょうけれど、たとえば
数字として、〇、一、二、三…などと使う場合は、丸○ではなくゼロ〇 を使い、
今回のような記号としての扱いなら、ゼロ〇ではなく、丸○を使うのが良いと思います。

(細かくて済みません。こういうのが気になってしまう性質でして)

漢字の「力」と片仮名の「カ」が混同されていた場合とか、
"C#" の事を  C#  や "C♯と書いている場合とか、
全角マイナス「-」、長音記号「ー」、漢数字「一」、ダッシュ記号「―」などが
入れ替わっている場合なども、ついつい気になって指摘してしまう…。


> 業種を明らかにしてしまうようなものですが、これは、
実際の業務仕様を明かしすぎるとマズいでしょうし、
細かい点は単純化するなどして、適宜暈しておいてください。
(背景が分かった方が、状況を理解しやすいという側面はありますが…)

前回投稿いただいた時のように、元となるテーブルの各列の型や、
求めている最終的な変換後の構造が分かっていると答えやすいです。


> 「消費税課税」フィールドには、-1、0、1の数値を入れることとし、それぞれ、
> -1 : 免税
> 0 : 原則課税
> 1 : 簡易課税
> という意味を持たせています。

数値データを、対応する文字列として表示したいという点については、
その変換テーブルを Access 側に持たせているなら、JOIN して持ってくるのが
手っ取り早いでしょう。(あるいは、IIF 等で変換しておくとか)

でも今回求めているのは、Access 側で変換しておく方法ではなく、
データベースからは -1 とか 1 といった値のまま受け取るようにして、
それを VB 側で "原則課税" や "(原)" といった値に変換しようとしているものと認識しています。


> 親フィールドと子フィールドの型を一致させる、とのことですが、
これが必要なのは、DataSet 上で、複数テーブル間のリレーションを貼る場合に限ります。
(先の Relations.Add メソッドの所です)


そのため、データベースから得られる -1 や 1  という値を持つ [消費税課税] 列の情報が、
DataTable に格納された時に、Integer なのか String なのか Decimal なのか、
どの型になっているのか、クエリ1 を格納した DataTable の内容を
デバッガ等で調べてください、とお願いしている次第です。
(それを調べられるのは、実際に環境をお持ちの、さすがさん御自身だけです!)


連結する列の型が判明したら、リレーション先となる変換用テーブル
(Access 側には存在せず、VB 側にだけ持たせる DataTable) 内の
「主キー列」の型を、それに合わせるようにします。

VB 側で DataTable を用意する場合、それの列の型を指定する方法は、
何度も登場しているので問題ないですよね。

shu さんのコードから引用すれば、
> With SrcTbl.Columns
>         .Add("Column1", GetType(Integer))   'Integer 型の列を定義
>         .Add("Column2", GetType(String))    'String 型の列を定義
> End With

私のコードから引用すれば、
> dt.Columns.Add("契約ID", GetType(Long))   'Long 型の列を定義
> dt.Columns.Add("決算日", GetType(Date))   'Date 型の列を定義
> dt.Columns.Add("消費税課税", GetType(消費税課税)) 'Enum 消費税課税 As Integer な列
> dt1.Columns.Add("消費税課税", GetType(Decimal))    'Decimal 型の列を定義

などのように、Columns.Add の第二引数で「目的の型」を指定します。


> 子フィールド("消費税課税")が、データベースから取り出した、
> -1
> 0
> 1
> というInteger型である、

Integer 型であると判明したのですね?
であれば、変換テーブルの主キーを GetType(Integer) にするだけです。


変換テーブルの主キー列の型が Integer に決まったので、
親となるその変換テーブルに対して、shu さんが提示してくださったように、
.Rows.Add を必要な回数呼び出して、Integer を主キーとしたデータを登録しておきます。
  dt.Rows.Add(-1, "(免)")
  dt.Rows.Add(0, "(原)")
  dt.Rows.Add(1, "(簡)")

あるいは固定値を Rows.Add する他、列挙型や Dictionary から生成したり、
CSV などから読み取って変換テーブルを作ることもできるわけですが…何はともあれ、
とにかく、変換用の DataTable を適切な列定義で準備することが肝となります。
PrimaryKey の設定も忘れずに!


変換テーブル側の主キーの型を、クエリ1 の型に合致させておけば、先の
 ds.Relations.Add("Rel消費税課税", 変換テーブルの主キー列, クエリ1の消費税課税列, True)
がエラーになる事は無くなるでしょう。



あるいは、「そもそも Relations を使わない」という選択肢もあります。

クエリ1 で取得したテーブルというのは、要するにただの DataTable に過ぎないわけですから、
SQL から取得した後で、さらにそこに、空の String 列を追加で .Columns.Add することも
できるわけです。

空列を追加したら、クエリ1 用の DataTable の各行をループ処理で回し、
If 文で「消費税課税」列の値を調べ、それが "原則課税" を表す 0 であったなら、
追加した列に、"(原)" という値をセットしていくということができます。

これならば、リレーション用の第二の DataTable を用意する必要はなくなります。


あるいは クエリ1 のテーブルを追加加工するのではなく、クエリ1 の DataTable は残したまま、
最終結果を入れるための DataTable を用意して、そこに値を詰めていくのでも良いでしょう。
(此方の実装パターンも、形を変えて既にサンプルを投稿してありますね)

投稿者 さすが   (社会人)   投稿日時 2019/8/19 18:17:33
取り急ぎご報告いたします。

よくよく調べますと、Accessファイルより取得した"クエリ1"の、「消費税課税」フィールドの
型はSystem.Int16でした。

> dt.PrimaryKey = New DataColumn() {dt.Columns.Add("Key", GetType(消費税課税))}

を、

 dt.PrimaryKey = New DataColumn() {dt.Columns.Add("Key"GetType(System.Int16))}

に書き換え、いくらかの試行錯誤の結果、ようやくDataGridViewに表示されました。

大変ありがとうございました。

もう1つの方は、近々じっくり当たりたいと考えております。
もうしばらくお待ちください。

投稿者 さすが   (社会人)   投稿日時 2019/8/22 10:18:13
当初の質問につき、引き続きよろしくお願いします。

実は、DataGridViewのフォームとは別に、レコードの追加・編集用フォームを設けたいのです。
レコードを追加・編集をすれば、瞬時にDataGridViewの表示内容も更新されるようにです。
表示させる順序も決めていまして、Columns("決算日")の降順です。
ここでDataGridViewのDataSourceはDs、
DataMenberは"クエリ1"というDataTableとします。

まずは、レコードの追加から取り掛かっています。

自分で調べ、コードを書き、試行錯誤の末、ついに
データベース(Accessファイル)へのレコード追加ができました。

次に、更新後の内容をDataGridViewに表示させるところに取り掛かっています。
頭を冷やす時間がなく、昨晩は、わざわざDataSetから"クエリ1"をRemoveしたり、
そのためにRelationやForeingKeyConstraintのRemoveをしたりして
再度"クエリ1"を生成する方法をとりました。

あとで、直接"クエリ1"にRowsをAddするコードに書き直した方がいいですかね?

本題に戻りますと、
ところがその後、DataGridViewが更新されません。
DataSetビジュアライザーを覗くと、"クエリ1"は更新されていますのに。

実は、今回の質問をさせていただく前は、Access側のTable(正規化に問題あり)で、
レコード追加後に瞬時にDataGridViewは更新されました。

その時のコードは、

Rs = New ADODB.Recordset
(中略)
Da.Fill(Ds, Rs, "クエリ1")

でした。つまりRecordsetがありました。

現在は、正規化に伴い必要となったコードの書き換え作業ということで、
教えていただいたコードを用い、

Ds.Fill(dt1)

としています。

ここから、Recordsetがないのが原因では?と推測しているのですが、
かといって、どう書き直せば良いかも分かりません。

以上、改めましてよろしくお願いします。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/23 00:11:49
> ここから、Recordsetがないのが原因では?と推測しているのですが、
> かといって、どう書き直せば良いかも分かりません。

ここ(VB中学校)のサイトには、「データベース講座」も用意されているのですが、
未完のままで、データを更新するところまでは書かれていないのですよね…。
(VB6 編の方は、レコードの修正・追加・削除まで書かれているのですけれども)


さて。データベース側に編集結果を反映させるには、
 (案1) OleDbCommand の ExecuteScalar メソッドを用いて、INSERT/DELETE/UPDATE クエリを呼び出す
 (案2) DataTable を編集後、OleDbDataAdapter.Update で反映させる。(または TableAdapter)
 (案3) Recordset を更新後、ADODB.Recordset の Update メソッドで、編集結果を反映させる。
 (案4) ADODB.Command の Execute メソッドを用いて、INSERT/DELETE/UPDATE クエリを呼び出す
 (案5) ADODB.Connection の Execute メソッドを用いて、INSERT/DELETE/UPDATE クエリを呼び出す
などの方法があります。

案1と案2 は、「ADO.NET」による一般的な手法であり、
案3~案5 は、VB6 以前や VBA で使われている「ADO」による手法です。


データベースに値を書き戻すことを考えると、ADO ベースの開発はデメリットが多いです。
ADO.NET を用いた開発手法を習得されることを、強く強くお奨めします。


というのも、System.Windows.Forms の画面部品は、ADO.NET の DataSet/DataTable との
連携を前提として設計されている部分が数多くある一方で、
ADO (ADODB) との連携については、ほとんど考慮されていないためです。


たとえば、Recordset の内容を DataGridView に直接表示させる方法さえ存在しません。

Recordset を MoveNext しながら値を読み取って、各行のデータを転記するループ処理を記述するか、
もしくは既に行われているように、OleDbDataAdapter.Fill メソッドを呼び出して、Recordset の内容を
いったん DataTable に転記しておいてから、それをバインドさせて表示するかのいずれかとなります。


またその逆に、ユーザーが編集した DataGridView あるいは DataTable の内容をデータベース側に
書き戻すための機能についても特に用意されていません。そのため、更新された DataTable の内容を
上記の案3~5 の処理に渡すための処理は、自分で個別に作りこまねばなりません。


そのため、Recordset 経由でのアクセスとなると、ADO.NET と ADO の両方の機能に
精通している必要があるという点は覚悟しておいてください。
それでも良ければお付き合いしますが、資料は少ないので、茨の道かも知れません。

VBA からの Recordset 操作であれば、それこそ 1997年ぐらいから多数執筆されていますが、
.NET からの Recordset 操作となると、詳しい資料は殆ど無いはずです。
あったとしても、OleDbDataAdapter.Fill による Recrodset からの「読み取り」ぐらいまでであり、
データ更新やストアドの呼出し手順にまで言及された資料を見た記憶がありません。


一応参考までに、DataTable の内容を Recordset に逆変換するための資料も置いておきます。
ただしこれは、データベースに書き戻すための処理では無いので、今回使うことは無さそうですが…。
https://dobon.net/vb/bbs/log3-55/32198.html



もしも ADO.NET 方針で行くのなら、まずはそちらについて学んでおいてください。

どうしても ADO を引き続き使わねばならない理由があるというのであれば、それはそれで
お付き合いいたしますが、どちらにせよ ADO.NET の知識は必要になってくるでしょう。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/23 01:01:52
2002~2003 年当時であれば、ADO から ADO.NET への移行(マイグレーション)のための資料も
それなりにあったのですが、今見たら、既に失われている資料も多いですね…。
http://uno036.starfree.jp/PRGmanual/vbnet_migration/ado.html

とはいえ、現在お使いの VB2005 にも、ADO からの移行のための資料は記載されていたと
思いますので探してみてください。

あるいは、ADO 時代の知識は捨てて、一から ADO.NET について学び直しても良いかも知れません。
日本語である事にこだわらなければ、YouTube での動画解説も幾つかアップされていました。


> 実は、DataGridViewのフォームとは別に、レコードの追加・編集用フォームを設けたいのです。

こういう場合は、『DataGridViewのフォーム』と『レコードの追加・編集用フォーム』とで、
同一の DataSet インスタンスを使うようにします。
http://blogs.wankuma.com/hatsune/archive/2006/11/28/47367.aspx
https://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=13532&forum=7

つまり、それぞれのフォームにおいて、Load イベント菜緒で
 Me.MyDataSet = あらかじめ取得済みのデータセットインスタンス
 Me.BindingSource1.DataSource = Me.MyDataSet
 Me.BindingSource1.DataMember = テーブル名
 Me.DataGridView1.DataSource = Me.BindingSource1
のようにしておきます。
※ DataSet インスタンスをセットするところ以外は、デザイン時に済ませておいても OK。

各画面で同じ DataSet を共有していれば、一方の画面で編集/追加/削除した行が、
即時にもう一方の画面にも反映されるので、データ転送の手間が省けます。


> そのためにRelationやForeingKeyConstraintのRemoveをしたりして
> 再度"クエリ1"を生成する方法をとりました。

こういう場合には、自前で DataSet を組み上げるのではなく、「型付き DataSet」を使うようにすると、
デザイナ画面からの割り当てが可能になるため、コーディングの手間を削減できます。

型付き DataSet の作成・設定方法はこちらをご覧ください。
前回行った主キー設定やリレーションの設定も、デザイン時に行っておくことができます。
https://masagoroku.com/%E3%80%90vb-net%E3%80%91%E5%9E%8B%E4%BB%98%E3%81%8D%E3%83%87%E3%83%BC%E3%82%BF%E3%82%BB%E3%83%83%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95


今回はクロス集計があるため、型付 DataSet だけでは対処しきれない点も
あるかもしれませんが、その場合は前回同様、動的に列や表を追加するスタイルとも併用できます。

この「型付 DataSet」にはテーブル定義に加え、データベースから
データを取得(あるいは削除・更新・追加)するための「TableAdapter」を
追加することもできます。(今は「Recordset + DataAdapter」を使っているのでしょうけれども)



フォーム作成の件に話を戻すと……。


(1) データ一覧用フォーム

プロジェクトに型付 DataSet がある場合、Visual Studio の
[データ ソース]ペインから該当テーブルを Form にドロップすることで、
フォームへの DataGridView の配置と、DataSource プロパティ等へのバインド作業が完了します。

この場合、フォームに配置されるのは DataGridView のインスタンスだけではなく、
DataSet & BindingSource & BindingNavigator のインスタンスも自動配置されます。


BindingNavigator の役目については、見ればすぐに分かると思いますが、
「別のレコードへの移動」「新規レコードの追加」「レコードの削除」などを
ひとまとめのボタン群に束ねたものです。

BindingSource コンポーネントは、「コントロール」と「DataSet」の間を取り持つ中継役として働きます。
そして BindingNavigator.DataSource や DataGridView.DataSource は
DataSet を直接参照するのではなく、この BindingSource を参照しています。
どうして、BindingSource 経由で参照されているのかというと、これには 2 つの理由が
あるのですが、ここでは説明を省きます。


(2) データ編集用フォーム

[データ ソース]ペインにあるアイテムを Form にドロップする際に、
テーブルそのものではなく、テーブル内の各列をフォームにドロップすると、
レコード編集画面用のデザインパーツが配置されます。
https://docs.microsoft.com/ja-jp/visualstudio/data-tools/add-new-data-sources?view=vs-2019



DataGridView の時は、DataSource プロパティを通じてバインドされていましたが、
此方の場合は、「TextBox1.DataBindings.Add("Text", データソース, …)」などの設定が行われます。
(デザイナを用いず、自前で .DataBindings.Add してももちろん OK)

ドロップで配置されるコントロールは TextBox だけでなく、各列のデータ型に合わせて、
Label/ComboBox/ListBox/NumericUpDown/DateTimePicker 等、他のコントロールにもできます。
使用可能なコントロールの種類を増やしたい場合は、オプション設定画面の
[データ UI カスタマイズ]から設定しておくことができます。
https://docs.microsoft.com/ja-jp/visualstudio/ide/reference/options-windows-forms-designer-data-ui-customization



もし、コントロールを配置した後で、バインド設定を修正したい場合には、
デザイン画面でそのコントロールを選択し、プロパティ一覧から
"(DataBindings)" - "(詳細)" 欄から [...] を選択して、
「フォーマットと詳細バインド」画面から変更します。
これにより、先の .DataBindings.Add の自動生成コードが変更されます。
https://www.atmarkit.co.jp/fdotnet/chushin/introwinform_06/introwinform_06_02.html
[IMAGE:https://www.atmarkit.co.jp/fdotnet/chushin/introwinform_06/introwinform_06_07_s.gif]


あるいは先の 簡易課税 ⇔ 1 の相互変換や、イベント処理などの都合から、
あえてデータバインドは使わずに、該当行のデータを読み取って、
 TextBox1.Text = 現在行のデータ
とか、
 Combox1.SelectedIndex = 該当番号
などのコードを、一つ一つ書いて表示させる方法もあるでしょう。この辺は要件次第。

ただしデータバインドを用いなかった場合、コントロールで編集した結果を、
もう一度 DataTable に書き戻すためのコードも書く必要がありますね。


DataTable の内容が、更新あるいは削除された後は、
OleDbDataAdapter (あるいは TableAdapter)の Update メソッドに対して
編集した DataSet を渡してやれば、その内容がデータベースに反映されます。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/23 01:30:12
> ところがその後、DataGridViewが更新されません。
> DataSetビジュアライザーを覗くと、"クエリ1"は更新されていますのに。

実際のコードを見てみない事には何とも言えませんが、おそらくそれは、
両者が見ている DataTable が、それぞれ別のインスタンスなのだと思います。


話を単純化するために、単一のフォームで説明しますが、
たとえば下記を実行すると、ComboBox1 と ComboBox2 が選択しているアイテムは
それぞれ連動するのに対し、ComboBox3 は連動していないことがわかります。


Private Sub Form1_Load(ByVal sender As ObjectByVal e As EventArgs) Handles MyBase.Load
    Dim ds As New DataSet()
    Dim tbl As DataTable = ds.Tables.Add("TBL")
    tbl.Columns.Add("Key"GetType(Integer))
    tbl.Columns.Add("Value"GetType(String))
    tbl.PrimaryKey = New DataColumn() {tbl.Columns("Key")}
    tbl.Rows.Add(100, "AAA")
    tbl.Rows.Add(200, "BBB")
    tbl.Rows.Add(300, "CCC")
    tbl.Rows.Add(400, "DDD")
    tbl.AcceptChanges()

    ComboBox1.DisplayMember = "Value"
    ComboBox2.DisplayMember = "Value"
    ComboBox3.DisplayMember = "Value"

    ComboBox1.ValueMember = "Key"
    ComboBox2.ValueMember = "Key"
    ComboBox3.ValueMember = "Key"

    '1 と 2 は同じインスタンスを見ているが 
    '3 は別のインスタンスを参照している 
    ComboBox1.DataSource = tbl
    ComboBox2.DataSource = tbl
    ComboBox3.DataSource = tbl.Copy()
End Sub

Private Sub ComboBoxes_SelectionChangeCommitted(ByVal sender As ObjectByVal e As EventArgs) Handles ComboBox1.SelectionChangeCommitted, ComboBox2.SelectionChangeCommitted, ComboBox3.SelectionChangeCommitted
    Label1.Text = "SelectedIndex"
    Label2.Text = "SelectedValue"
    Label3.Text = "Text"
    Label4.Text = "GetItemText()"
    For Each cb As ComboBox In New ComboBox() {ComboBox1, ComboBox2, ComboBox3}
        Label1.Text &= ", [" & CStr(cb.SelectedIndex) & "]"
        Label2.Text &= ", [" & CStr(cb.SelectedValue) & "]"
        Label3.Text &= ", [" & cb.Text & "]"
        Label4.Text &= ", [" & cb.GetItemText(cb.SelectedItem) & "]"
    Next
End Sub


投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/23 10:28:27
【ADO.NET を使って、データベースに書き戻す場合】

ADO.NET では、行編集・行追加・行削除された状態の DataSet を、
OleDbDataAdapter オブジェクトの Update メソッドに渡すことで、
編集したレコードを、データベースへと反映することができます。

このとき使う DataSet は、Recordset から Fill されたものであっても構いません。
(DataTable 上にある主キー情報や列設定さえ正しければ OK)

このとき使う OleDbDataAdapter オブジェクトは、事前に
InsertCommand/UpdateCommand/DeleteCommand プロパティに対して
更新用の DML クエリをセットしておくことが必要となります。
この DML クエリは、自分で個別に設定する必要はなく、通常は、
OleDbCommandBuilder を通じてこれらを自動的にセットするようにするか、
もしくは、型付 DataSet の TableAdapter を使って事前に用意しておきます。
http://vb.net-informations.com/dataadapter/dataadapter-commandbuilder-oledb.htm


【ADO を使って、データベースに書き戻す場合】

DataSet の内容を、ADO を通じて更新する場合は、DataTable から
 ・編集された行
 ・新規に追加された行
 ・削除された行
の情報を取り出し、それを使ってデータベースに書き戻すことになります。
(ADO の使い方は Access VBA と基本的に同じなので、ここでは説明を省略します)

DataTable から値を取り出す手順については、下記を参照してみてください。
http://masa49406.blogspot.com/2013/12/vbnetdataviewrowstate.html

投稿者 さすが   (社会人)   投稿日時 2019/8/23 18:04:30
魔界の仮面弁士様

いつもたくさんのご回答、誠にありがとうございます。

【投稿日時 2019/8/23 01:30:12】について、取り急ぎご返事いたします。

>実際のコードを見てみない事には何とも言えませんが、おそらくそれは、
>両者が見ている DataTable が、それぞれ別のインスタンスなのだと思います。

そうだとすれば非常におかしいですね。
直前に、dt1(TableNameは"クエリ1")をRemoveし、一度DataSetからdt1を除去し、
そのあとにdt1を再生成し(TableNameは引き続き"クエリ1")、
DataGridViewのDataSourceやDataMenberをNothingにしてから
再びDataSourceをDataSet、DataMemberを"クエリ1"に指定していますのに。
DataSetとDataGridViewの間に、除去する前のデータを格納する場所があるかのようです。

いちおう、このご回答をヒントに、
最初に生成したDataTableの名前を"Query"のように名づけ、
レコード追加後に生成するDataTableの名前を"Query1"のように名づけ変えてみましたところ、

ケース①
DataGridViewのDataSourceやDataMenberをNothingにしてから、
DataSourceをDataSetに指定し、DataMemberにはDataSetにはない"Query"を再び指定してやると、
DataSetビジュアライザにて"Query"は存在せず、"Query1"が存在することを確認しましたが、
エラーなくDataGridViewに"Query"が表示され、あたかも未更新のようになりました。

ケース①-2
DataGridViewのDataSourceやDataMenberをNothingにする、
そこまででデバッグが終わるように書き換えたところ、
DataGridViewは何も表示されないことを確認しました。

ケース② 
DataGridViewのDataSourceやDataMenberをNothingにしてから、
DataSourceをDataSetに指定し、新たにDataSetにできた"Query1"をDataMemberに指定してやると、
DataGridViewには"Query1"の内容が表示され、あたかも更新されたようになりました。

レコード更新回数のカウンターをすでにコードに設けていますので、
更新や編集の都度、カウンターを1ずつ増やし、DataTableの名前を"Query" & "カウンター"にしてやるという応用を行えば、
あたかもDataGridViewが更新されたように見えますので、これで解決はするのですが、気持ち悪いです。

ADO.Netにつきましては、時間はかかりそうですが少しずつ学んでいきたいと考えております。

どうぞよろしくお願いします。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/8/27 09:41:18
DataMember が、ところどころで
DataMenber になっているのが気にかかりますが、それはさておき。


> 直前に、dt1(TableNameは"クエリ1")をRemoveし、一度DataSetからdt1を除去し、
そのあとにdt1を再生成し(TableNameは引き続き"クエリ1")、

DataSet のインスタンスは引き続き同じものを使っており、
DataTable のインスタンスだけを新たに作り直した……という事ですね。


> DataGridViewのDataSourceやDataMenberをNothingにしてから
> 再びDataSourceをDataSet、DataMemberを"クエリ1"に指定していますのに。

先の『ところがその後、DataGridViewが更新されません。』という表現が、
「レコードの追加」を行うためにどのような操作をしていたのかが分かりませんでした。

実際の事例に沿ったものでなくても良いので、状況が伝わりやすくなるよう、
最低限のコードを提示した上で質問いただけると有難いです。

たとえばこんな感じで…。(下記はレコード追加の例では無いですが)
Private Ds As DataSet
Private Sub Button1_Click(…
  Rs = New ADODB.Recordset
  '(中略) 
  Da.Fill(Ds, Rs, "クエリ1")

  'これで、クエリ1 の内容が表示されることまでは確認できた 
  dataGridView1.DataSource = Ds
  dataGridView1.DataMember = "クエリ1"
End Sub
Private Sub Button2_Click(…
  'しかし下記のようにしても、何故か DataGridView が空にならなかった 
  Ds.Tables("クエリ1").Row.Clear()
End Sub




=== 以下、先の回答時に自分が連想していたパターンを列挙してみます ===

➊「レコードの追加・編集用フォーム」で編集したときの DataTable の内容を
DataSetビジュアライザーを覗くと、"クエリ1"は更新されていたけれども、
それを「DataGridViewのフォーム」で確認した時に、
>> ところがその後、DataGridViewが更新されません。
という状態になっている…という状況。(先の回答はこれを想定した物です)

➋ まだ「レコードの追加・編集用フォーム」での操作するまでには至っておらず、
DataGridView 上の新規追加行に書きこんだ上で
DataSet ビジュアライザーの内容を見たときに、DataGridView の編集結果が
DataSet に反映されていなかった…という状況。

➌ "クエリ1" を持つ DataTable に対して .Rows.Add した後で、
DataSet ビジュアライザーの内容を見ると書き換わっているのに、
それが DataGridView の表示に反映されていない…とい状況。


> ケース①
> ケース①-2
> ケース② 
ありがとうございます。
どのように確認したのかという状況が、少し把握しやすくなりました。


> DataSetとDataGridViewの間に、除去する前のデータを格納する場所があるかのようです。

DataSet や DataTable をバインドした場合、実質的には DataTable の DefaultView がバインドされます。
この場合の DefaultView は、既定で DataViewRowState.CurrentRows モードであるために、
DataTable 上で削除済みとしてマークされた行については、
DataSet 上には存在していますが、そのままだと DataGridView には表示されません。
でも、今話しているのはそのことではなさそうですね。


また、DataGridView 上で編集されたデータは、その処理タイミングごとに、
 (1)バインド元の DataRow が、まだ何も編集されていない状態(DataRowState が Unchanged)
 (2)ユーザーの入力によって、DataGridViewEditingControl 上で入力されたが、
  バインド元の行にはまだ伝わっていない段階
 (3)ユーザーからの入力が DataRowView を通じて DataRow に渡されたが、
  その行はまだ、DataTable の行としてアタッチされていない状態(DataRowState が Detached)
 (4)DataTable の内容が DataRow に渡されており、DataRowState が
  編集行(Modified)、削除行(Deleted)、新規行(Added)のいずれかとして
  マークされている段階
の 4 段階の遷移を取ります。
なので、(2) や (3) の時点で確認すると、DataGridView の内容と DataSet の内容に
タイミング的なズレが生じます。上記 ➋ の場合は (1) や (2) の段階がありえますね。



> ケース①
> DataGridViewのDataSourceやDataMenberをNothingにしてから、
> DataSourceをDataSetに指定し、DataMemberにはDataSetにはない"Query"を再び指定してやると、
> DataSetビジュアライザにて"Query"は存在せず、"Query1"が存在することを確認しましたが、
> エラーなくDataGridViewに"Query"が表示され、あたかも未更新のようになりました。

これはデータをリロードする処理でしょうか。
DataSoure や DataMember の操作が反映されていないようであるのだと理解しました。

最初はスペルミス等により、コードから操作している DataGridView というのが、
実は画面上に見えている DataGridView とが別物だったという可能性を疑いましたが、
でもそれだと、ケース①-2 や ケース② の説明がつかないですものね…。


この割り当てはどのタイミングで行っていますか?
Button1_Click とかでしょうか?

64bit 環境の場合は、Form1_Load で例外が発生しても気がつきにくいのでご注意ください。
http://rucio.cocolog-nifty.com/blog/2011/04/post-f125.html

とりあえず、手元の環境では ① のパターンを確認できなかったので、現象を再現するために、
この部分に関与するコードを提示頂けないでしょうか。こちらで追試してみたいと思います。

投稿者 さすが   (社会人)   投稿日時 2019/9/26 18:48:01
大変ご無沙汰しております。申し訳ございません。

2019/8/14 09:38:11に返答くださった内容について質問させていただきます。

For Each entry1 As KeyValuePair(Of KeyValuePair(Of StringString), StringIn dic
     Dim newRow As DataRow = dt.NewRow()
     newRow("担当者名") = entry1.Key
     dt.Rows.Add(newRow)
     For Each entry2 As KeyValuePair(Of StringStringIn entry1.Value
         newRow(entry2.Key) = entry2.Value
     Next
Next

ですと、

「列"何月何日"はテーブルに属していません」

というエラーが発生しました。

For Each entry1 As KeyValuePair(Of KeyValuePair(Of StringString), StringIn dic
     Dim newRow As DataRow = dt.NewRow()
     newRow("担当者名") = entry1.Key
     dt.Rows.Add(newRow)
     For Each entry2 As KeyValuePair(Of StringStringIn entry1.Value
         dt.Colums.Add(entry2.key)
         newRow(entry2.Key) = entry2.Value
     Next
Next

とすると、無事テーブルが完成しました。

 dt.Colums.Add(entry2.key)

の1行は必要と理解してよろしいでしょうか?

どうぞよろしくお願いします。

投稿者 魔界の仮面弁士   (社会人)   投稿日時 2019/9/27 09:15:35
> dt.Colums.Add(entry2.key)
> の1行は必要と理解してよろしいでしょうか?

はい。Columns.Add は必ず必要です。

2019/8/14 09:38:11 の投稿で言えば、最後の (案2) のところに書かれた、
『Columns.Add するようにする』と書いたのがそれに当たりますし、
2019/8/13 21:39:08 の投稿で言えば、下記の箇所に当たります。

Dim gDay As String = CStr(row("業務完了日"))   '元データが日付型の場合は Format して取得すること 
'(中略) 
If Not dt.Columns.Contains(gDay) Then
    '未登録の列見出しを追加  
    dt.Columns.Add(gDay, GetType(String)).AllowDBNull = True
End If


既に登録済みのキーを再追加するわけには行かないので、
2019/8/13 21:39:08 の実装例では Contains を併用したコードとしています。
2019/8/14 09:38:11 のように、列見出しの一覧(昇順)を一意に取得しておく方法を
採用する場合には、Contains 判定を行う必要はありません。

投稿者 さすが   (社会人)   投稿日時 2019/9/27 19:30:11
魔界の仮面弁士さま

長らくお付き合い、誠にありがとうございます。

「DataTableから新たなDataTableを生成し表示」という質問
(本旨は、Accessでいうところの?集計クエリから新たな集計クエリを生成し表示)
に対しまして、

dictionaryを使用するというご提案をいただき、
この度ようやく、その案で実現できるとの実感を得ました。

これにて「解決」とさせていただきます。