マラキア-MongoDBのORM

タイトルを読んだ後、多くの人はおそらく疑問を抱いています-すでに走り込んだモングース、モンゴリート、TypeORMなどを持っているなら、なぜ別の自転車ですか? 答えるには、ORMとODMの違いを理解する必要があります。 ウィキペディアを見る:







ORM(オブジェクトリレーショナルマッピング、オブジェクトリレーショナルマッピング、または変換)は、データベースをオブジェクト指向プログラミング言語の概念と接続し、「仮想オブジェクトデータベース」を作成するプログラミングテクノロジです。

つまり、ORMはまさにデータのリレーショナル表現です。 リレーショナルデータベースでは、ドキュメントを取得して別のドキュメントのフィールドに埋め込む方法はありません(この記事のテーブルエントリはドキュメントとも呼ばれますが、これは正しくありません)。もちろん、JSONフィールドに文字列として保存できますが、そのデータにインデックスを作成することはできません出てきます。 代わりに、「リンク」が使用されます-添付ドキュメントがあるフィールドでは、代わりにその識別子が書き込まれ、この識別子を持つドキュメントは隣接するテーブルに保存されます。 ORMはそのようなリンクを処理できます-それらからのレコードはデータベースから自動的または即座に遅延取得され、保存するとき、最初に子ドキュメントを保存し、それに割り当てられた識別子を取得し、親ドキュメントのフィールドに書き込み、その後親ドキュメントのみを保存する必要はありません ORMに親ドキュメントとそれに関連するすべてを保存するように要求するだけで、彼(オブジェクトリレーショナルマッパー)が正しい方法を見つけ出します。 それどころか、ODMはそのようなリンクの操作方法を知りませんが、埋め込みドキュメントについては知っています。







違いはほぼ明らかなので、上記のすべては単なるODMです。 TypeORMでMongoDBを使用する場合でも、いくつかの制限( https://github.com/typeorm/typeorm/issues/655 )があり、これが通常のODMになります。







そして、あなたは尋ねます-なぜですか? ドキュメント指向のデータベースで作業しているときにリンクが必要なのはなぜですか? 少なくとも1つのシンプルですが、それでも必要な場合によく遭遇する状況があります:複数の親ドキュメントは子ドキュメントを示し、各親はこれらのコピーのデータの整合性を確保するために苦労するか、または子ドキュメントを個別のコレクションに保存することができます。すべての親にリンクを渡します(親を子に埋め込むことはできますが、これは常に可能とは限りません。第1に、関係が多対多になることがあり、第2に、子のタイプが二次的すぎる可能性があります Nシステムで、明日は)何かキーを望んでいない、それに埋め込まれたデータベースから完全に消えることがあります。







長い間、いくつかの優れたORMthinkyrequelize 、...)があるRethinkDBを使用していましたが、最近、このデータベースの開発活動は非常に落胆しています。 私はMongoDBに目を向けることにしました 、最初に見つけなかったのはそのようなパッケージです。 自分で書いてみませんか。それはかなり興味深い経験になると思い、マラキアに出会いました。







設置



npm i -S maraquia
      
      





typescriptと共に使用する場合は、 tsconfig.json



"experimentalDecorators": true



も追加する必要があります。







接続設定



次の2つの方法があります。ここでは、より簡単な方法を検討します。プロジェクトフォルダーで、次を追加するconfig/maraquia.json



を作成します。







 { "databaseUrl": "mongodb://localhost:27017/", "databaseName": "Test" }
      
      





使用する



DBへの保存



