C#でLinq to objectの結果をDataTableにしてみた

SQLite3のデータを読み込んだDataTableからLinq to objectでフィルタまたは結合した結果を、DataTableに入れてみました。そのメモです。SQLiteには非接続型のデータアクセスをしてるので、SQLiteに限った話ではないです。

目次

  1. 環境
  2. CopyToDataTableメソッドを使う
  3. 結合結果格納用のDataTableを使う
  4. 試してみた
    1. アプリの概要
    2. アプリの準備
    3. ウィンドウのレイアウトをする(Viewを作る)
    4. ViewModelを作る
    5. Modelを作る
    6. 動かしてみた
  5. まとめ

環境

  • Visual Studio 2015 Express for Windows Desktop (C#)

CopyToDataTableメソッドを使う

Linqのクエリの結果をIEnumerableで得られる場合は、 `DataTableExtensions.CopyToDataTable()メソッド <https://msdn.microsoft.com/ja-jp/library/bb396189%28v=vs.110%29.aspx>`_でDataTableが得られます。

例えば、hogehogeというDataTableのField2という列にpiyoという文字列が含まれる行(レコード)を抜き出したDataTableを作りたい場合は、下記のようにします。

IEnumerable<DataRow> query =
    from p in hogehoge.AsEnumerable()
    where p.Field<string>("field2").Contains("piyo")
    select p;
DataTable ViewTable = query.CopyToDataTable<DataRow>();

結合結果格納用のDataTableを使う

Linq to objectで結合(JOIN)した場合ですが、その結果をIEnumerableにする方法が分りませんでした。(勉強不足)

ということで、新しいDataTableを定義して結合結果を入れてみました。例えば、下記の2つのDataTableがあるとします。

  • テーブル名 hogehoge フィールド id, fieldA, fieldB

  • テーブル名 piyopiyo フィールド id, fieldC

hogehogeのfieldBとpiyopiyoのidがリンクするとして、hogehogeとpiyopiyoを結合します。結合した結果を入れるDataTableをrとします。結合結果用のDataTableにフィールドの定義をするには、 DataColumnクラス のインスタンスを作成してそのインスタンスのプロパティにフィールドの属性を定義し、 DataColumnCollection.Add()メソッド でDataTableに追加します。フィールド毎にこれを繰り返します。そして、Linqのクエリの結果をforeachでDataTableに追加します。

// 結合の結果を保持するテーブルの作成
DataTable r = new DataTable();
DataColumn column = new DataColumn();
column.DataType = System.Type.GetType("System.Int64");
column.ColumnName = "id";
r.Columns.Add(column);
column = new DataColumn();
column.DataType = System.Type.GetType("System.String");
column.ColumnName = "fieldA";
r.Columns.Add(column);
column = new DataColumn();
column.DataType = System.Type.GetType("System.String");
column.ColumnName = "fieldC";
r.Columns.Add(column);

// 結合を実行する
var query =
    from p in hogehoge.AsEnumerable()
    join q in piyopiyo.AsEnumerable()
    on p.Field<Int64>("fieldB") equals q.Field<Int64>("id")
    select new
    {
        id = p.Field<Int64>("id"),
        fieldA = p.Field<string>("fieldA"),
        fieldC = q.Field<string>("fieldC")
    };
DataRow row;
foreach (var s in query)
{
    row = r.NewRow();
    row["id"] = s.id;
    row["fieldA"] = s.fieldA;
    row["fieldC"] = s.fieldC;
    r.Rows.Add(row);
}

何か力技な感じ。

試してみた

アプリの概要

2つのテーブルを持つSQLiteのデータベースファイルからデータを読み込んで、下記をするアプリを作ってみました。

  • DataGridに、それぞれのテーブルを表示する。

  • DataGridに、「山」を含む短歌を表示する。

  • DataGridに、短歌と作者を合わせて表示する。

元のデータは、下図の様なものです。(各テーブルをPupSQLiteで表示したもの。)

サンプル1 サンプル2

poemテーブルのpoetフィールドとpoet_masterフィールドのidフィールドがリレーションするわけです。

アプリの準備

SQLiteのデータにアクセスするので、NuGetでSystem.Data.SQLiteをインストールします。

参照の追加

また、MVVMっぽく作りたいので、ソリューションにcommonというフォルダを作ってそこに以前使ったViewModelBase.csとDelegateCommand.csをコピーします。

ファイルの追加

ウィンドウのレイアウトをする(Viewを作る)

ウィンドウに、DataGridを1つとbuttonを4つ配置します。

画面レイアウト

XAMLを下記のようにします。

<Window x:Class="linq_trial.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:linq_trial"
        mc:Ignorable="d"
        Title="MainWindow" Height="480" Width="640">
    <Grid>
        <DataGrid x:Name="dataGrid" ItemsSource="{Binding Path=ViewTable, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="312" Width="615"/>
        <Button x:Name="buttonShowPoemTable" Command="{Binding Path=ShowPoemTable}" Content="Poemテーブル" HorizontalAlignment="Left" Margin="90,424,0,0" VerticalAlignment="Top" Width="75"/>
        <Button x:Name="buttonShowPoetTable" Command="{Binding Path=ShowPoetTable}" Content="Poetテーブル" HorizontalAlignment="Left" Margin="170,424,0,0" VerticalAlignment="Top" Width="75"/>
        <Button x:Name="buttonFilter" Command="{Binding Path=FilterPoem}" Content="山でフィルタ" HorizontalAlignment="Left" Margin="250,424,0,0" VerticalAlignment="Top" Width="75"/>
        <Button x:Name="buttonJoin" Command="{Binding Path=JoinData}" Content="JOIN" HorizontalAlignment="Left" Margin="330,424,0,0" VerticalAlignment="Top" Width="75"/>

    </Grid>
</Window>

自動生成されたXAMLに対して変更したのは、下記の点です。

  • Button要素のContent属性(ボタンに表示される文字列)の変更

  • Button要素のCommand属性の追加(コマンドバインディング)

  • DataGrid要素のItemSource属性の追加(データバインディング)

ViewModelを作る

ViewModel(MainWindowVM.cs)を下記のようにしました。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using System.Data;

namespace linq_trial
{
    public class MainWindowVM : ViewModelBase
    {
        public DataTable ViewTable { get; set; } // DataGridにバインディングするDataTable
        public DataTable PoemTable { get; set; } // poemを保持するためのDataTable
        public DataTable PoetTable { get; set; } // poet_masterを保持するためのDataTable

        // poemテーブルを表示する
        private ICommand _ShowPoemTable;
        public ICommand ShowPoemTable
        {
            get
            {
                if (_ShowPoemTable == null)
                {
                    _ShowPoemTable = new DelegateCommand(ExecuteShowPoemTable);
                }
                return _ShowPoemTable;
            }
        }
        private void ExecuteShowPoemTable()
        {
            ViewTable = PoemTable;
            RaisePropertyChanged("ViewTable");
        }

        // poet_masterテーブルを表示する
        private ICommand _ShowPoetTable;
        public ICommand ShowPoetTable
        {
            get
            {
                if (_ShowPoetTable == null)
                {
                    _ShowPoetTable = new DelegateCommand(ExecuteShowPoetTable);
                }
                return _ShowPoetTable;
            }
        }
        private void ExecuteShowPoetTable()
        {
            ViewTable = PoetTable;
            RaisePropertyChanged("ViewTable");
        }

        // poemテーブルのうち、「山」という文字が入っている短歌だけ表示する
        private ICommand _FilterPoem;
        public ICommand FilterPoem
        {
            get
            {
                if (_FilterPoem == null)
                {
                    _FilterPoem = new DelegateCommand(ExecuteFilterPoem);
                }
                return _FilterPoem;
            }
        }
        private void ExecuteFilterPoem()
        {
            IEnumerable<DataRow> query =
                from p in PoemTable.AsEnumerable()
                where p.Field<string>("body").Contains("山")
                select p;
            ViewTable = query.CopyToDataTable<DataRow>();
            RaisePropertyChanged("ViewTable");
        }

        // poemテーブルとpoet_masterテーブルを結合して表示する
        private ICommand _JoinData;
        public ICommand JoinData
        {
            get
            {
                if (_JoinData == null)
                {
                    _JoinData = new DelegateCommand(ExecuteJoinData);
                }
                return _JoinData;
            }
        }
        private void ExecuteJoinData()
        {
            // 結合の結果を保持するテーブルの作成
            DataTable r = new DataTable();
            DataColumn column = new DataColumn();
            column.DataType = System.Type.GetType("System.Int64");
            column.ColumnName = "id";
            r.Columns.Add(column);
            column = new DataColumn();
            column.DataType = System.Type.GetType("System.String");
            column.ColumnName = "body";
            r.Columns.Add(column);
            column = new DataColumn();
            column.DataType = System.Type.GetType("System.String");
            column.ColumnName = "name";
            r.Columns.Add(column);

            // 結合を実行する
            var query =
                from p in PoemTable.AsEnumerable()
                join q in PoetTable.AsEnumerable()
                on p.Field<Int64>("poet") equals q.Field<Int64>("id")
                select new
                {
                    id = p.Field<Int64>("id"),
                    body = p.Field<string>("body"),
                    poet = q.Field<string>("name")
                };
            DataRow row;
            foreach (var s in query)
            {
                row = r.NewRow();
                row["id"] = s.id;
                row["body"] = s.body;
                row["name"] = s.poet;
                r.Rows.Add(row);
            }
            ViewTable = r;
            RaisePropertyChanged("ViewTable");
        }
    }
}

バインディング用に各プロパティを作成し、ボタンとバインドしているプロパティについてはgetアクセサーからDelegateCommand()で操作を実行するメソッドを呼び出します。各ボタン用実行メソッド内でViewTableプロパティ(DataGridにデータバインドしたプロパティ)にDataTableを渡して、RaisePropertyChanged()メソッドでPropertyChangedイベントを発行します。そうすると、DataGridの表示内容が書き換わります。

Modelを作る

本当はModel用のクラスを作らないとMVVMとは名乗れないと思いますが、MainWindow.xamlのコードビハインド(MainWindow.xaml.cs)に書いてしまいました。

using System;
using System.Windows;
using System.Data;
using System.Data.Common;

namespace linq_trial
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // ViewModelのインスタンスを作成し、Viewにバインディングする
            MainWindowVM vm = new MainWindowVM();
            this.DataContext = vm;

            // データを保持するDataTableの作成
            DataTable poemTable = new DataTable();
            DataTable poetTable = new DataTable();

            // データの読み込み
            try
            {
                DbProviderFactory factory = DbProviderFactories.GetFactory("System.Data.SQLite");
                using (DbConnection connection = factory.CreateConnection())
                {
                    connection.ConnectionString = "Data Source=poemData.db"; // ファイル名を指定する
                    using (connection)
                    {
                        // コマンドを作る
                        DbCommand command = connection.CreateCommand();
                        command.CommandText = "SELECT * FROM poem";
                        command.CommandType = CommandType.Text;
                        command.Connection = connection;

                        // DataAdapterを作る
                        DbDataAdapter adapter = factory.CreateDataAdapter();
                        adapter.SelectCommand = command;

                        // 読み込み
                        adapter.Fill(poemTable);

                        // コマンドを作る
                        DbCommand command2 = connection.CreateCommand();
                        command2.CommandText = "SELECT * FROM poet_master";
                        command2.CommandType = CommandType.Text;
                        command2.Connection = connection;

                        // DataAdapterを作る
                        DbDataAdapter adapter2 = factory.CreateDataAdapter();
                        adapter2.SelectCommand = command2;

                        // 読み込み
                        adapter2.Fill(poetTable);
                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }

            // ViewModelにデータを渡す
            vm.PoemTable = poemTable;
            vm.PoetTable = poetTable;
        }
    }
}

内容はコード内のコメントの通りです。データ読み込み部分を別のクラスにしようとしたのですが、インスタンス間のデータ受け渡しがなかなかうまくいかず。精進が必要ですね。

動かしてみた

「poemテーブル」ボタンを押した状態です。

実行結果1

「poetテーブル」ボタンを押した状態です。

実行結果2

「山でフィルタ」ボタンを押した状態です。

実行結果3

「JOIN」ボタンを押した状態です。

実行結果4

期待通り動いてくれました。

まとめ

表示のためだけにDataTableを作るのはリソースの無駄というかDataViewを使うのが正しい道なのかもしれません。Linqは初めて使ってみました。慣れると便利かも。

公開日

広告