C#用SQLite3インタフェースDLL(sqlite3cs.dll)
C#で記述したプログラムからsqlite3.dllを使うためのクラスライブラリです。
http://www.vector.co.jp/soft/winnt/prog/se467674.html
|
アプリケーション
|
C#で作成したアプリケーションから、SQLite3データベース・ファイルを操作するためのインタフェース用のDLL(ラッパー)です。
実際の動作は、SQLite3本来のsqlite3.dllが行います。
http://www.sqlite.org/
の「DOWNLOAD」から、「Precompiled Binaries For Windows」「sqlitedll-3_6_6_2.zip」を使いました。
|
|
↓↑
|
|
Sqlite3cs.dll
|
|
↓↑
|
|
Sqlite3.dll
|
|
↓↑
|
|
データベース・ファイル
|
1.サンプル・プログラム
 |
メニューのそれぞれのイベント処理で、データベース操作の説明をしようとするものです。
「DB作成」で、データベースファイルと、2つのテーブルを作成します。
「データ追加」で、説明のためのデータを挿入します。
「データ表示」で、ツリービューにデータのインデクスを表示します。
|
 |
下左隅の「DB情報」をクリックすると、sqlite3.dllのバージョン、データベース・ファイルのヘッダに書かれたバージョン、テーブル定義を表示します。
|
1.1.テーブルの構成
menu と contents の、2つのテーブルを使います。
|
|
menu
|
|
contents
|
|
|
列名
|
型
|
|
列名
|
型
|
|
|
id
|
INTEGER PRIMARY KEY
|
|
id
|
INTEGER PRIMARY KEY
|
|
|
title
|
TEXT
|
|
title
|
TEXT
|
|
|
content_id
|
INT
|
|
graph
|
BLOB
|
|
|
level
|
INT
|
|
|
|
menu.content_id には、cintents.idの値を入れ、menuテーブルの行から、contentsテーブルの行を指します。
両テーブルのtitleは、テーブル名の扱いの説明のために同じ名前を付けました。行の見出しに使います。
levelは、TreeView表示の階層を表します。
graphには、ビットマップを入れ、型の説明に使います。
1.2.サンプル・データ
SQLite3のデータベースは単一ファイルです。サンプルのデータベース・ファイルは、sample1.sqlite と言う名前で作られます。場所は、指定していないのでカレントディレクトリに作られます。Visual C# のデバッグモードでサンプルを実行中なら、プロジェクトのディレクトリ以下のbin/Debug/に作られます。
10件の行を挿入します。 menu、contentsテーブルのtitleには、それぞれ「タイトル_n_M」「タイトル_n_C」の形式の文字列を入れます。
levelには、適当に数値を入れTreeViewの階層を示します。
graphには、レコード番号を64x64のビットマップで格納します。適当に背景色も変えてあります。
1.3.サンプルデータの表示
menuテーブルのtitleの文字列を、levelの値に従ってTreeViewに表示します。
TreeViewでタイトルを選ぶと、両テーブルの全てのカラムを、右のListBoxに表示します。上の図の表示でテーブル名がA.idのようになっているのは別名(アリアス)の例です。また、graphについては内容ではなく型が書かれています。これは各列を一律にrow[n].ToString()のようにして表示していることによります。
右下の矩形は、graphに格納されていたビットマップです。
1.4.サンプルプログラムの操作
メニューの「DB作成」「データ追加」「データ表示」を左から順にクリックします。それぞれ、成功しても、エラーでもダイアログが表示されます。
TreeViewからタイトルを選んで、画面右側の表示が変わるのを見てください。
やり直すには、「DB削除」をしてください。
2.プログラミング
サンプル・プログラムは、「Windowsフォームアプリケーション」です。メニューで各機能を起動するように、Form1.csにイベント処理メソッドとして書きました。
エラー処理は、例外をスローする方法にしました。
データベース・ファイルを開いたままにすると、他のプログラムがデータベースを開けないのでイベント処理ごとに、Open()-Close() します。
2.1.参照設定
sqlite3cs.dll クラスライブラリを使うには参照の追加が必要です。
- 「ソリューションエクスプローラ」の「参照設定」の上でマウスの右ボタンメニューを表示。「参照の追加」「参照」で、sqlite3cs.dllを選び、参照を追加します。
- using SQLiteDatabase;
を、加えます。
2.2.Form1のメンバ変数
- readonly string DB_FILE = "sample1.sqlite";//DBファイルのパス
- int[] ContentID; //TreeViewのノードからcontentsテーブルのIDへ
2.3.データベース・ファイルの作成とテーブルの作成
- SQLite3オブジェクトを生成します。
SQLite3 db = new SQLite3();
- データーベースファイルを作成します。
db.Create(DB_FILE);
- テーブルを作成するSQL文を実行します。
db.Execute("CREATE TABLE menu (...);");
- データベースをクローズします。
db.Close();
- private void dB作成ToolStripMenuItem_Click(object sender, EventArgs e)
- {
- SQLite3 db;
- //DBファイルを作成。既存ならエラー
- try
- {
- db = new SQLite3();
- db.Create(DB_FILE);
- }
- catch(Exception ex)
- {
- MessageBox.Show("データベースファイル作成失敗\r\n" + ex.Message);
- return;
- }
- //テーブルを作成
- try
- {
- db.Execute("CREATE TABLE menu (id INTEGER PRIMARY KEY, title text, content_id INT, level INT);");
- db.Execute("CREATE TABLE contents (id INTEGER PRIMARY KEY, title text, graph BLOB);");
- }
- catch (Exception ex)
- {
- MessageBox.Show("TABLE作成失敗\r\n" + ex.Message);
- }
- db.Close();
- MessageBox.Show("完了");
- }
db.Create()だけを行って、db.Close()すると、サイズがゼロのファイルが作られます。SQLite3のデータベース・ファイルに必要なヘッダも記録されないので、かならずテーブルを作成するときにdb.Create()をするようにします。
2.4.データー追加
- SQLite3オブジェクトを生成します。
SQLite3 db = new SQLite3();
- データーベースファイルをオープンします。
db.Open(DB_FILE);
ここでは、開けなかった場合の処理を含めたOpneDB()の中で行っています。
- 重複したデータ追加を避けるため、テーブルが空なのを確認します。
- 10件のデータをmenuテーブルとcontentsテーブルに追加します。
まず、contentsテーブルに追加して、そこで発番された id を取得し、menuテーブルのcontend_idの値に使います。
- データベースをクローズします。
db.Close();
- private void データToolStripMenuItem_Click(object sender, EventArgs e)
- {
- int[] levelSample=new int[]{0,1,2,0,1,2,0,1,2,2};
- SQLite3 db = new SQLite3();
- if (!OpenDB(db)) return;
- try
- {
- //テーブルがあり空であるか確認
- SQLite3.Statement stmt = db.Query("SELECT COUNT(*) from menu;");
- ROW row = stmt.NextRow();
- stmt.Close();//重要です
- if ((row != null) && ((int)row["COUNT(*)"] != 0))
- {
- MessageBox.Show("既にデータがある。(行数取得失敗の場合も含まれる。)");
- return;
- }
- //10行分のテストデータを追加
- db.Execute("BEGIN;");
- for (int i = 0; i < 10; i++)
- {
- string title = "タイトル_" + (i+1).ToString();
- stmt = db.Prepare("INSERT INTO contents (title, graph) VALUES (?,?);");
- stmt.Bind(1, title + "_C");
- stmt.Bind(2, CreateSampleBitmap(i));
- stmt.Execute(); //実行
- stmt.Close(); //重要です
- long lastId = db.GetLastInsertRowId();
- stmt = db.Prepare("INSERT INTO menu (title, content_id, level) VALUES (?,?,?);");
- stmt.Bind(1, title + "_M");
- stmt.Bind(2, lastId);
- stmt.Bind(3, levelSample[i]);
- stmt.Execute(); //実行
- stmt.Close(); //重要です
- }
- db.Execute("END;");
- db.Close();
- MessageBox.Show("完了");
- }
- catch (Exception ex)
- {
- db.Close();
- MessageBox.Show("データ追加失敗\r\n" + ex.Message);
- }
- }
INSERT文の実行は、Prepare()とSQLite3.Statementクラスのメソッドを使います。?を含むINSERT文に、Bind()で値をセットしてから実行します。 SQLite3.Statementのインスタンスは、確実にクローズします。クローズしないとデータベースはBUSY状態のままで他の操作ができません。
2.5.TreeViewの表示
- SQLite3オブジェクトを生成します。
SQLite3 db = new SQLite3();
- データーベースファイルをオープンします。
db.Open(DB_FILE);
ここでは、開けなかった場合の処理を含めたOpneDB()の中で行っています。
- menuテーブルの全行を選択します。
SQLite3.Statement stmt
= db.Query("SELECT title,content_id,level FROM menu ORDER BY id;");
- menuテーブルを1行ずつ読み出して、levelの値でネストしながら、TreeViewを作ります。
ROW row = stmt.NextRow();
- SQLite3.Statementオブジェクトをクローズします。
stmt.Close();
- データベースをクローズします。
db.Close();
- private void データ表示ToolStripMenuItem_Click(object sender, EventArgs e)
- {
- SQLite3 db = new SQLite3();
- if (!OpenDB(db)) return;
- SQLite3.Statement stmt = db.Query("SELECT title,content_id,level FROM menu ORDER BY id;");
- treeView1.Nodes.Clear();
- List<TreeNode> list = new List<TreeNode>();
- List<int> content_id = new List<int>();
- TreeNode top = new TreeNode("INDEX");
- int n = 1;
- while (true)
- {
- ROW row = stmt.NextRow();
- if (row == null) break;
- TreeNode node = new TreeNode((string)row["title"]);
- node.Name = n.ToString();
- content_id.Add(n);
- n++;
- int level = (int)row["level"];
- if (level == 0)
- {
- top.Nodes.Add(node);
- }
- else
- {
- int pos = level - 1;
- while (pos > 0)
- {
- if (list[pos] != null) break;
- }
- list[pos].Nodes.Add(node);
- }
- for (int i = list.Count; i <= level; i++)
- list.Add(null);
- list[level] = node;
- }
- stmt.Close();
- ContentID = content_id.ToArray();
- db.Close();
- treeView1.Nodes.Add(top);
- top.Expand();
- }
TreeNodeオブジェクトのNameプロパティに、Form1のメンバ変数ContentID[]のインデクスを格納しています。ContentID[]には、各行のcontent_idの値を入れます。TreeViewの表示で、ノードが選択されたとき、contentsテーブルから対応する1行を読み出すのに使います。
2.6.ノード(コンテンツ)の表示
TreeViewの表示で、ノードを選択が変更されたとき、対応するcontentsテーブルの行の内容を表示します。
- SQLite3オブジェクトを生成します。
SQLite3 db = new SQLite3();
- データーベースファイルをオープンします。
db.Open(DB_FILE);
ここでは、開けなかった場合の処理を含めたOpneDB()の中で行っています。
- 選択されたノードのNameプロパティの値でContentID[]を引き、contentsテーブルの行を読み出します。
SQLite3.Statement stmt = db.Prepare("SELECT ... WHERE B.id=?;");
stmt.Bind(1, content_id);
stmt.Execute(); //実行
ROW row = stmt.NextRow();
stmt.Close(); //重要
- contentsテーブルの内容を文字列でlistBox1に表示します。graphはビットマップで、右下に図形として描画します。
- データベースをクローズします。
db.Close();
- private void treeView1_AfterSelect(object sender, TreeViewEventArgs e)
- {
- int n = Convert.ToInt32(e.Node.Name);
- int content_id = ContentID[n - 1];
- SQLite3 db = new SQLite3();
- if (!OpenDB(db, true)) return;
- try
- {
- listBox1.Items.Clear();
- SQLite3.Statement stmt = db.Prepare(
- "SELECT A.*, B.* "
- + "FROM menu AS A JOIN contents AS B ON A.content_id = B.id "
- + "WHERE B.id=?;");
-
- stmt.Bind(1, content_id);
- stmt.Execute(); //実行
- ROW row = stmt.NextRow();
- if (row != null)
- {
- for (int i = 0; i < row.Length; i++)
- listBox1.Items.Add(row.names[i]+" : "+row[i].ToString());
- if (Array.IndexOf(row.names, "B.graph") >= 0)
- pictureBox1.Image = ToImage((byte[])row["B.graph"]);
- }
- stmt.Close(); //重要
- db.Close();
- }
- catch (Exception ex)
- {
- db.Close();
- MessageBox.Show("コンテンツ取得失敗\r\n" + ex.Message);
- }
- }
内容を表示するには、単にcontentsテーブルから1行読み出すだけで済みます。
この例のSQL文は、別名(アリアス)の説明(後述)に使うことを意図しています。2つのテーブルを結合し、別名を使って読み出しています。
3.sqlite3cs.dll クラスライブラリ
このクラスライブラリは、sqlite3.dllの関数群を簡便に使えるようにすることを意図しています。データベースのないサーバで、直接sqlite.soを使ったことから知ったことをまとめました。
このクラスは、ADOなど標準的なアクセス方法にはまったく準拠していません。
また、sqlite3.dllの関数を網羅しているわけでもありません。
出来ることは、サンプルプログラムで説明したことだけです。
文字コードもUTF-8で使うこと以外は考えていません。
3.1.SQLiteDatabase名前空間
sqlite3cs.dllには、SQLiteDatabase名前空間に以下のクラスがあります。
- ROWクラス
SQL文の実行の結果取得された「行」のデータを保持します。
- SQLite3クラス
バインドを使用しないSQL文を実行します。
- SQLite3.Statementクラス
バインドと「行」の読み出しの機能を行います。
3.2.ROWクラス
このクラスは、SQLite3.Statement.GetNext()メソッドの戻り値として使います。データベースから得られた「行」の値を保持します。「行」は、列名と値のペアの配列です。
■ プロパティ
- string[] names;
列名の配列。個数は、値の個数(列数)と一致します。順番は、値の順番に対応します。順番は、SELECTなどのSQL文で指定した列名の順番になります。(*で指定した場合はテーブルの定義順になるものと思います。)
- int Length;
列数。
■ 演算子
- [文字列]
列名を指定して、値を取得します。列名は、テーブル名や別名が付加されている場合があります。
- [int]
順番で値を取得します。
■ メソッド
- object[] ToArry();
値の並びの object[] を返します。
3.3.SQLite3クラス
■ プロパティ
- string LibVersion;
sqlite3.dllのバージョン番号を返します。
sqlite3.dllがロードできない場合は例外がスローされます。
- string FileVersion;
Open()が成功した場合、データベース・ファイルのヘッダから得られたバージョン番号がセットされます。現在は、"3"がセットされます。
■ コンストラクタ
- SQLite3();
SQLite3の新しいインスタンスを初期化します。
■ メソッド
- void Open(string fileName);
データベース・ファイルのパスを指定して、データベースをオープンします。
- void Open(string fileName, bool bFullColumnNames);
データベース・ファイルのパスを指定して、データベースをオープンします。
bFullColumnNamesは、SELECTなどのSQL文を実行して「行」をROWオブジェクトを取得した場合に取得される、列名の形式を指定します。
falseの場合は、列名が使われ、trueの場合は、テーブル名.列名が使われます。テーブル名は、別名(アリアス)の場合があります。
※ この形式のOpen()を指定した場合は、true/falseいずれでも設定を行います。単にパスのみを指定したOpen()は、設定を行いません。デフォルトの動作になります。デフォルトは「列名のみ」です。このデフォルトを変える方法もあるのかもしれません。
- void Close();
データベース・ファイルをクローズします。Open()からクローズの間は、他のスレッド、プロセスはデータベース・ファイルにアクセスできません。
- void Execute(string sql);
SELECT以外のSQL文を実行します。実行の結果「行」が返されないSQL文の実行に使います。
「行」が返されないSQL文でも、バインド機能を使う場合は、SQLite3.Statement.Prepare()を使います。
- Statement Query(string sql);
SELECT文を実行します。戻り値のStatementオブジェクトで「行」を読み取ります。
SELECT文でも、バインド機能を使う場合は、SQLite3.Statement.Prepare()を使います。
- Statement Prepare(string template);
?マークを含むSQL文の実行に使います。実行するSQL文は文字列として用意しますが、実行ごとに変わる値を文字列にして再構成するのは手数が掛かります。可変になる箇所を?と記述しておき、Statement.Bind()でバインドして実行できます。
- long GetLastInsertRowId();
最後に発番されたauto incrementキーの値を取得します。これは、DLLの機能そのもので、詳細は良く分かりません。実用上は問題ありません。
- int GetChanges();
最後に実行したSQL文で更新された行数。これは、DLLの機能そのもので、詳細は良く分かりません。
3.4.SQLite3.Statementクラス
このクラスは、2つの目的で使います。1つは、Query()の結果の「行」を読み出します。もう一つは、SQL文のバインド機能を行います。
■ コンストラクタ
このクラスは、SQLite3.Query()かSQLite3.Prepare()で生成され、アプリケーションでコンストラクトすることはありません。
■ メソッド
Close();
これを忘れると以降の操作でデータベースがBUSYだと言われる。
ROW NextRow();
SQL文の実行の結果取得された「行」を読み出す。行がないときはnullを返す。終了はnullで判定すること。最初に行数を知る方法はない。
1.void ClearBindings();
バインドした状態を開放します。
2.void Reset();
同じStatementオブジェクトを繰り返し使用する場合、前回のSQL文の実行を完了し再実行可能にします。バインドしたデータは保持されます。
n番目のパラメータに値をバインドします。
void BindNull(int n)
void Bind(int n, int value)
void Bind(int n, long value)
void Bind(int n, double value)
void Bind(int n, byte[] value)
void Bind(int n, string value)
void Bind(object[] values);
values[]の要素の型に応じて順次バインドを行います。
int,long,double,string,byte[]が可能です。
4.参考になれば
4.1.SQLite3の型について
SQLite3のもともとの考え方は、入れた通りに読み出すことで、型による処理をしないことが始まりのようです。「入れた通りに読み出すこと」は、プログラミング言語やOS、文字コードの影響を受けると言うことです。また、入れたときの条件を知らないと読み出せないことも意味します。
テーブルを作成する際の列の型定は、単に指定した文字列が格納されるだけです。何でも入ります。また、まったく指定しなくてもエラーになりません。
ただし、現在ではまったく型が使われないわけではなく、一部使われています。
- INTEGER PRIMARY KEY と指定された列は、auto increment処理される。
- int型を指定した列を、ダブルクオートで囲んだ数字で検索するとミスマッチになる。
DLLには sqlite3_column_type() 関数があり、内部的な型があることが分かります。この型はテーブル作成時に指定したことによって決まっているわけではないことに注意が必要です。
これらのことから、以下のように型を扱うことにしました。
- アプリケーションは、各列の型を知っていると言うことを前提とする。
データベースから読み出した列データは、C#の型に変換した上で、object型で渡す。
使う側で、(int)row["id"]、(string)row["title"]のように使う。
- テーブル作成時には、以下の型のうちどれかを必ず指定する。(後々のため)
INT、LONG、DOUBLE、TEXT、BLOB、INTEGER PRIMARY KEY
4.2.並列処理
サーバー・クライアントではないので、プロセス間での並列処理のためには、データベース・ファイルを同時に複数のプロセス間で開ける必要がありますが、これはできません。
スレッドでも同様です。
また、複数のSQL文も並列に処理できません。あるSELECTを実行して結果の行を順次読み出しながら、その値を使って別のSQL文を実行すると言ったこともできません。最初のSELECTの読み出しに使うsqlite3_stmtオブジェクトが閉じられるまでは、他の要求がBUSYでエラーになります。
この件は、何か別な方法があるのかもしれませんが、わかりません。
4.3.テーブルの別名(アリアス)
このホームページは、CakePHPが使われていますが、CakePHPはプログラム上のラベルとテーブルの列名の関係を自動的に行います。このネーミングのルールでは、列名がテーブル名を伴う必要があります。また、テーブル名は別名になっています。
これを sqlite3.so で行いました。そのとき分かったのは、
- PHPインタフェースは連想記憶型の配列を使っており、同名の列は1つになってしまう。
- テーブル名.列名の名前は作れるが、別名は不可。
今回、sqlite3.dllを使ったsqlite3cs.dllでは以下のようになりました。
- C#の配列で、インデクスでも名前でも引けるようにしたので、同名の列名があっても列数が減ってしまうようなことはありません。
- バージョンの差か、別名も使えます。
■ テーブル名.列名
列名を「テーブル名.列名」と表すのをフル、「列名のみ」をショートと呼んでいます。
この切り替えは、SQLite3.Open()で指定します。Open("データベース・ファイル",true)、または Open("データベース・ファイル",false)とし、trueならフル形式になります。このオプションは、SELECTで行を読み出したとき、ROWオブジェクトに格納される列名がどちらの形式になるかを指定するものです。それ以外は、両形式が有効です。
サンプル・プログラムで使用したmenuテーブルを例でtitle列(フィールド)の値を参照する場合、フル形式の場合は、row["menu.title"]となり、ショート形式の場合は、row["title"]とすることになります。
■ テーブルの結合
複数のテーブルがあると同じ列名(フィールド名)が使われていることがあります。サンプル・プログラムの例では、2つのテーブルでidとtitleが重複しています。複数のテーブルを同時に扱うときには、区別するためにフル形式の名前を使います。
下記のSQL文のを実行するときに、Open("データベース・ファイル",true)としてオープンしていれば、読み出される「行」の列名もフル形式となって列名で区別できます。
ただし、フル形式でなくても、列をインデクスで参照すれば区別できるので、Open("データベース・ファイル",true)でオープンすることは必須のことではありません。
- SELECT menu.*, contents.*
- FROM menu JOIN contents ON menu.content_id = contents.id
- WHERE contents.id = 1;
■ テーブル名の別名(アリアス)
下記は、上のSQL文と同じ検索をテーブル名の別名を使って書いたものです。
下記のSQL文のを実行するときに、Open("データベース・ファイル",true)としてオープンしていれば、読み出される「行」の列名が「別名.列名」となります。
menuテーブルのtitle列を参照するには、row["A.title"]とすることになります。
- SELECT A.*, B.*
- FROM menu AS A JOIN contents AS B ON A.content_id = B.id
- WHERE B.id = 1;
|