C ++用の別のReflectionライブラリとORM





すぐにレビューのためにここで発行されたサイクリングについて警告します。 タイトルを読んでも、「お母さん、新しいORM 分類群ではない!」という感嘆符がほとんど抑制されない場合は、おそらく、私たちが泳ぐ宇宙ブロスでの攻撃性のレベルを上げないように、さらに読むことを控えることをお勧めします。 この記事は、私がしばらく休暇をとっていたという事実のために非難されました。その間、オコロカブロフスキーのトピックに関するブログ投稿の分野で自分自身を試してみることにしました。 加えて、ここで建設的な批判を得ることを望んでおり、この種の興味深いことで他に何ができるかを理解することができます。 最後に、コードを確認できるgithubリポジトリへのリンクがあります。



別のORMライブラリとは何ですか?



プレゼンテーション層(プレゼンテーション層)、ビジネスロジック(論理層)、およびデータストレージ(データ層)が分離された3層アプリケーションを開発する場合、これらの層の接合部でアプリケーションコンポーネントの相互作用を制限する問題が必ず発生します。 従来、リレーショナルデータベースへのインターフェイスはSQLクエリの言語に基づいて提供されますが、ビジネスロジックのレベルから直接使用することは、通常、ORM(オブジェクトリレーショナルマッピング)を使用して簡単に解決できるいくつかの問題に関連付けられています。





これらの問題に対するこのような簡単な解決策は、すべての味と色に対するさまざまなORM実装の豊富さをもたらしました(リストはWikipediaにあります )。 既存のソリューションが豊富にあるにもかかわらず、既存の品揃えでは満足できない嗜好を持つ「グルメ」(その著者)が常に存在します。 しかし、それ以外の場合はどうでしょうか、これは消費財であり、私たちのプロジェクトはあまりにもユニークであり、既存のソリューションは単に私たちに適合しません( これは皮肉、K.O。の署名です )。







おそらく、数年前に自分のニーズに合うようにORMを書き始めたときに、そのような最大限の考えが私を導きました。 試したORMの何が問題で、何を修正したかを簡単に説明します。



  1. まず、静的な型指定が必要です。これにより、コンパイル時にもDBMSにクエリを書き込むときにほとんどのエラーをキャッチできるため、開発速度が大幅に向上します。

    実装の条件:これは、クエリ検証のレベル、コンパイル時間(C ++の場合はIDEの応答性にも関連する)、およびコードの可読性の間の合理的な妥協点である必要があります。
  2. 第二に、これは柔軟性であり、任意の(合理的な制限内で)要求を書き込む機能です。 実際には、この項目は、任意のWHERE部分式を使用して制御システム(create-delete-get-update)クエリを記述する可能性と、クロス集計クエリを実行する機能に要約されます。
  3. これに続いて、「あるDBMSから別のDBMSにジャンプするときにプログラムが正しく動作し続ける必要がある」というレベルで、さまざまなDBMSベンダーのサポートが続きます。
  4. ORMリフレクションを他のニーズ(シリアル化、スクリプトバインディング、実装から解放されたファクトリなど)に再利用する機能。 私が言えることは、ほとんどの場合、既存のソリューションへの反映はORMに「釘付け」されているということです。
  5. それでも、Qt moc、protoc、thriftなどのコードジェネレーターに依存したくありません。 したがって、C ++テンプレートとCプリプロセッサの手段のみを使用するようにします。


実際に実装



SQL教科書の「おもちゃ」の例で検討します。 2つのテーブル:CustomerとBookingがあり、1対多の関係で相互に関連付けられています。







コードでは、ヘッダー内のクラスの宣言は次のとおりです。



//    struct Customer : public Object { uint64_t id; String first_name; String second_name; Nullable<String> middle_name; Nullable<DateTime> birthday; bool news_subscription; META_INFO_DECLARE(Customer) }; struct Booking : public Object { uint64_t id; uint64_t customer_id; String title; uint64_t price; double quantity; META_INFO_DECLARE(Booking) };
      
      





