タイトルを読んだ後、多くの人はおそらく疑問を抱いています-すでに走り込んだモングース、モンゴリート、TypeORMなどを持っているなら、なぜ別の自転車ですか? 答えるには、ORMとODMの違いを理解する必要があります。 ウィキペディアを見る:
ORM(オブジェクトリレーショナルマッピング、オブジェクトリレーショナルマッピング、または変換)は、データベースをオブジェクト指向プログラミング言語の概念と接続し、「仮想オブジェクトデータベース」を作成するプログラミングテクノロジです。
つまり、ORMはまさにデータのリレーショナル表現です。 リレーショナルデータベースでは、ドキュメントを取得して別のドキュメントのフィールドに埋め込む方法はありません(この記事のテーブルエントリはドキュメントとも呼ばれますが、これは正しくありません)。もちろん、JSONフィールドに文字列として保存できますが、そのデータにインデックスを作成することはできません出てきます。 代わりに、「リンク」が使用されます-添付ドキュメントがあるフィールドでは、代わりにその識別子が書き込まれ、この識別子を持つドキュメントは隣接するテーブルに保存されます。 ORMはそのようなリンクを処理できます-それらからのレコードはデータベースから自動的または即座に遅延取得され、保存するとき、最初に子ドキュメントを保存し、それに割り当てられた識別子を取得し、親ドキュメントのフィールドに書き込み、その後親ドキュメントのみを保存する必要はありません ORMに親ドキュメントとそれに関連するすべてを保存するように要求するだけで、彼(オブジェクトリレーショナルマッパー)が正しい方法を見つけ出します。 それどころか、ODMはそのようなリンクの操作方法を知りませんが、埋め込みドキュメントについては知っています。
違いはほぼ明らかなので、上記のすべては単なるODMです。 TypeORMでMongoDBを使用する場合でも、いくつかの制限( https://github.com/typeorm/typeorm/issues/655 )があり、これが通常のODMになります。
そして、あなたは尋ねます-なぜですか? ドキュメント指向のデータベースで作業しているときにリンクが必要なのはなぜですか? 少なくとも1つのシンプルですが、それでも必要な場合によく遭遇する状況があります:複数の親ドキュメントは子ドキュメントを示し、各親はこれらのコピーのデータの整合性を確保するために苦労するか、または子ドキュメントを個別のコレクションに保存することができます。すべての親にリンクを渡します(親を子に埋め込むことはできますが、これは常に可能とは限りません。第1に、関係が多対多になることがあり、第2に、子のタイプが二次的すぎる可能性があります Nシステムで、明日は)何かキーを望んでいない、それに埋め込まれたデータベースから完全に消えることがあります。
長い間、いくつかの優れたORM ( thinky 、 requelize 、...)がある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で問題を作成します 。
私は主にフロントエンドの開発に携わっており、データベースが苦手なので、どこかでナンセンスを書いた場合は、理解して許してください:)。
ご清聴ありがとうございました。