一方向リンクとの1対多の関係の簡単な例(例はtypescriptにあり、最後にjavascriptの例があります):







 import { BaseModel, Field, Model } from 'maraquia'; @Model({ collectionName: 'Pet' }) class Pet extends BaseModel { @Field() name: string | null; } @Model({ collectionName: 'Owner' }) class Owner extends BaseModel { @Field() name: string | null; @Field(() => Pet) pets: Promise<Array<Pet> | null>; } (async () => { //         let pet = new Pet({ name: 'Tatoshka' }); let owner = new Owner({ name: 'Dmitry', pets: [pet] }); await owner.save(); })();
      
      





データベースに、エントリを持つPet



Owner



2つのコレクションが表示されます。







 { "_id": "5a...1f44", "name": "Tatoshka" }
      
      





そして







 { "_id": "5a...1f43", "name": "Dmitry", "pets": ["5a...1f44"] }
      
      





save



メソッドは、 owner



モデルであるMaraquiaでのみ呼び出され、予想どおり、2番目のドキュメント自体を保存しました。







例を複雑にするために、多対多の関係と両方向のリンク:







 @Model({ collectionName: 'User' }) class User extends BaseModel { @Field() name: string | null; @Field(() => Group) groups: Promise<Array<Group> | null>; } @Model({ collectionName: 'Group' }) class Group extends BaseModel { @Field() name: string | null; @Field(() => User) users: Promise<Array<User> | null>; } let user1 = new User({ name: 'Dmitry' }); let user2 = new User({ name: 'Tatoshka' }); let group1 = new Group({ name: 'Admins', users: [user1] }); let group2 = new Group({ name: 'Moderators', users: [user1, user2] }); user1.groups = [group1, group2] as any; user2.groups = [group2] as any; await group1.save();
      
      





エントリを含むUser



コレクションがデータベースに表示されます。







 { "_id": "5a...c56f", "name": "Dmitry", "groups": ["5a...c56e", "5a...c570"] } { "_id": "5a...c571", "name": "Tatoshka", "groups": ["5a...c570"] }
      
      





およびエントリを持つGroup



コレクション:







 { "_id": "5a...c56e", "name": "Admins", "users": ["5a...c56f"] } { "_id": "5a...c570", "name": "Moderators", "users": ["5a...c56f", "5a...c571"] }
      
      





通常、ORMの場合のように、 hasOne



hasMany



belongsTo



ような名前のデコレータがないことにすでに気づいているでしょう。 Maraquiaはこの追加情報なしで管理します。hasOneまたはhasManyは値によって決定され、配列はhasManyを意味します。 組み込みドキュメントまたは外部ドキュメント(別のコレクションに格納されている)は、そのスキーム内の塗りつぶされたcollectionName



存在によって決定されます。 たとえば、最初の例で、 collectionName: 'Pet'



という行をコメント化して再度実行すると、レコードはOwner



コレクションにのみ表示され、次のようになります。







 { "_id": "5b...ec43", "name": "Dmitry", "pets": [{ "name":"Tatoshka" }] }
      
      





さらに、フィールドタイプのpets



は約束をやめます。

つまり、Maraquiaを使用すると、埋め込みドキュメントを簡単に操作することもできます。







DBからの読み取り



以前にデータベースから保存したものから何かを読み取ろうとします。







 let user = User.findOne<User>({ name: 'Dmitry' }); console.log(user instanceof User); // true console.log(user.name); // 'Dmitry' console.log(await user.groups); // [Group { name: 'Admins', ... }, Group { name: 'Moderators', ... }]
      
      





groups



フィールドを読み取るときに、 await



キーワードが使用されました。対応するフィールドを初めて読み取ると、外部ドキュメントがデータベースから遅延して取り出されます。







しかし、データベースから識別子に対応するドキュメントを引き出すことなく、フィールドに格納された識別子にアクセスする必要があるが、同時にオプションでそれらを引き出す必要がある場合はどうでしょうか? モデルのフィールド名はドキュメントのフィールド名に対応していますが、 dbFieldName



オプションを使用すると、この対応を変更できdbFieldName



。 つまり、ドキュメントの1つのフィールドを参照するモデルの2つのフィールドを定義し、そのうちの1つのタイプを指定しないことで、この問題を解決できます。







 @Model({ collectionName: 'Group' }) class Group extends BaseModel { @Field({ dbFieldName: 'users' }) readonly userIds: Array<ObjectId> | null; //    @Field(() => User) users: Promise<Array<User> | null>; //       }
      
      





文書を削除する



remove



メソッドは、対応するドキュメントをデータベースから削除します。 Maraquiaは、リンクがどこにあるかを知りません。ここで、プログラマは自分で作業する必要があります。







 @Model({ collectionName: 'User' }) class User extends BaseModel { @Field() name: string | null; @Field({ dbFieldName: 'groups' }) groupIds: Array<ObjectId> | null; @Field(() => Group) groups: Promise<Array<Group> | null>; } @Model({ collectionName: 'Group' }) class Group extends BaseModel { @Field() name: string | null; @Field({ dbFieldName: 'users' }) userIds: Array<ObjectId> | null; @Field(() => User) users: Promise<Array<User> | null>; } let user = (await User.findOne<User>({ name: 'Tatoshka' }))!; //     for (let group of await Group.find<Group>({ _id: { $in: user.groupIds } })) { group.userIds = group.userIds!.filter( userId => userId.toHexString() != user._id!.toHexString() ); await group.save(); } //    await user.remove();
      
      





この例では、 userIds



配列がArray#filter



メソッドによって作成された新しい配列に置き換えられましたが、既存の配列を変更できます。Maraquiaはそのような変更も検出します。 つまり、次のようになります。







 group.userIds!.splice( group.userIds!.findIndex(userId => userId.toHexString() == user._id!.toHexString()), 1 );
      
      





検証



フィールドを検証するには、 validate



プロパティをそのオプションに追加します。







 @Model({ collectionName: 'User' }) class User extends BaseModel { @Field({ validate: value => typeof value == 'string' && value.trim().length >= 2 }) name: string | null; @Field({ validate: value => { //  false     : return typeof value == 'number' && value >= 0; //     : if (typeof value != 'number' || value < 0) { return '-   '; //  : return new TypeError('-   '); //  : throw new TypeError('-   '); } } }) age: number | null; @Field(() => Account, { validate: value => !!value }) /*    : @Field({ type: () => Account, validate: value => !!value }) */ account: Promise<Account | null>; }
      
      





joiライブラリによって作成されたオブジェクトを転送することもできます。







 import * as joi from 'joi'; @Model({ collectionName: 'User' }) class User extends BaseModel { @Field({ validate: joi.string().min(2) }) name: string | null; @Field({ validate: joi.number().min(0) }) age: number | null; }
      
      





フック



次のメソッドは、名前に従ってbeforeSave



afterSave



beforeRemove



afterRemove



ます。







JavaScriptで使用



Typescriptは素晴らしいですが、時にはそれなしでそれを必要とすることもあります。 これを行うには、 Model



デコレータに渡されるオブジェクトの代わりに、静的な$schema



フィールドを定義する必要があり$schema



フィールドには、 fields



フィールドもありfields









 const { BaseModel } = require('maraquia'); class Pet extends BaseModel { } Pet.$schema = { collectionName: 'Pet', fields: { name: {} } }; class Owner extends BaseModel { } Owner.$schema = { collectionName: 'Owner', fields: { name: {}, pets: { type: () => Pet } } }; let pet = new Pet({ name: 'Tatoshka' }); let owner = new Owner({ name: 'Dmitry', pets: [pet] }); await owner.save();
      
      





フィールドへの書き込みは、 setField



メソッドを介して行われます。







 pet.setField('name', 'Tosha');
      
      





そして、 fetchField



メソッドを介して外部ドキュメントでフィールドを読み取ります。







 await owner.fetchField('pets');
      
      





残りのフィールドは通常どおり読み取られます。







性能



MongooseおよびMongoritoとパフォーマンスを比較するために、いくつかの簡単なベンチマークを作成しました。 最初のモデルでは、モデルインスタンスが作成されます。 3つすべてについて、同じように見えます。







 let cat = new Cat({ name: 'Tatoshka', age: 1, gender: '1', email: 'tatoshka@email.ru', phone: '+79991234567' });
      
      





結果(多いほど良い):







 Mongoose x 41,382 ops/sec ±7.38% (78 runs sampled) Mongorito x 28,649 ops/sec ±3.20% (85 runs sampled) Maraquia x 1,312,816 ops/sec ±1.70% (87 runs sampled)
      
      





2つ目は同じことですが、データベースに保存します。 結果:







 Mongoose x 1,125 ops/sec ±4.59% (69 runs sampled) Mongorito x 1,596 ops/sec ±4.08% (69 runs sampled) Maraquia x 1,143 ops/sec ±3.39% (73 runs sampled)
      
      





perfフォルダー内のソース。







フッター



このライブラリが実際のプロジェクトでまだ使用されていないので、誰かがこのライブラリを役に立つと思うことを願っています。 期待どおりに動作しない場合は、githubで問題を作成します







私は主にフロントエンドの開発に携わっており、データベースが苦手なので、どこかでナンセンスを書いた場合は、理解して許してください:)。







ご清聴ありがとうございました。








All Articles