みなさんこんにちは。 私は初めてHabréに書き込みます。厳密に判断しないでください。 C ++でユニバーサルSQLite ORMライブラリを見つけた経験と、C ++ sqlite_ormでSQLiteを操作するための独自のライブラリの新しい開発を共有したいと思います。
ORMを検索したとき、いくつかの重要なポイントから始めました。
- ライブラリにはCRUDと非CRUDの両方が必要です
- 柔軟なWHERE条件は必要ですが、愚かな
WHERE id = ?
- アプリケーションの更新の場合、移行(同期スキーム)機能が必要です。
- ORDER BYやLIMITなどの機能も必要です
- クラスのシリアル化は、クラスに記述しないでください。 これは私にとって非常に重要なポイントです。Java、特にAndroid開発に精通したからです。 Android開発者は、単一責任の原則(単一責任の原則)を順守しようとします。これは、時間とともに変化する可能性のあるライブラリとモジュールの異なるヒープからアプリケーションを組み立てる場合に非常に重要です。 したがって、C ++ hiberliteの Github SQLiteで最も人気のあるORM'kaは、シリアル化メソッドに適していない-モデルクラスには、直接シリアル化コードを使用した静的
serialize
関数が必要です。 データモデルのコードに依存しないモジュールを探していました。 結局のところ、私はいくつかのシリアライザー(JSON、XML、SQLite)を持つことができ、すべての人をデータモデルに接続する必要がありますが、それを変更しないでください。そうしないと、モデルコードが混乱します。
- 標準ライブラリのスタイルのコード-最近、この傾向が人気を集めています(一般的に、このライブラリは私に感銘を与えました)
- ファイルシステム上のデータベースとメモリ内の両方のサポート
- SQLiteクライアントを介してデータベースにアクセスする必要がある場合に備えて、クラス名とそのフィールドに関係なく、テーブルと列に名前を付ける機能を開発者に任せます。
- 取引
hiberlite
加えて、さまざまなライブラリがhiberlite
ありますが、何らかの理由で機能がほとんどありません。 言い換えると、開発者はlibsqlite3
を使用してデータベースに直接接続するためのコードを記述する必要があることがlibsqlite3
ますが、なぜそのようなORMが必要なのでしょうか?
私は導入部に引きずり込まれたようで、 sqlite_ormライブラリが提供する可能性に直接行きます 。
1)CRUD
例:
struct User{ int id; std::string firstName; std::string lastName; int birthDate; std::shared_ptr<std::string> imageUrl; int typeId; };
struct UserType { int id; std::string name; };
2つのクラス、次に2つのテーブル。
相互作用は、データベースへのインターフェイスを持つサービスオブジェクトであるstorage
オブジェクトを介して行われます。 storage
はmake_storage
関数によって作成されます。 作成時に、スキームが示されます。
using namespace sqlite_orm; auto storage = make_storage("db.sqlite", make_table("users", make_column("id", &User::id, autoincrement(), primary_key()), make_column("first_name", &User::firstName), make_column("last_name", &User::lastName), make_column("birth_date", &User::birthDate), make_column("image_url", &User::imageUrl), make_column("type_id", &User::typeId)), make_table("user_types", make_column("id", &UserType::id, autoincrement(), primary_key()), make_column("name", &UserType::name, default_value("name_placeholder"))));
データモデルはリポジトリについて「最新ではない」ことに注意してください。 また、クラスの列名とフィールド名は、互いに独立しています。 これにより、たとえば、ラクダの場合のコードと、アンダースコアを使用したデータベーススキーマを記述できます。
make_storage
最初のパラメーターはファイル名で、その後テーブルが移動します。 テーブルを作成するには、テーブル名を指定します(クラスとは関係ありません。自動命名を行う場合、実装はあまり良くないためです: typeid(T).name()
を使用する必要があります。これは常に明確な名前ではなくシステム名を返しますが、またはマクロを使ったトリック(通常は承認していません)を使用して、列を示します。 1つの列を作成するには、少なくとも2つのパラメーターが必要です。列の名前とクラスフィールドへのリンクです。 このリンクは、列のタイプと将来の割り当てのアドレスを決定します。 後でAUTOINCREMENT
および/またはPRIMARY KEY
をDEFAULT
追加することもできます。
これで、 storage
オブジェクトの関数の呼び出しを介してデータベースにクエリを送信できます。 たとえば、ユーザーを作成してINSERT
を実行しましょう。
User user{-1, "Jonh", "Doe", 664416000, std::make_shared<std::string>("url_to_heaven"), 3 }; auto insertedId = storage.insert(user); cout << "insertedId = " << insertedId << endl; user.id = insertedId;
INSERT INTO users(first_name, last_name, birth_date, image_url, type_id) VALUES('Jonh', 'Doe', 664416000, 'url_to_heaven', 3)
。
ユーザーオブジェクトの作成時に指定した最初の引数-1はidです。 idはPRIMARY KEY列であるため、作成時には無視されます。 sqlite_orm
は、INSERT sqlite_orm
PRIMARY KEY列を無視し、新しく作成されたオブジェクトのIDを返します。 したがって、INSERTの後、 user.id = insertedId;
を実行しuser.id = insertedId;
-その後、ユーザーは本格的になり、コードでさらに使用できます。
同じユーザーを取得するには、 get
関数を使用します。
try{ auto user = storage.get<User>(insertedId); cout << "user = " << user.firstName << " " << user.lastName << endl; }catch(sqlite_orm::not_found_exception) { cout << "user not found with id " << insertedId << endl; }catch(...){ cout << "unknown exeption" << endl; }
get
は、 User
クラスのオブジェクト(テンプレートパラメーターとして渡したもの)を返します。 このIDを持つユーザーがいない場合、例外sqlite_orm::not_found_exception
ます。 例外のあるこのようなインターフェイスは不便かもしれません。 これは、C ++では、Java、C#、またはObjective-Cで実行できるため、オブジェクトだけを無効にできないためです。 null許容型としてstd::shared_ptr<T>
を使用できます。 この場合、 get
関数の2番目のバージョンget_no_throw
ます。
if(auto user = storage.get_no_throw<User>(insertedId)){ cout << "user = " << user->firstName << " " << user->lastName << endl; }else{ cout << "no user with id " << insertedId << endl; }
ここで、ユーザーはstd::shared_ptr<User>
あり、 nullptr
と同じにすることも、ユーザー自体を保存することもできます。
次に、ユーザーをUPDATE
たい場合があります。 これを行うには、変更するフィールドを変更し、 update
関数を呼び出します。
user.firstName = "Nicholas"; user.imageUrl = "https://cdn1.iconfinder.com/data/icons/man-icon-set/100/man_icon-21-512.png" storage.update(user);
これは次のように機能しUPDATE users SET ... primary key... WHERE id = % , , primary key%
が呼び出されUPDATE users SET ... primary key... WHERE id = % , , primary key%
。
すべてがシンプルです。 リポジトリと対話するためのプロキシオブジェクトはないことに注意してください。リポジトリは「クリーンな」モデルオブジェクトを受け入れて返します。 これにより、ジョブが簡素化され、エントリのしきい値が下がります。
IDによるオブジェクトの削除は、次のように実装されます。
storage.remove<User>(insertedId);
ここでは、コンパイラで型を推測する場所がないため、テンプレートパラメータとして型を明示的に指定する必要があります。
これがCRUDの終了点です。 ただし、これは機能に限定されません。 sqlite_orm
のCRUD関数は、 PRIMARY KEY
を持つ1つの列を持つオブジェクトでのみ機能する関数です。 CRUD以外の機能もあります。
たとえば、 SELECT * FROM users
実行してみましょう。
auto allUsers = storage.get_all<User>(); cout << "allUsers (" << allUsers.size() << "):" << endl; for(auto &user : allUsers) { cout << storage.dump(user) << endl; }
変数allUsers
のタイプはstd::vector<User>
です。 dump
関数に注意してください。リポジトリに関連付けられたクラスオブジェクトを取得し、 std::string
形式でjsonスタイルで情報を返しstd::string
。 たとえば、「{id: '1'、first_name: 'Jonh'、last_name: 'Doe'、birth_date: '664416000'、image_url: '0x10090c3d8'、type_id: '3'}」。
しかし、これでは十分ではありません。 ORMライブラリは、WHERE句がないと完全と見なすことはできません。 したがって、 sqlite_orm
にも存在しますが、非常に強力です。
上記のget_all
関数は、条件付きのwhere
関数の結果を引数として取ることができます。 たとえば、IDが10未満のユーザーを選択してみましょう。クエリは次のようになりますSELECT * FROM users WHERE id < 10
。 コードでは、次のようになります。
auto idLesserThan10 = storage.get_all<User>(where(lesser_than(&User::id, 10)));
または、firstNameフィールドが「John」に等しくないユーザーを選択します。 クエリはSELECT * FROM users WHERE first_name != 'John'
auto notJohn = storage.get_all<User>(where(is_not_equal(&User::firstName, "John")));
さらに、演算子&&
、 ||
そして!
(より明確にするために、これらの演算子のリテラルバージョンを使用することをお勧めします。
auto notJohn2 = storage.get_all<User>(where(not is_equal(&User::firstName, "John")));
notJohn2
同等notJohn
。
そして、リンクされた条件を持つ別の例:
auto id5and7 = storage.get_all<User>(where(lesser_or_equal(&User::id, 7) and greater_or_equal(&User::id, 5) and not is_equal(&User::id, 6)));
このクエリSELECT * FROM users WHERE where id >= 5 and id <= 7 and not id = 6
を実装しました。
またはSELECT * FROM users WHERE id = 10 or id = 16
:
auto id10or16 = storage.get_all<User>(where(is_equal(&User::id, 10) or is_equal(&User::id, 16)));
したがって、条件の任意の組み合わせを「接着」できます。 さらに、SQLiteの「生のクエリ」のように括弧を使用して条件の優先度を指定できます。 たとえば、次の2つのクエリは、返される結果が異なります。
auto cuteConditions = storage.get_all<User>(where((is_equal(&User::firstName, "John") or is_equal(&User::firstName, "Alex")) and is_equal(&User::id, 4))); cuteConditions = storage.get_all<User>(where(is_equal(&User::firstName, "John") or (is_equal(&User::firstName, "Alex") and is_equal(&User::id, 4))));
最初の条件ではWHERE (first_name = 'John' or first_name = 'Alex') and id = 4
で、2番目の条件ではWHERE first_name = 'John' or (first_name = 'Alex' and id = 4)
です。
この魔法は、C ++の括弧には操作の優先順位を明示的に決定するのと同じ機能があるために機能します。 さらに、 sqlite_orm
自体はC ++でSQLiteを操作するための便利なフロントエンドであり、それ(ライブラリ)自体はクエリを実行せず、テキストに変換してsqlite3エンジンを送信します。
IN
ステートメントもあります。
auto evenLesserTen10 = storage.get_all<User>(where(in(&User::id, {2, 4, 6, 8, 10})));
SELECT * FROM users WHERE id IN (2, 4, 6, 8, 10)
が判明しました。 またはここに行があります:
auto doesAndWhites = storage.get_all<User>(where(in(&User::lastName, {"Doe", "White"})));
ここでは、 SELECT * FROM users WHERE last_name IN ("Doe", "White")
リクエストをデータベースに送信しました。
in
関数は、クラスフィールドへのポインターとベクトル/初期化リストの2つの引数を取ります。 ベクトル/初期化リストのコンテンツタイプは、最初のパラメーターとして渡したフィールドポインターと同じです。
条件関数is_equal
、 is_not_equal
、 greater_than
、 greater_or_equal
、 lesser_than
、 lesser_or_equal
はそれぞれ2つの引数を取ります。 引数は、クラスフィールドへのポインタ、または定数/変数のいずれかです。 フィールドへのポインタは、列名、およびリテラルのままクエリに解析され、文字列のみがエッジの周りに引用符が残っています。
質問があるかもしれません:どの列にも示されていないクラスフィールドへのポインターを条件に渡すとどうなりますか? この場合、例外std::runtime_error
が説明テキストとともにスローされます。 リポジトリにバインドされていないタイプを指定した場合も同じことが起こります。
ところで、 WHERE
句はDELETE
クエリで使用できます。 これにはremove_all
関数がありremove_all
。 たとえば、IDが100未満のすべてのユーザーを削除しましょう。
storage.remove_all<User>(where(lesser_than(&User::id, 100)));
上記の例はすべて、本格的なオブジェクトで動作します。 しかしSELECT
1つの列でSELECT
を呼び出したい場合はどうでしょうか? これもあります:
auto allIds = storage.select(&User::id);
これをSELECT id FROM users
と呼びました。 allIds
のタイプはstd::vector<decltype(User::id)>
またはstd::vector<int>
です。
条件を追加できます:
auto doeIds = storage.select(&User::id, where(is_equal(&User::lastName, "Doe")));
ごSELECT id FROM users WHERE last_name = 'Doe'
とおり、これはSELECT id FROM users WHERE last_name = 'Doe'
です。
多くのオプションがあります。 たとえば、idが300未満のすべての姓を要求できます。
auto allLastNames = storage.select(&User::lastName, where(lesser_than(&User::id, 300)));
ORDER BY
ORMもORMも注文なし。 ORDER BY
多くのプロジェクトで使用されており、 sqlite_orm
にはインターフェースがあります。
最も簡単な例-idで並べられたユーザーを選択しましょう:
auto orderedUsers = storage.get_all<User>(order_by(&User::id));
これはSELECT * FROM users ORDER BY id
変わりSELECT * FROM users ORDER BY id
。 または、 where
とorder_by
組み合わせてみましょう: SELECT * FROM users WHERE id < 250 ORDER BY first_name
auto orderedUsers2 = storage.get_all<User>(where(lesser_than(&User::id, 250)), order_by(&User::firstName));
明示的なASC
およびDESC
指定することもできます。 例: SELECT * FROM users WHERE id > 100 ORDER BY first_name ASC
:
auto orderedUsers3 = storage.get_all<User>(where(greater_than(&User::id, 100)), order_by(asc(&User::firstName)));
またはここ:
auto orderedUsers4 = storage.get_all<User>(order_by(desc(&User::id)));
SELECT * FROM users ORDER BY id DESC
でした。
そしてもちろん、 select
はorder_by
でも動作しselect
:
auto orderedFirstNames = storage.select(&User::firstName, order_by(desc(&User::id)));
SELECT first_name FROM users ORDER BY ID DESC
でした。
移行
ライブラリには移行自体はありsync_schema
が、 sync_schema
関数があります。 この関数の呼び出しは、データベースに現在のスキームを要求し、ストレージの作成時に指定されたスキームと比較し、一致しないものがあればそれを修正します。 同時に、既存のデータの安全性はこの呼び出しを保証しません。 スキームが同一になることのみを保証します(またはstd::runtime_error
がスローされます。スキームを同期するルールの詳細については、 githubのリポジトリページを参照してください。
取引
ライブラリには、トランザクションを実装するための2つのオプション、明示的および暗黙的があります。 明示的とは、 begin_transaction
およびcommit
またはrollback
関数の直接呼び出しを意味します。 例:
auto secondUser = storage.get<User>(2); storage.begin_transaction(); secondUser.typeId = 3; storage.update(secondUser); storage.rollback(); // storage.commit(); secondUser = storage.get<decltype(secondUser)>(secondUser.id); assert(secondUser.typeId != 3);
2番目の方法は少し複雑です。 最初のコード:
storage.transaction([&] () mutable { auto secondUser = storage.get<User>(2); secondUser.typeId = 1; storage.update(secondUser); auto gottaRollback = bool(rand() % 2); if(gottaRollback){ // return false; // ROLLBACK } return true; // COMMIT });
transaction
関数はすぐにBEGIN TRANSACTION
呼び出し、ラムダ式を引数として受け取り、 bool
を返します。 true
返された場合、 COMMIT
が実行されROLLBACK
false
場合) ROLLBACK
このメソッドは、トランザクション終了関数(標準ライブラリのmutexのstd::lock_guard
など)を呼び出すことを忘れないようにします。
集計関数AVG
、 MAX
、 MIN
、 COUNT
、 GROUP_CONCAT
ます。
auto averageId = storage.avg(&User::id); // 'SELECT AVG(id) FROM users' auto averageBirthDate = storage.avg(&User::birthDate); // 'SELECT AVG(birth_date) FROM users' auto usersCount = storage.count<User>(); // 'SELECT COUNT(*) FROM users' auto countId = storage.count(&User::id); // 'SELECT COUNT(id) FROM users' auto countImageUrl = storage.count(&User::imageUrl); // 'SELECT COUNT(image_url) FROM users' auto concatedUserId = storage.group_concat(&User::id); // 'SELECT GROUP_CONCAT(id) FROM users' auto concatedUserIdWithDashes = storage.group_concat(&User::id, "---"); // 'SELECT GROUP_CONCAT(id, "---") FROM users' auto maxId = storage.max(&User::id); // 'SELECT MAX(id) FROM users' auto maxFirstName = storage.max(&User::firstName); // 'SELECT MAX(first_name) FROM users' auto minId = storage.min(&User::id); // 'SELECT MIN(id) FROM users' auto minLastName = storage.min(&User::lastName); // 'SELECT MIN(last_name) FROM users'
詳細はこちらをご覧ください 。 批判と同様、貢献も歓迎します。
編集1
最後のコミットでは、タプル(タプル)から複数の列を選択してベクターに「生」する機能が追加されています。 例:
// `SELECT first_name, last_name FROM users WHERE id > 250 ORDER BY id` auto partialSelect = storage.select(columns(&User::firstName, &User::lastName), where(greater_than(&User::id, 250)), order_by(&User::id)); cout << "partialSelect count = " << partialSelect.size() << endl; for(auto &t : partialSelect) { auto &firstName = std::get<0>(t); auto &lastName = std::get<1>(t); cout << firstName << " " << lastName << endl; }
編集2
最後のコミットにより、 LIMIT
およびOFFSET
サポートが追加されます。 LIMIT
およびOFFSET
を使用するための3つのオプションがあります。
- LIMIT%limit%
- LIMIT%limit%OFFSET%offset%
- LIMIT%offset%、%limit%
例:
// `SELECT first_name, last_name FROM users WHERE id > 250 ORDER BY id LIMIT 5` auto limited5 = storage.get_all<User>(where(greater_than(&User::id, 250)), order_by(&User::id), limit(5)); cout << "limited5 count = " << limited5.size() << endl; for(auto &user : limited5) { cout << storage.dump(user) << endl; } // `SELECT first_name, last_name FROM users WHERE id > 250 ORDER BY id LIMIT 5, 10` auto limited5comma10 = storage.get_all<User>(where(greater_than(&User::id, 250)), order_by(&User::id), limit(5, 10)); cout << "limited5comma10 count = " << limited5comma10.size() << endl; for(auto &user : limited5comma10) { cout << storage.dump(user) << endl; } // `SELECT first_name, last_name FROM users WHERE id > 250 ORDER BY id LIMIT 5 OFFSET 10` auto limit5offset10 = storage.get_all<User>(where(greater_than(&User::id, 250)), order_by(&User::id), limit(5, offset(10))); cout << "limit5offset10 count = " << limit5offset10.size() << endl; for(auto &user : limit5offset10) { cout << storage.dump(user) << endl; }
LIMIT 5, 10
およびLIMIT 5 OFFSET 10
意味が異なることを忘れないでください。 正確には、 LIMIT 5, 10
LIMIT 10 OFFSET 5
はLIMIT 10 OFFSET 5
です。