QueryExecuter
Model
クラスはQueryExecuter
クラスを継承しています。(マジックメソッドを用いた委譲を行っています。)
よいModelを作るために、QueryExecuter
について学びましょう。
目次
- QueryExecuterの生成
- 先頭レコードの取得
- 全レコードを取得
- ユニークキーでのレコードの取得
- フィールドの選択
- クエリの条件式
- クエリのパラメータを確認する
- 大量のレコードを扱う
- Join
- Table Join
- Recordset Join
- GroupBy
- OrderBy
- MatchBy
- Union
- Calculate
- スナップショット
- データベースアクセスのトリガー
- 書き込みオペレーション
- トランザクションとロック
- 例外
- レコードをPHPの配列で取得する
- Transcatdのネイティブオブジェクトを取得する
QueryExecuterの生成
customers
テーブルのQueryExecuter
を生成する
$qe = DB::queryExecuter('customers');
Note: 戻り値のオブジェクトはテーブルごとにシングルトンなオブジェクトです。
先頭レコードの取得
指定されたインデックスの順にみて、先頭のレコードを取得します。インデックスを指定しなかった場合はprimary keyが使用されます。
primary keyを使用し、例外を発生させない
$customer = $qe->first();
$id = $customer->id; // $customerはstdClassオブジェクト
$throwException = true
を指定した場合、レコードが見つからなかったときに例外を発生させます。
インデックス番号1を使用し、見つからなかった場合に例外を発生させる
try {
$customer = $qe->index(1)->first(true);
} catch(\Exception $e) {
// (省略)
}
全レコードを取得
customer
テーブルの全レコードを取得
$customers = $qe->all(); // $customersはstdClassオブジェクトのCollection
複数のオブジェクトが返るメソッドは、Collection
オブジェクトを返します。
ユニークキーでのレコードの取得
find
、first
メソッドはユニークなインデックスを使って高速な検索を行います。インデックス番号を指定しなかった場合は、primary keyが使用されます。メソッドのパラメータにはキーの値を指定します。
primary keyでid = 3
のレコードを取得
$customer = $qe->find(3);
インデックス番号1を指定しname = 'John'
のレコードを取得
$nameKeyNum = 1;
$john = $qe->index($nameKeyNum)->find('John');
マルチセグメントキーを使用しレコードを取得
$multiSegKeyNum = 2;
$customer = $qe->index($multiSegKeyNum)->find([3, '1567-900-00']);
複数のレコードを一度に取得
primary keyでid = 3
、id = 4
、id = 6
のレコードを取得します。
$customers = $qe->findMany([3, 4, 6]);
マルチセグメントキーを使用し複数のレコードを一度に取得
$customers = $qe->index(2)->findMany([[3, '1567-900-00'],[9, '1467-700-01']]);
フィールドの選択
それぞれをパラメータで指定
$customers = $qe->select('id', 'name', 'note')->all();
配列で指定
$customers = $qe->select(['id', 'name', 'note'])->all();
フィールドを選択しなかった場合は、すべてのフィールドが読み取られます。
クエリの条件式
条件式のメソッドはメソッドチェーンできます。最後にget
またはall
メソッドを呼び出して実行します。実行結果は常にCollection
オブジェクトです。
条件式の基礎
これから説明する条件式は、SQLによく似ていますがSQLではありません。はじめにTransactdならではの点について説明します。
- インデックスの指定が必要
- 検索を開始するインデックスフィールドの値の指定が必要
- 検索を中止する条件として、検索にマッチしないレコード数(リジェクトカウント)の指定が必要
インデックスの指定
index
メソッドでインデックスの番号を指定します。省略した場合は自動でprimary key
が使用されます。
$groupKeyNum = 1;
$qe->index($groupKeyNum);
インデックスフィールドの値の指定
インデックスフィールドの値はkeyValue
メソッドで指定します。keyValue
のパラメータは8個まで指定可能で、マルチセグメントのキーにも対応します。
レコードの検索はこの値で指定したレコードから開始されます。
$qe->keyValue(1);
この値で指定したレコードが無い場合は、その値の直後のレコードが開始レコードになります。
リジェクトカウントの指定
条件式にindex
で指定したインデックスフィールドが含まれ、それに続く条件がor
でない場合は、インデックスフィールドの値が条件を満たさないレコードが見つかると、自動で検索を終了します。
また、条件にマッチしないレコード数がリジェクトカウントに達した場合も、検索が中止されます。リジェクトカウントを指定しないとデフォルトは1
です。
以下の例は、name
が'akio'
でないレコードが10レコードに達すると検索を中止し、見つかったレコードを返します。
$customers = $qe->index(1)->keyValue('')->where('name', 'akio')->reject(10);
リジェクトカウントによる検索中止を無効にする場合は、noBreakReject
メソッドを使用します。noBreakReject
はreject(0xffff)
の別名で、リジェクトを無効にするマジックナンバー0xffff
をセットします。
Where
primary keyを使用し、name = 'John'
のレコードを取得
$customers = $qe->keyValue(0)->where('name', 'John')->noBreakReject()->get();
keyValue(0)
で、primary keyの値0
が検索の開始位置として使用されます。noBreakReject
を指定すると、マッチしないレコードの数によって検索を中止しません。
group >= 3 AND group < 5
のレコードを取得
$groupKeyNum = 1;
$customers = $qe->index($groupKeyNum)->keyValue(3)
->where('group', '>=', 3)
->where('group', '<', 5)->reject(1)->get();
- キーの値
3
が検索の開始位置として使用されます。 reject(1)
を指定すると、マッチしないレコードが1つ見つかった時点で検索を中止します。
WhereNull
note = null
のレコードを取得
$customers = $qe->keyValue(0)->whereNull('note')->noBreakReject()->get();
WhereNotNull
note != null
のレコードを取得
$customers = $qe->keyValue(0)->whereNotNull('note')->noBreakReject()->get();
OrWhere
primary keyを使用し、name = 'John'
またはname = 'Akio'
のレコードを取得
$customers = $qe->keyValue(0)->where('name', 'John')
->orWhere('name', 'Akio')
->noBreakReject()->get();
OrNull
primary keyを使用し、name = 'John'
またはname = 'Akio'
またはnote = null
のレコードを取得
$customers = $qe->keyValue(0)->where('name', 'John')
->orWhere('name', 'Akio')
->orNull('note')
->noBreakReject()->get();
OrNotNull
primary keyを使用し、group = 3
またはnote != null
のレコードを取得
$customers = $qe->keyValue(0)->where('group', 3)
->orNotNull('note')
->noBreakReject()->get();
WhereColumn
xxColumn
はフィールド同士の値を比較します。
note = comment
のレコードを取得
$customers = $qe->keyValue(0)->whereColumn('note', '=', 'comment')->noBreakReject()->get();
OrColumn
name = 'John'
またはnote = comment
のレコードを取得
$customers = $qe->keyValue(0)
->where('name', 'John')
->orColumn('note', '=', 'comment')->noBreakReject()->get();
WhereIn
この条件は内部的にwhere
もしくはorWhere
に変換されます。従って、keyValue
とreject
を指定する必要があります。
primary keyを使用し、id = 5
のレコードを取得
$customers = $qe->keyValue(5)->whereIn('id', [5])->reject(1)->get();
primary keyを使用し、id in 5,7,10,11
のレコードを取得
$customers = $qe->keyValue(5)->whereIn('id', [5,7,10,11])->reject(10)->get();
WhereNotIn
この条件は内部的にwhere
もしくはorWhere
に変換されます。従って、keyValue
とreject
を指定する必要があります。
primary keyを使用し、id not in 5,7,10,11
のレコードを取得
$customers = $qe->keyValue(5)->whereNotIn('id', [5,7,10,11])->noBreakReject()->get();
WhereBetween
primary keyを使用し、id = 5 to 11
のレコードを取得
$customer = $qe->keyValue(5)->whereBetween('id', [5,11])->get();
条件のフィールドがキーフィールドだった場合、reject
は必要ありません。条件のフィールドがキーフィールドではない場合は、noBreakReject()
が必要になります。
WhereNotBetween
primary keyを使用し、not (id = 5 to 11)
のレコードを取得
$customers = $qe->keyValue(0)->whereNotBetween('id', [5,11])->noBreakReject()->get();
WhereInKey
インデックスを使用してレコードを取得します。
keyValue
とreject
は必要ありません。- ユニークキー以外のキーを使用することができます。
- マルチセグメントキーの下位セグメントの値を省略することもできます。
$values
には1次元配列を渡します。
groupキーを使用しgroup = 2
のレコードを取得
$groupKeynum = 1; // groupキー(ユニークではないキー)のインデックス
$customers = $qe->index($groupKeynum)->whereInKey([2])->get();
マルチセグメントキーの一部を使用しレコードを取得
以下の例では先頭セグメントが3
と4
のレコードを取得しています。
$index = 2; // セグメント数2のユニークキーのインデックス
$segments = 1; // レコード取得に使用するセグメントの数
$customers = $qe->index(2)->whereInKey([3, 4], $segments)->get();
Skip
skip
はサーバーからのデータ取得後にローカルでレコードを除外します。そのため、大きな値を指定することは推奨されません。
最初の10レコードを除外
$customers = $qe->skip(10)->all();
Take
最初の10レコードだけを取得
$customers = $qe->take(10)->all();
21番目から10レコードだけを取得
$customers = $qe->skip(20)->take(10)->all();
フィールド名のエイリアス
フィールドに別名を指定した場合、クエリ内ではすべて別名を使用してください。
$qg = DB::queryExecuter('groups');
$qg->setAliases(['name' => 'groupName']);
クエリのパラメータを確認する
get()
、chunk()
、cursor()
、update()
、delete()
の代わりにqueryDescription()
を使用し、結果を表示します。
queryDescription
を取得
$params = $qe->keyValue(0)
->where('name', 'John')
->orColumn('note', '=', 'comment')
->noBreakReject()
->queryDescription();
echo $params;
結果の例
tablename : customers
key read : index = 0: id = 0
key write : index = 0: id =
conditions : name >= 'John' or note = [comment],
reject = 655361, limit = 0, stopAtLimit = 0
大量のレコードを扱う
Chunk
結果に大量のレコードが含まれる場合は、サーバーへのアクセスを分割することで、メモリを節約することができます。
一度に100行ずつ取得し、合計を計算する
$amount = 0;
$qe->keyValue(0)->where('id' ,'>=', 50)->chunk(100,
function($records) use (&$amount) {
foreach($records as $record) {
$amount += $record->amount;
}
return true; // falseを返した場合、処理が中断される
});
Cursor
結果に大量のレコードが含まれる場合は、カーソルを使用することで、メモリを節約することができます。このメソッドで得られるカーソルは読み取り専用のクライアントカーソルです。
カーソルを使用し合計を計算する
$cr = $qe->keyValue(0)->where('id' ,'>=', 50)->cursor();
$amount = 0;
foreach ($cr as $row) {
$amount += $row->amount;
}
Join
TransactdのJoinはすべてクライアント側で行われます。異なるデータべースやサーバー間のデータであってもJoinすることができます。
Joinには大きく分けて2種類あります。
- 結果セットの結合値をキーに、テーブルからデータを読んで結合する方法。
recordset - table 結合
- 2つの結果セットを結合する方法。
recordset - recordset 結合
それぞれ、Table Join
、Recordset Join
と呼びます。
QueryExecuterでは、右側のtable
またはrecordset
をjoin
メソッドで事前にセットし、読み取りのトリガーとなるメソッドで左側のrecordset
を取得したあと、Joinを行います。
Table Join
Table Join
は、結合先のテーブルのインデックスを使って高速に処理できます。この方法を使用するには以下の制約があります。
- テーブルの結合フィールドは、インデックスフィールドでなければならない。(複数セグメントキーの場合、下位フィールドは対象外にできる)
- 検索のインデックスには前項のインデックスを使用する。
- 結合条件は
=
のみ使用可能。
テーブルとその関連が正しく設計されていれば、ほとんどのJoinはこの方法で行うことができます。
Table InnerJoin
Table InnerJoin
では、通常のクエリーの条件に加えて以下の2つを指定します。
- Joinするテーブルの
QueryExecuter
(recordset - table
のtable
側) - 結合に使用するフィールド名
JoinするQueryExecuter
はindex
とselect
のみ指定します。get
の呼び出しは行いません。
グループIDを使用しグループ名をJoinする
// recordset - table のtable側
$qg = DB::queryExecuter('groups');
$qg->setAliases(['name' => 'groupName'])
->select('groupName');
->index(0);
// recordset - table の recordset側
$qe = DB::queryExecuter('customers');
$customers = $qe->keyValue(1)
->select('id', 'group')
->where('id', '>=', 1)
->where('id', '<=', 10)
->join($qg, 'group')
->get();
結果
+------+-------+-----------+
| id | group | groupName |
+------+-------+-----------+
| 1 | 1 | Users |
| 2 | 2 | Guests |
| 3 | 2 | Guests |
| 4 | 3 | Unknowns |
| 7 | 2 | Guests |
| 8 | 3 | Unknowns |
| 9 | 1 | Users |
| 10 | 1 | Users |
+------+-------+-----------+
上記の例では、customers
テーブルからid = 1-10
のレコードを取得し、group
列をキーに $qg
でgroups
テーブルのレコードを取得して結合します。キーとなる列名は、複数を配列で指定するも可能です。
Table OuterJoin
Table OuterJoin
は、結合する行がない場合にも元の行を削除しないという点以外は、Table InnerJoinと同じです。 join
メソッドをouterJoin
に変えて呼び出します。
結合する行がない場合、そのフィールドの値はnull
になります。
グループIDを使用しグループ名をOuterJoinする
$qg = DB::queryExecuter('groups');
$qg->setAliases(['name' => 'groupName'])
->select('groupName');
->index(0);
$qe = DB::queryExecuter('customers');
$customers = $qe->keyValue(1)
->select('id', 'group')
->where('id', '>=', 1)
->where('id', '<=', 10)
->outerJoin($qg, 'group')
->get();
結果
+------+-------+-----------+
| id | group | groupName |
+------+-------+-----------+
| 1 | 1 | Users |
| 2 | 2 | Guests |
| 3 | 2 | Guests |
| 4 | 3 | Unknowns |
| 5 | NULL | NULL |
| 6 | NULL | NULL |
| 7 | 2 | Guests |
| 8 | 3 | Unknowns |
| 9 | 1 | Users |
| 10 | 1 | Users |
+------+-------+-----------+
Recordset Join
Recordset Join
はTable Joinに比べてほとんど制約がなく、自由自在に2つのrecordset
をJoinできます。結合条件には、列と列の関係のみ指定可能です。
Recordset InnerJoin
Recordset Joinでは、事前にJoinするデータをrecordset
型で取得しておきます。レコードセットの取得にはget
でなくrecordset
メソッドを使用します。
すべてのgroupレコードをrecordset
型で取得する
$qg = DB::queryExecuter('groups');
$group_rs = $qg->setAliases(['id' => 'group_id', 'name' => 'groupName'])
->select('group_id', 'groupName')
->index(0)
->recordset(); // すべてのgroupレコード
idが1~10のcustomerと$group_rs
をcustomer.group = groups.group_id
でJoinする
$rq = new RecordsetQuery();
$rq->when('group', '=', 'group_id')
$customers = $qe->keyValue(1)
->select('id', 'group')
->where('id', '>=', 1)
->where('id', '<=', 10)
->join($group_rs, $rq)
->get();
結果
+------+-------+----------+-----------+
| id | group | group_id | groupName |
+------+-------+----------+-----------+
| 1 | 1 | 1 | Users |
| 2 | 2 | 2 | Guests |
| 3 | 2 | 2 | Guests |
| 4 | 3 | 3 | Unknowns |
| 7 | 2 | 2 | Guests |
| 8 | 3 | 3 | Unknowns |
| 9 | 1 | 1 | Users |
| 10 | 1 | 1 | Users |
+------+-------+----------+-----------+
結合条件は、RecordsetQuery
で指定します。when
の最初のパラメータに結合元の列名、3番目に結合先の列名を指定します。条件は=
だけでなくすべて使用できます。複数の条件の指定も可能です。
また、事前に2つのレコードセットを取得してからJoinすることもできます。この場合はRecordset::join
メソッドを使用します。
事前に取得済みの2つのレコードセット$group_rs
と$customers_rs
をcustomer.group = groups.group_id
でJoinする
$rq = new RecordsetQuery();
$rq->when('group', '=', 'group_id')
$customers_rs->join($group_rs, $rq);
$customers = $customers_rs->toArray(); // recordsetからオブジェクトの配列に変換
この方法だと、事前に不要な行をRecordset::matchBy
を使って取り除いておくことができます。
recordset::toArray
でrecordset
からオブジェクトの配列に変換できます。
Recordset OuterJoin
Recordset OuterJoin
は、結合する行がない場合にも元の行を削除しないという点以外は、Recordset InnerJoinと同じです。 join
メソッドをouterJoin
に変えて呼び出します。
結合する行がない場合、そのフィールドの値はnull
になります。
GroupBy
GroupBy
の条件はGroupQuery
で指定します。
グループごとの人数を数える
$gq = new GroupQuery();
$gq->keyField('group')->addFunction(new Count('memberCount'));
$result = $qe->groupBy($gq)->all();
echo 'group ='. $result[0]->group . ' member count = '. $result[0].memberCount;
OrderBy
グループと名前でソートする
$asc = true;
$customers = $qe
->orderBy('group', $asc) // 1番目のソートキー
->orderBy('name', $asc) // 2番目のソートキー
->all();
MatchBy
MatchBy
はローカルでレコードをフィルタリングします。条件はRecordsetQuery
で指定します。
データ取得後、ローカルでnote = null
のレコードを残し他を除外する
$rq = new RecordsetQuery();
$rq->whenIsNotNull('note');
$customers = $qe->keyValue(5)
->whereBetween('id', [5,11])
->matchBy($rq)
->get();
Union
Recordset
オブジェクトはUnion結合することができます。Recordset
オブジェクトを取得するためには、get()
の代わりにrecordset()
を使用します。
id = 1 to 10
のレコードセットとid = 31 to 40
のレコードセットを結合する
$rs = $qe->keyValue(1)
->where('id', '>=', 1)
->where('id', '<=', 10)
->recordset();
$customers = $qe->keyValue(31)
->where('id', '>=', 31)
->where('id', '<=', 40)
->union($rs)
->get();
Calculate
sales
テーブルの金額について様々な計算をしてみましょう。
$qa = DB::queryExecuter('sales');
Count
'2016-11'
の売上の数を数える
$lines = $qa::index(1)->keyValue('2016-11-01')
->where('date', '>=', '2016-11-01')
->where('date', '<=', '2016-11-30')
->count();
Sum
'2016-11'
の売上高を合計する
$amount = $qa::index(1)->keyValue('2016-11-01')
->where('date', '>=', '2016-11-01')
->where('date', '<=', '2016-11-30')
->sum('amount');
Min
'2016-11'
の売上高の最小値を取得
$amount = $qa::index(1)->keyValue('2016-11-01')
->where('date', '>=', '2016-11-01')
->where('date', '<=', '2016-11-30')
->min('amount');
Max
'2016-11'
の売上高の最大値を取得
$amount = $qa::index(1)->keyValue('2016-11-01')
->where('date', '>=', '2016-11-01')
->where('date', '<=', '2016-11-30')
->max('amount');
Average
'2016-11'
の売上高の平均値を取得
$amount = $qa::index(1)->keyValue('2016-11-01')
->where('date', '>=', '2016-11-01')
->where('date', '<=', '2016-11-30')
->average('amount');
スナップショット
スナップショットを使って、複数の読み取りを、同一のトランザクションで保存された一貫性のあるものにします。(一貫性読み取り、CONSISTENT_READ)
DB::beginSnapshot(); // スナップショット開始
$rs = $qe->keyValue(1)
->where('id', '>=', 1)
->where('id', '<=', 10)
->recordset();
$customers = $qe->keyValue(31)
->where('id', '>=', 31)
->where('id', '<=', 40)
->union($rs)
->get();
DB::endSnapshot(); // スナップショット終了
データベースアクセスのトリガー
QueryExecuter
に対するオペレーションはキューに蓄積され、トリガーとなるメソッドが呼ばれた際に実行され、データベースにアクセスします。トリガーとなるメソッドは以下の通りです。
get()
,read()
,recordset()
all()
first()
,firstOrFail()
,firstOrNew()
,firstOrCreate()
find()
,findOrFail()
,findMany()
chunk()
,cursor()
update()
,delete()
count()
,sum()
,min()
,max()
,avg()
,average()
実行順序
トリガーメソッドが呼ばれた際、キューのオペレーションは蓄積された順に実行されます。
例:
$results = $qe->groupBy(xx)->orderBy(xx)->all();
上記のコードは以下のように実行されます:
$rs = $at->read($q);
$rs->groupBy(xx);
$rs->orderBy(xx);
$results = $rs->toArray();
書き込みオペレーション
Insert
新しいレコードを挿入し、auto-incrementのid
を取得します。
$customer = $qe->insert(['id' => 0, 'name' => 'akio']);
echo $customer->id; // 新しいidを表示
Update
条件に合ったレコードを更新します。
id = 100
の顧客の名前を'hanako'
に更新
$count = $qe->keyValue(100)->update(['name' => 'hanako']);
group = 3
の顧客すべてのグループを5
に更新
$count = $qe->index(1)->keyValue(3)->where('group', 3)->update(['group' => 5]);
Delete
条件に合ったレコードを削除します。
id = 100
の顧客を削除
$count = $qe->keyValue(100)->delete();
group = 5
の顧客すべてを削除
$count = $qe->index(1)->keyValue(5)->where('group', 5)->delete();
ServerCursor
複雑な更新や削除などは、サーバーカーソルを使用すると簡単に効率よく処理できます。サーバーカーソルはインデックス順に1レコードずつ移動し、値を判定しながら更新や削除などを行うことができます。
トランザクション中であればカレントレコードはロックされ、安全に更新できます。
カーソルの取得
$op = self::SEEK_EQUAL;
$lockBias = Transactd::LOCK_BIAS_DEFAULT;
$forword = true;
$scr = $qe->index(0)->keyValue(5)->serverCursor($op, $lockBias, $forword);
カーソルの取得には、使用するインデックスとキー値を指定してserverCursor
を呼び出します。最初のカーソル位置を決める$op
パラメータには以下の値を指定できます:
QueryExecuter::SEEK_EQUAL
キー値が指定した値と一致するレコードに移動します。QueryExecuter::SEEK_FIRST
インデックスの先頭レコードに移動します。QueryExecuter::SEEK_LAST
インデックスの最終レコードに移動します。QueryExecuter::SEEK_GREATER_OREQUAL
キー値が指定した値以上の最初のレコードに移動します。QueryExecuter::SEEK_GREATER
キー値が指定した値より大きい最初のレコードに移動します。QueryExecuter::SEEK_LESSTHAN_OREQUAL
キー値が指定した値以下の最初のレコードに移動します。QueryExecuter::SEEK_LESSTHAN
キー値が指定した値より小さい最初のレコードに移動します。
$lockBias
にはロックオプションを指定します。ロックオプションの詳細はTransactdでのロック制御を参照してください。
カーソルには順方向カーソルと逆方向カーソルがあります。方向は$forword
パラメータで指定します。
SEEK_FIRST
とSEEK_LAST
では$forword
パラメータは無視され、それぞれ順方向と逆方向のカーソルが返ります。
カレントの取得と移動
カレントレコードの値はcurrent
メソッドで取得します。prev
とnext
で前後のレコードに移動できます。
$scr = $qe->index(0)->keyValue(5)->serverCursor(self::SEEK_EQUAL);
if ($scr->valid()) { // 移動できたかどうか
$customer = $scr->current(); // 値の取得
if ($customer->active) {
$scr->next(); // 次のレコードに移動
if ($scr->valid()) {
$customer = $scr->current();
// (省略)
$scr->prev(); // 前のレコードに移動
// (省略)
}
}
}
移動できたかどうかはvalid
メソッドで確認します。 validOrFail
を使用すると、無効なカーソルの場合にIOException
をスローできます。
$scr = $qe->index(0)->keyValue(5)->serverCursor(self::SEEK_EQUAL);
$scr->validOrFail(); // 無効な場合IOExceptionをスローする
$customer = $scr->current();
また、foreach
でループもできます。
$scr = $qe->index(0)->keyValue(5)->serverCursor(self::SEEK_EQUAL);
foreach($scr as $customer) {
if ($customer->id > 10) {
break;
}
// (省略)
}
更新と削除
update
とdelete
でカレントレコードの更新や削除が行えます。
更新オペレーションは失敗するとIOException
をスローします。
$scr = $qe->index(0)->keyValue(5)->serverCursor(self::SEEK_EQUAL);
foreach($scr as $customer) {
if ($customer->id > 100) {
break;
}
if (!$customer->active) {
$scr->delete(); // 現在のレコードを削除
} elseif ($customer->zip === '390-831') {
$customer->zip = '390-0831';
$scr->update($customer); // 現在のレコードを$customerで更新
}
}
更新時にキー値を変更した場合でも、デフォルトではカレントレコードの位置は変化しません。削除もカレントレコードの位置は変化しません。どちらも継続して処理を行うことができます。
追加
insert
でレコードの追加ができます。
追加オペレーションは失敗するとIOException
をスローします。
$scr = $qe->index(0)->keyValue(5)->serverCursor(self::SEEK_EQUAL);
if (!$scr->valid()) {
$customer = new stdClass;
$customer->name = 'abc';
$scr->insert($customer); // $customerを追加
}
追加後も、カレントレコードの位置は変化しません。
トランザクションとロック
トランザクション内で、書き込みオペレーションおよびサーバーカーソルで読み取ったレコードは必ずロックされます。ロックはデフォルトで排他ロックです。各オペレーションでのロック指定は不要です。
サーバーカーソルではオペレーションごとに、共有ロックの指定を行うこともできます。
また、トランザクション開始時にロックタイプと開放について指定することができます。
Note: Transactd PHP ORMには、読み取りはスレーブ、書き込みはマスターへ自動で振り分ける機能があります。
トランザクションで読み取りロックされるのはマスターへのアクセスのみです。マスターにアクセスするのは、queryExecuter::getWritableTable()
で取得したテーブルへのオペレーションです。具体的にはinsert
、update
、delete
系のオペレーションとサーバーカーソルです。それ以外のfind
やget
などによる読み取りはスレーブアクセスであり、トランザクション中に呼び出してもロックはされません。
ただし、スレーブホストを省略した場合や、スレーブとマスターが同じホストの場合は、読み取りアクセスに使用するテーブルと書き込みアクセスに使用するテーブルが同じであるため、find
やget
などによる読み取りでもロックされます。
ロックタイプ
トランザクション時のロックタイプは以下の2種類があります。
row lock
(行ロック)next key lock
(行ロック+ギャップロック)
row lock
は、読み取ったレコードをロックしたい場合に使用します。
next key lock
は、読み取ったレコードのロックおよびその直後への挿入のブロックを行いたい場合に使用します。
トランザクション内で、両方必要になる場合は、next key lock
を指定しましょう。
ロック開放
ロック開放には以下の2種類があります。
シングルレコードロック
トランザクション内で各テーブルごとに、最後に読み取ったレコードのみロックを保持します。このタイプはrow lock
のみ使用可能です。マルチレコードロック
トランザクション内で読み取ったレコードのすべてロックを保持します。
シングルレコードロック
は、ロックが保持されるレコードが少ないため同時実行性に優れています。可能であればこの方法を選択しましょう。
なお、シングルレコードロック
を選択しても、insert
、delete
、update
されたレコードはトランザクションが終了するまでロックが保持されます。
トランザクションの例
シングルレコードロックでトランザクションを使用
try {
DB::beginTransaction();
$count = $qe->index(1)->keyValue(5)->where('group', 5)->delete();
DB::commit();
} catch(\Exception $e) {
DB::rollBack();
}
マルチレコードロック、ギャップロックでトランザクションを使用
try {
DB::beginTransaction(Transactd::MULTILOCK_GAP);
$count = $qe->index(1)->keyValue(5)->where('group', 5)->delete();
DB::commit();
} catch(\Exception $e) {
DB::rollBack();
}
トランザクションロックの詳細はInnoDBのロックとその制御を参照してください。
例外
読み取りオペレーションで該当するレコードが見つからなかった場合、例外は発生しません。ただし、xxxOrFail
メソッドを使用した場合および引数に$throwException = true
を指定した場合は、例外がスローされます。また、処理でエラーが発生した場合は例外がスローされます。
書き込みオペレーションでは、処理でエラーが発生した場合に例外がスローされます。
レコードをPHPの配列で取得する
1行をオブジェクトではなくPHPのハッシュ配列で取得できます。
$recordset = $qe->keyValue(1)->where('id', '>=', 1)->recordset(); // recordsetで取得
/* フィールド名とフィールド番号をキーにした配列でフェッチするようモードを指定 */
$recordset->fetchMode = transactd::FETCH_VAL_BOTH;
$records = $recordset->toArray(); // transactd::FETCH_VAL_BOTHで取得
foreach($records as $record) {
$id = $record['id'];
// (省略)
}
Transcatdのネイティブオブジェクトを取得する
TransactdのネイティブAPIも簡単に使用できます。
Table
書き込みも可能なtable
オブジェクトを取得します。
$table = $qe->table();
ActiveTable
読み取り専用(書き込み不可)なactiveTable
オブジェクトを取得します。
$at = $qe->activeTable();
Recordset
get()
の代わりにrecordset()
を使用します。
$recordset = $qe->keyValue(1)->where('id', '>=', 1)->recordset();
Database
デフォルトのコネクションを使用しmasterのdatabase
オブジェクトを取得
$database = DB::master();
コネクションInternal
を使用しslaveのdatabase
オブジェクトを取得
$database = DB::connection('Internal')->slave();