NoSQLと Google firestore
データベースシステムとして、最近は NoSQL (Not Only SQL) が注目されている。この中で、広く使われている物として、Google FireStore などが有名である。教科書以外の最近のデータベースの動向ということで、最後に NoSQL の説明を行う。
リレーショナルデータベースシステムの問題
リレーショナルデータベースのシステムでは、大量の問い合わせに対応する場合、データのマスターとなるプライマリサーバに、そのデータの複製を持つ複数のセカンダリサーバを接続させる方式がとられる。しかしながら、この方式ではセカンダリサーバへのデータ更新を速やかにプライマリサーバに反映させる、さらにその結果が他のセカンダリサーバに反映させる必要があることから、大量のデータに大量の問い合わせがあるようなシステムでは、これらのデータ同期の性能が求められる。しかも、プライマリサーバが故障した場合の復旧なども考えると、こういったプライマリ・セカンダリ・サーバ構成での運用・管理は大変である。
NoSQLの利点
NoSQLのデータベースでは、すべてのデータを複数のサーバ(別のデバイス,ネットワーク)に冗長化したうえで分散して保存する。この際に、どのサーバがプライマリサーバといった概念はない。もし1つのサーバが故障したとしても、分散して保存されたデータから元のデータを自動的に修復できるような構造となっている。
データの分散保存であれば、ハードディスクの RAID システムなども関連知識となるであろう。
- RAID0 – ストライピング(データを別デバイスに分散保存し、並行読み出しで高速化)
- RAID1 – ミラーリング(データを複数デバイスに同じものを書き込んで、データ故障耐性を実現)
- RAID5 – データを複数デバイスに分散保存する際に、データ誤り訂正のデータも分散保存し、高速化と故障耐性を実現
- RAID6 – データ誤り補正のデータを複数もたせて、分散保存
リレーショナルデータベースで大量のユーザからアクセスされる場合、データが安全に取り扱うことができたり、システムに障害が発生した時の対応や、システムのスケーラビリティ(利用状況に応じて処理するプロセッサなどを増やしたり減らしたりする機能)が重要となる。NoSQLのシステムでは、中心となるプライマリサーバを作るのではなく、データを複数のシステムに分散して保存し、障害が発生しても、分散したデータから自動的にデータを修復できるような構成とする。
NoSQLのシステムでは、データ格納形式から、キーバリューストア型、カラムストア型、ドキュメントデータベース、グラフデータベースに分類される。最も代表的なものは、保存するデータ(Value)に対し検索するためのキー(Key)だけの基本的なデータ検索だけを提供する キーバリューストア(Key-Value store)である。こういった構成ではSQLとは違い、複数のテーブルをまたがった検索などができない(サブコレクションなどを使えば代用可能)。
Google の Firestore
NoSQLのデータベースを構築したのは、Google が先駆けであった。現在、このGoogle の NoSQL のシステムは、Firestore として利用されている。(データベースはFireBase)
Firestore は、ドキュメントモデルデータベースの一種であり、すべてのデータはドキュメントとコレクションに保存される。ドキュメントは、データベースでのレコードに相当するが、属性名とそれに対応したデータの JSON オブジェクトである。コレクションは、キーにより対応するドキュメントを取り出せるデータ群である。ドキュメントの中に、サブコレクションを保存することもできる。
情報構造論とオブジェクト指向
データ構造を扱うプログラムの書き方を説明してきたが、その考え方をプログラムにするためには手間もかかる。こういった手間を少しでも減らすために、プログラム言語が支援してくれる。その代表格がオブジェクト指向プログラミング(Object Oriented Programming:略称OOP)であり、以下にその基本を説明する。
データ指向のプログラム記述
名前と年齢のデータを扱うプログラムをC言語で書く時、私なら以下のようなプログラムを作成する。
このプログラムの書き方では、saitohというデータにset_NameAge() , print_NameAge() を呼び出していて、データに対して処理を加えるという雰囲気がでている。(C言語なのでデータに処理を施す関数には、必ずどのデータに対する処理なのかを与えるポインタがある。) このようにプログラムを書くと、saitoh というデータに対して命令するイメージとなり、擬人化したデータに向かってset,printしろ…って命令しているように見える。
// 名前と年齢の構造体 struct NameAge { char name[ 20 ] ; int age ; } ; // NameAgeを初期化する関数 void set_NameAge( struct NameAge* p , char s[] , int a ) { strcpy( p->name , s ) ; p->age = a ; } // NameAgeを表示する関数 void print_NameAge( struct NameAge* p ) { printf( "%s %d¥n" , p->name , p->age ) ; } void main() { struct NameAge saitoh ; set_NameAge( &saitoh, "t-saitoh" , 53 ) ; print_NameAge( &saitoh ) ; // NameAge の中身を知らなくても、 // set_NameAge(),print_NameAge() の中身を見なくても、 // saitoh を set して print する....という雰囲気は伝わるよね!! }
このプログラムでは、例えば、データに誕生日も覚えたいという改良を加えるとしても、main の前のデータ構造と関数の部分は色々と書き換えることになるだろうけど、main の内部はあまり変わらないだろう。こういう書き方をすればプログラムを作成するときには、データ構造とそれを扱う関数を記述する人と、データ構造を使う人(main内部を書く人)と、分業ができるようになる。
隠蔽化
このような記述では、データ構造の中身を知らなくても、main で、setしてprintして…という処理の雰囲気は分かる。さらに、set_NameAge()とか、print_NameAge() の処理の中身を知らなくても、設定するとか表示するとか…は予想できる。
これは、NameAge というデータをブラックボックス化(隠蔽化)して捉えていると見れる。データ構造の中身を知らなくてもプログラムを理解できることは、データ構造の隠蔽化という。また、関数の中身を知らなくても理解できることは、手続きの隠蔽化という。
オブジェクト指向プログラミング
前述のように、プログラムを書く時には、データ構造とそのデータを扱う関数を一緒に開発する方が分かり易い。そこで、プログラム言語の文法自体を、データ構造とその関数(メソッドと呼ぶ)をまとめてクラスとして扱うプログラムスタイルが、オブジェクト指向プログラミングの基本である。
class NameAge { private: // データ構造の宣言 char name[ 20 ] ; int age ; public: // メソッドの定義 void set( char s[] , int a ) { // 初期化関数 strcpy( name , s ) ; // どのデータに対する処理かは省略できるので、 age = a ; // データへのポインタ引数は不要。 } void print() { // 表示関数 printf( "%s %d¥n" , name , age ) ; } } ; void main() { NameAge saitoh ; saitoh.set( "t-saitoh" , 53 ) ; // set,printはpublicなので自由に使える。 saitoh.print() ; // saitoh.age = 54 ; エラー:クラス外でprivateの要素は触れない。 }
このプログラムでは、saitoh というデータ(具体的なデータが割り当てられたものはオブジェクトと呼ぶ)に対して、set() , print() のメソッドを呼び出している。
# C++ではクラス毎に関数名を区別してくれるので、関数名もシンプルにset,printのようにかける。
オブジェクト指向では、データに対して private を指定すると、クラス以外でその要素やメソッドを扱うことができなくなる。一方 public が指定されたものは、クラス外で使っていい。これにより、クラスを設計する人と、クラスを使う人を明確に分けることができ、クラスを使う人が、クラス内部の変数を勝手に触ることを禁止できる。
プログラムを記述する時には、データ件数を数える時に、カウンタの初期化を忘れて動かないといった、初期化忘れも問題となる。オブジェクト指向のプログラム言語では、こういうミスを減らすために、データ初期化専用の関数(コンストラクタ)を定義することで、初期化忘れを防ぐことができる。
// コンストラクタを使う例 class NameAge { // 略 public: NameAge( char s[] , int a ) { // データ初期化専用の関数 strcpy( name , s ) ; // コンストラクタと呼ぶ age = a ; } // 略 } ; void main() { NameAge saitoh( "t-saitoh" , 53 ) ; // オブジェクトの宣言と初期化をまとめて記述できる。 saitoh.print() ; }
プログラムにオブジェクト指向を取り入れると、クラスを利用する人とクラスを記述する人で分業ができ、クラスを記述する人は、クラスを利用するプログラマーに迷惑をかけずにプログラムを修正できる。
この結果、クラスを記述する人はプログラムを常により良い状態に書き換えることができるようになる。このように、よりよく改善を常に行うことはリファクタリングと呼ばれ、オブジェクト指向を取り入れる大きな原動力となる。。
最近のC++なら
最近のオブジェクト指向プログラミングは、テンプレート機能と組み合わせると、単純リスト処理が以下のように書けてしまう。struct 宣言やmalloc()なんて出てこない。(^_^;
#include <iostream> #include <forward_list> #include <algorithm> int main() { // std::forward_list<>線形リスト std::forward_list<int> lst{ 1 , 2 , 3 } ; // 1,2,3の要素のリストで初期化 // リスト先頭に 0 を挿入 lst.push_front( 0 ) ; // 以下のような処理を最新のC++なら... // * もともとのC言語なら以下のように書くだろう。 // for( struct List*p = top ; p != NULL ; p = p->next ) // printf( "%d¥n" , p->data ) ; // * 通常の反復子iteratorを使って書いてみる。 // auto は、lst の型推論。 // ちょっと前のC++なら型推論がないので、 // std::forward_list<int>::iterator itr = lst.begin() と書く。 // * C++では演算子の処理をクラス毎に書き換えることができる。 // itr++ といっても、カウントアップ処理をする訳ではない。 for( auto itr = lst.begin() ; itr != lst.end() ; itr++ ) { std::cout << *itr << std::endl ; } // 同じ処理を algorithm を使って書く。 std::for_each( lst.begin() , lst.end() , []( int x ) { // 配列参照のコールバック関数 std::cout << x << std::endl ; } ); // 特に書かなくてもデストラクタがlstを捨ててくれる。 return 0 ; }
テンプレート機能
テンプレート機能は、実際のデータを覚える部分の型を後で指定できるようにしたデータ構造を定義する機能。
template <class > struct List { T data ; struct List* next ; } ; int main() { List<int> li ; // 整数を要素とするList型の宣言 List<double> ld ; // 実数を要素とするList型の宣言 }
関数ポインタ
関数ポインタとコールバック関数
JavaScript のプログラムで、以下のようなコーディングがよく使われる。このプログラムでは、3と4を加えた結果が出てくるが、関数の引数の中に関数宣言で使われるfunctionキーワードが出てきているが、この意味を正しく理解しているだろうか?
このような (function()…)は、無名関数と呼ばれている。このような機能は、C言語では関数ポインタと呼ばれたり、新しいプログラム言語では一般的にラムダ式などと呼ばれる。
// JavaScriptの無名関数の例 3+4=7 を表示 console.log( (function( x , y ) { return x + y ; })( 3 , 4 ) ) ; // 無名関数 console.log( ((x,y) => { return x + y ; })( 3 , 4 ) ) ; // アロー関数
C言語の関数ポインタの仕組みを理解するために、以下のプログラムを示す。
int add( int x , int y ) { return x + y ; } int mul( int x , int y ) { return x * y ; } void main() { int (*f)( int , int ) ; // fは2つのintを引数とする関数へのポインタ f = add ; // f = add( ... ) ; ではないことに注意 printf( "%d¥n" , (*f)( 3 , 4 ) ) ; // 3+4=7 // f( 3 , 4 ) と書いてもいい f = mul ; printf( "%d¥n" , (*f)( 3 , 4 ) ) ; // 3*4=12 }
このプログラムでは、関数ポインタの変数 f を定義している。「 int (*f)( int , int ) ; 」 は、int型の引数を2つ持つ、返り値がint型の関数へのポインタであり、「 f = add ; 」では、f に加算する関数を覚えている。add に実引数を渡す()がないことに注目。
そして、「 (*f)( 3 , 4 ) ; 」により、実引数を3,4にて f の指し示す add を呼び出し、7 が答えとして求まる。
こういう、関数に、「自分で作った関数ポインタ」を渡し、その相手側の関数の中で自分で作った関数を呼び出してもらうテクニックは、コールバックとも呼ばれる。コールバック関数を使うC言語の関数で分かり易い物は、クイックソートを行う qsort() 関数だろう。qsort 関数は、引数にデータを比較するための関数を渡すことで、様々な型のデータの並び替えができる。
#include <stdio.h> #include <stdlib.h> // 整数を比較するコールバック関数 int cmp_int( int* a , int* b ) { return *a - *b ; } // 実数を比較するコールバック関数 int cmp_double( double* a , double* b ) { double ans = *a - *b ; if ( ans == 0.0 ) return 0 ; else if ( ans > 0.0 ) return 1 ; else return -1 ; } // ソート対象の配列 int array_int[ 5 ] = { 123 , 23 , 45 , 11 , 53 } ; double array_double[ 4 ] = { 1.23 , 12.3 , 32.1 , 3.21 } ; void main() { // 整数配列をソート qsort( array_int , 5 , sizeof( int ) , (int(*)(const void*,const void*))cmp_int ) ; for( int i = 0 ; i < 5 ; i++ ) printf( "%d\n" , array_int[ i ] ) ; // 実数配列をソート qsort( array_double , 4 , sizeof( double ) , (int(*)(const void*,const void*))cmp_double ) ; for( int i = 0 ; i < 5 ; i++ ) printf( "%f\n" , array_double[ i ] ) ; }
無名関数
コールバック関数を使っていると、データを比較するだけの関数とか簡単な短い処理が使われることが多い。こういった処理を実際に使われる処理と離れた別の場所に記述すると、プログラムが読みづらくなる。この場合には、その場で関数の名前を持たない関数(無名関数)を使用する。(最近のC++の文法なのでテストには出さない)
void main() { int (*f)( int , int ) ; // fは2つのintを引数とする関数へのポインタ f = []( int x , int y ) { return x + y } ; // add を無名関数化 printf( "%d¥n" , (*f)( 3 , 4 ) ) ; // 3+4=7 // mul を無名関数にしてすぐに呼び出す3*4=12 printf( "%d¥n" , []( int x , int y ) { return x * y ; }( 3 , 4 ) ) ; // メモ:C++11では、ラムダ式=関数オブジェクト // C++14以降は、変数キャプチャなどの機能が追加されている。 }
演習(ハッシュ法)
ハッシュ法のプログラム(オープンアドレス法もしくはチェイン法)を用いて、
(1)名前と電話番号,(2)名前と住所,(3)名前と誕生日について、名前をキーとして検索するプログラムを作成せよ。
原則として「出席番号 % 3 + 1」の番号のテーマに取り組むこと。
レポートを作成する際には、ハッシュ関数を変更してどういった変化があるか確認せよ。
ハッシュサイズは、10〜20件程度で良い。