ご覧のとおり、そのようなクラスはObjectの共通の祖先(なぜオリジナルなのでしょうか)を継承します。そして、メソッドを宣言することに加えて、マクロMETA_INFO_DECLAREを含んでいます。 このメソッドは、オーバーロードおよびオーバーライドされたObjectメソッドの宣言を追加するだけです。 ご想像のとおり、一部のフィールドはNullableラッパーを介して宣言されており、そのようなフィールドは特別なNULL値を取ることができます。 また、すべての列フィールドはパブリックでなければなりません。



クラスの定義はやや怪しいです:



 STRUCT_INFO_BEGIN(Customer) FIELD(Customer, id) FIELD(Customer, first_name) FIELD(Customer, second_name) FIELD(Customer, middle_name) FIELD(Customer, birthday) FIELD(Customer, news_subscription, false) STRUCT_INFO_END(Customer) REFLECTIBLE_F(Customer) META_INFO(Customer) DEFINE_STORABLE(Customer, PRIMARY_KEY(COL(Customer::id)), CHECK(COL(Customer::birthday), COL(Customer::birthday) < DateTime(1998, January, 1)) ) STRUCT_INFO_BEGIN(Booking) FIELD(Booking, id) FIELD(Booking, customer_id) FIELD(Booking, title, "noname") FIELD(Booking, price) FIELD(Booking, quantity) STRUCT_INFO_END(Booking) REFLECTIBLE_F(Booking) META_INFO(Booking) DEFINE_STORABLE(Booking, PRIMARY_KEY(COL(Booking::id)), INDEX(COL(Booking::customer_id)), // N-to-1 relation REFERENCES(COL(Booking::customer_id), COL(Customer::id)) )
      
      





STRUCT_INFO_BEGIN ... STRUCT_INFO_ENDブロックは、クラスフィールドリフレクション記述子の定義を作成します。 マクロREFLECTIBLE_Fは、フィールドのクラス記述子を作成します(リフレクションメソッドをサポートするクラス記述子を作成するためのREFLECTIBLE_M、REFLECTIBLE_FMもありますが、これは投稿ではありません)。 META_INFOマクロは、オーバーロードされたオブジェクトメソッドの定義を作成します。 最後に、最も興味深いマクロであるDEFINE_STORABLEは、クラスのリフレクションと宣言された制約に基づいてリレーショナルテーブルの定義を作成し、スキーマの整合性を確保します。 特に、テーブル間の1対多の関係をチェックし、誕生日フィールドをチェックします(たとえば、大人のクライアントのみにサービスを提供したい場合)。 データベースに必要なテーブルを作成するのは簡単です。



  SqlTransaction transaction; Storable<Customer>::createSchema(transaction); Storable<Booking>::createSchema(transaction); transaction.commit();
      
      





SqlTransactionは、ご想像のとおり、実行される操作の分離と原子性を提供し、データベースへの接続もキャプチャします(異なるDBMSへの複数の名前付き接続、または1つのDBMSへの要求の並列化-接続プール)。 この点で、トランザクションの再帰的なインスタンス化は避けてください-デッドロックを取得できます。 すべての要求は、トランザクションのコンテキストで実行する必要があります。



お問い合わせ



リクエスト例
挿入


これは最も単純なタイプのクエリです。 オブジェクトを準備して、そのオブジェクトに対してinsertOneメソッドを呼び出すだけです。



  SqlTransaction transaction; Storable<Customer> customer; customer.init(); customer.first_name = "Ivan"; customer.second_name = "Ivanov"; customer.insertOne(transaction); Storable<Booking> booking; booking.customer_id = customer.id; booking.price = 1000; booking.quantity = 2.0; booking.insertOne(transaction); transaction.commit();
      
      





1つのコマンドで複数のレコードをデータベースに追加することもできます(バッチ挿入)。 この場合、リクエストは1回だけ準備されます。



  Array<Customer> customers; //    SqlTransaction transaction; Storable<Customer>::insertAll(transaction, customers); transaction.commit();
      
      





選択


データベースからのデータの取得は、通常次のように実行されます。



  const int itemsOnPage = 10; Storable<Booking> booking; SqlResultSet resultSet = booking.select().innerJoin<Customer>() .where(COL(Customer::id) == COL(Booking::customer_id) && COL(Customer::second_name) == String("Ivanov")) .offset(page * itemsOnPage).limit(itemsOnPage) .orderAsc(COL(Customer::second_name), COL(Customer::first_name)) .orderDesc(COL(Booking::id)).exec(transaction); // Forward iteration for (auto& row : resultSet) { std::cout << "Booking id: " << booking.id << ", title: " << booking.title << std::endl; }
      
      





この場合、イワノフのすべての注文のページネーションが発生します。 別の方法は、全員を獲得することです

テーブルエントリリスト:



  auto customers = Storable<Customer>::fetchAll(transaction, COL(Customer::birthday) == db::null); for (auto& customer : customers) { std::cout << customer.first_name << " " << customer.second_name << std::endl; }
      
      





更新


シナリオの1つ:データベースから受信したレコードを主キーで更新する:



  Storable<Customer> customer; auto resultSet = customer.select() .where(COL(Customer::birthday) == db::null) .exec(transaction); for (auto row : resultSet) { customer.birthday = DateTime::now(); customer.updateOne(transaction); } transaction.commit();
      
      





または、リクエストを手動で作成できます。



  Storable<Booking> booking; booking.update() .ref<Customer>() .set(COL(Booking::title) = "All sold out", COL(Booking::price) = 0) .where(COL(Booking::customer_id) == COL(Customer::id) && COL(Booking::title) == String("noname") && COL(Customer::first_name) == String("Ivanov")) .exec(transaction); transaction.commit();
      
      





削除


同様に、更新リクエストでは、主キーでエントリを削除できます。

  Storable<Customer> customer; auto resultSet = customer.select() .where(COL(Customer::birthday) == db::null) .exec(transaction); for (auto row : resultSet) { customer.removeOne(transaction); } transaction.commit();
      
      





または、リクエストを通じて:



  Storable<Booking> booking; booking.remove() .ref<Customer>() .where(COL(Booking::customer_id) == COL(Customer::id) && COL(Customer::second_name) == String("Ivanov")) .exec(transaction); transaction.commit();
      
      







注意が必要な主なものは、サブクエリがC ++式であり、これに基づいて抽象構文ツリー(AST)が構築されることです。 次に、このツリーは特定の構文のSQL式に変換されます。 これにより、冒頭で述べた静的型付けのみが提供されます。 また、ASTの形式の中間リクエストフォームにより、DBMSプロバイダーに関係なくリクエストの説明を統一することができます。これにはある程度の労力を費やす必要がありました。 現在のバージョンは、PostgreSQL、SQLite3、MariaDBをサポートしています。 バニラMySQLでも原則として開始する必要がありますが、それ以外の場合、このDBMSはそれぞれいくつかのデータ型を処理し、テストの一部が失敗します。



他に何



カスタムストアドプロシージャを記述して、クエリで使用できます。 ORMは、すぐに使用できる組み込みのDBMS機能の一部(upper、lower、ltrim、rtrim、random、abs、coalesceなど)をサポートしていますが、独自に定義することもできます。 ここでは、たとえば、SQLiteのstrftime関数について説明します。



 namespace sqlite { inline ExpressionNodeFunctionCall<String> strftime(const String& fmt, const ExpressionNode<DateTime>& dt) { return ExpressionNodeFunctionCall<String>("strftime", fmt, dt); } }
      
      





さらに、ORMの実装は、可能なリフレクションの使用に限定されません。 C ++で正しいリフレクションをまだ取得していないようです(正しいリフレクションは静的である必要があります(つまり、ライブラリではなくコンパイラレベルで提供される必要があります)。したがって、この実装を使用して、スクリプトエンジンとのシリアル化および統合を試みることができます。 しかし、誰かが興味を持っているなら、多分私はそれについて書きます。



何じゃない



SQLモジュールの主な欠点は、集計クエリ(カウント、最大、最小)およびグループ化(グループ化)をサポートすることができなかったことです。 また、サポートされているDBMSのリストはかなり少ないです。 おそらく将来的には、ODBCを介してSQL Serverをサポートする予定です。

さらに、mongodbとの統合についても考えられます。特に、ライブラリでは非平面構造(サブ構造と配列)を記述することができるためです。



リポジトリへのリンク



All Articles