C ++用の新しいSQLite ORM

みなさんこんにちは。 私は初めてHabréに書き込みます。厳密に判断しないでください。 C ++でユニバーサルSQLite ORMライブラリを見つけた経験と、C ++ sqlite_ormでSQLiteを操作するための独自のライブラリの新しい開発を共有したいと思います。







ORMを検索したとき、いくつかの重要なポイントから始めました。









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つのオプションがあります。







  1. LIMIT%limit%
  2. LIMIT%limit%OFFSET%offset%
  3. 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



です。








All Articles