در این مقاله قصد داریم تا برخی از ویژگیهای جالب مربوط به Schema در Mongoose را مورد بحث قرار میدهیم که به ما کمک میکند تا کدهای سازمانیافتهتر و قابل نگهداریتری بنویسیم. این مقاله مخصوصا برای کسانی که به ساخت برنامههای NodeJS با استفاده از Mongoose ORM علاقه دارند، میتواند بسیار مفید باشد.
Schema در Mongoose روشی ساختاریافته برای مدلسازی دادهها در پایگاه داده MongoDB ارائه میدهد و این امکان را برای ما فراهم میکند تا خصوصیات و رفتار داکیومنتها را تعریف کنیم. Schemaها به عنوان طرحی برای داکیومنتی که در پایگاه داده ذخیره میشود، عمل میکنند. آنها توسعهدهندگان را قادر میسازند تا دادههای یکپارچه داشته باشند و با MongoDB به شیوهای شهودی و سازماندهی شده کار کنند.
در یک کالکشن MongoDB، یک Schema فیلدهای documentها، data typeها، قوانین اعتبارسنجی، مقادیر پیشفرض، محدودیتها و موارد دیگر را مشخص میکند.
از نظر برنامه نویسی، Mongoose schema یک آبجکت جاوااسکریپت است. در واقع، این یک نمونه از یک کلاس داخلی به نام Schema
در ماژول mongoose
میباشد. به همین دلیل میتوانیم متدهای بیشتری را به نمونه اولیه آن اضافه کنیم. این موضوع به ما کمک میکند تا بسیاری از ویژگیها مانند middleware، متدها، statics و غیره را پیادهسازی نماییم. در ادامه با برخی از آنها آشنا خواهیم شد.
Discriminator ویژگی است که به ما کمک میکند تا چندین مدل (subtype) ایجاد کنیم که از یک مدل پایه (parent) ارثبری میکنند. این امر با تعریف یک schema پایه و سپس extend کردن آن با فیلدهای اضافی خاص برای هر subtype یا هر child schema اتفاق میافتد.
همه داکیومنتها، صرف نظر از مدل خاص آنها، در یک کالکشن MongoDB ذخیره میشوند. این کار دادههای ما را در یک مجموعه واحد سازماندهی میکند و در عین حال امکان انجام کوئری و مدیریت دادههای انعطافپذیر را فراهم میآورد. همچنین، هر داکیومنت شامل یک فیلد خاص است که نوع مدل خاص خود را نشان میدهد و به Mongoose اجازه میدهد بین زیرگروههای مختلف تمایز قائل شود.
۱ – کار خود را با تعریف یک schema پایه شروع میکنیم که دارای فیلدهای مشترک بین subtypeها خواهد بود. سپس، یک مدل از آن میسازیم:
import mongoose from 'mongoose'; const baseSchema = new mongoose.Schema({ name: { type: String, required: true }, }, { discriminatorKey: 'kind' }; // defaults to '__t'); const BaseModel = mongoose.model('Base', baseSchema);
۲ – subtypeهایی را ایجاد میکنیم که با تعریف discriminator
برای هر یک، schema پایه را extend کند.
const catSchema = new mongoose.Schema({ meow: { type: Boolean, default: true } }); // subtype const Cat = BaseModel.discriminator('Cat', catSchema); const dogSchema = new mongoose.Schema({ bark: { type: Boolean, default: true } }); // subtype const Dog = BaseModel.discriminator('Dog', dogSchema);
۳ – سپس میتوانیم داکیومنتها را به روش معمول بسازیم. همه داکیومنتها در یک collection ذخیره میشوند، اما هر کدام بسته به subtype model خود، نوع خاص مربوط به خود را دارا میباشد.
const fluffy = await Cat.create({ name: 'Fluffy' }); const rover = await Dog.create({ name: 'Rover' });
فرض کنید ما در حال ساخت یک وب اپلیکیشن چند کاربره Ecommerce هستیم که سه نقش اصلی کاربر را در خود جای میدهد: ادمین، مشتری و فروشنده. هر یک از این نقشها وظیفه مهمی در اکوسیستم خرید آنلاین برعهده دارد.
اگر بخواهیم برای هر نقش یک کلاس جدا بسازیم، متوجه میشویم که هر سه آنها فیلدها و متدهای مشترکی باهم دارند. بنابراین، ممکن است تصمیم بگیریم که یک parent schema (کاربر) و سایر schemaهای child (مشتری، فروشنده، ادمین) را بسازیم که از آن ارثبری میکنند.
برای رسیدن به این هدف میتوانیم از discriminator
استفاده کنیم.
کد زیر را در فایل user.model.js
مینویسیم:
import mongoose from "mongoose"; const userSchema = mongoose.Schema( { name: String, profilePic: String, email: String, password: String, birthDate: Date, accountAcctivated: { type: Boolean, default: false }, }, { timestamps: true, discriminatorKey: "role", } ); const User = mongoose.model("User", userSchema); export default User;
اکنون ما مدل پایه (User
) را داریم که subtypeهای دیگر از آن ارثبری میکنند. در این parent schema، فیلدهای مشترکی را تعریف میکنیم که همه کاربران، صرف نظر از نقش آنها به اشتراک میگذارند.
در فایل client.model.js
:
import mongoose from "mongoose"; import User from "./user.model.js"; const clientSchema = mongoose.Schema( { products: Array, address: String, phone: String, } ); const Client = User.discriminator("Client", clientSchema); export default Client;
سپس، در فایل seller.model.js
:
import mongoose from "mongoose"; import User from "./user.model.js"; export const sellerSchema = mongoose.Schema( { rating: Number, businessType: { type: String, enum: ["individual", "corporation"] }, } ); const Seller = User.discriminator("Seller", sellerSchema); export default Seller;
درنهایت، در فایل admin.model.js
:
import mongoose from "mongoose"; import User from "./user.model.js"; export const adminSchema = mongoose.Schema( { permissions: Array, assignedTasks: Array, department: String, } ); const Admin = User.discriminator("Admin", adminSchema); export default Admin;
subtypeها یا childها Client
، Seller
و Admin
خواهند بود. در هر subtype schema، ما باید هر گونه فیلد یا رفتار اضافی را فقط به این subtype اضافه کنیم. با ایجاد مدل child با استفاده از discriminator، تمامی فیلدها و متدهای مدل parent خود یعنی مدل User
را ارثبری میکند.
کد قبلی یک کالکشن user
در پایگاه داده ایجاد میکند که هر داکیومنت دارای یک فیلد role
با مقادیر Client یا Seller یا Admin است. همه داکیومنتها فیلدهای parent (user
) را به اشتراک میگذارند و بسته به role
هر داکیومنت، هر کدام یک فیلد اضافی دیگر نیز دارند.
اگرچه تمام داکیومنتها در یک collection ذخیره میشوند، اما مدلها در هنگام کدنویسی کاملاً از هم جدا هستند.
به عنوان مثال، اگر ما نیاز داریم همه مشتریان از کالکشن User
را بازیابی کنیم، باید Client.find({})
را بنویسیم. این عبارت از discriminator key برای یافتن تمام داکیومنتهایی که role
آنها Client
است، استفاده میکند. به این ترتیب، هر عملیات یا درخواستی که به یکی از مدلهای child اشاره دارد، همچنان جدا از مدل parent نوشته میشود.
Staticها برای تعریف توابعی که در سطح مدل عمل میکنند بسیار مفید میباشد. آنها به ما اجازه میدهند تا توابع با قابلیت استفاده مجدد را برای عملیات مربوط به کل مدل تعریف کنیم. همچنین، به کپسوله کردن منطقی که در مدل به جای داکیومنتهای individual اعمال میشود کمک میکنند و کد ما را تمیزتر، سازماندهی شدهتر و قابل نگهداریتر میکنند.
متدهایی مانند find
، findOne
، findById
و غیره همگی متدهایی هستند که به مدل متصل میباشند. با استفاده از ویژگی statics
مربوط به schema های Mongoose، میتوانیم متد مدل خود را بسازیم.
ویژگی Statics بسیار قدرتمند است. با استفاده از آنها، میتوانیم کوئریهای پیچیدهای را کپسوله نماییم که ممکن است بخواهیم دوباره از آنها استفاده کنیم. علاوه بر این، میتوانیم برای عملیاتهایی که دادهها را اصلاح یا جمعآوری میکنند، Statics ایجاد کنیم. مانند شمارش داکیومنتها یا یافتن آنها بر اساس معیارهای خاص.
Staticها به راحتی ساخته میشوند. ما با استفاده از آبجکت statics
روی schema خود یک متد static تعریف میکنیم.
در فایل user.model.js
، این متدهای static countUsers
و findByEmail
را اضافه مینماییم:
// model method userSchema.statics.countUsers = function () { return this.countDocuments({}); }; // model method userSchema.statics.findByEmail = async function (email) { return await this.findOne({ email }); };
داخل هر متد static، this
به خود مدل اشاره میکند. در این مثال، this
در this.findOne({ email })
به مدل User
اشاره دارد.
به عنوان مثال:
const user = await User.findByEmail("foo@bar.com"); //or const client = await Client.findByEmail("foo@bar.com"); //or const seller = await Seller.findByEmail("foo@bar.com"); //or const admin = await Admin.findByEmail("foo@bar.com");
وقتی متد static را روی مدل خود فراخوانی میکنیم، متد فراخوانی میشود و this
با مدلی که statics را روی آن فراخوانی کردهایم، جایگزین میگردد. این خط یک کوئری را برای یافتن یک داکیومنت واحد در کالکشن MongoDB انجام میدهد که در آن فیلد email
با آرگومان email
ارائه شده مطابقت دارد.
Methodها توابعی هستند که میتوانیم آنها را روی یک schema تعریف کنیم. همینطور میتوانیم آنها را بر روی نمونههایی از داکیومنتهای ایجاد شده از این schema فراخوانی نماییم. Methodها به کپسوله کردن منطق در خود داکیومنت کمک کرده و کد ما را تمیزتر و ماژولارتر میکنند.
با استفاده از متدهای نمونه، میتوانیم به راحتی با دادههای مرتبط با داکیومنتهای خاص تعامل داشته باشیم و آنها را دستکاری کنیم.
میتوانیم با استفاده از آبجکت methods
، methodها را روی schema تعریف کنیم.
در فایل user.model.js
، یک method داکیومنت اضافه میکنیم که از طریق آن میتوانیم رمز عبور یک کاربر را بررسی نماییم:
// instance or document method userSchema.methods.getProfile = function () { return `${this.name} (${this.email})`; }; // instance or document method userSchema.methods.checkPassword = function (password) { return password === this.password ? true : false; };
داخل هر method داکیومنت، this
به خود داکیومنت اشاره دارد. در این مثال، this
در this.password
به داکیومنت user
اشاره میکند که در آن متد فراخوانی میشود. یعنی ما میتوانیم به تمام فیلدهای این داکیومنت دسترسی داشته باشیم. این موضوع بسیار ارزشمند است. زیرا، میتوانیم هر چیزی را که مربوط به این داکیومنت است را بازیابی، اصلاح و بررسی نماییم.
به عنوان مثال:
const client = await Client.findById(...) client.checkPassword("12345") //or const seller = await Seller.findById(...) seller.checkPassword("12345") //or const admin = await Admin.findById(...) admin.checkPassword("12345")
از آنجایی که methodها توابع instance-level هستند، در داکیومنتها فراخوانی میشوند. await Client.findById(...)
داکیومنتی را return میکند که دارای تمام متدهای داخلی و همچنین متدهای از پیش تعریف شده ما، یعنی checkPassword
و getProfile
میباشد.
بنابراین با فراخوانی آن، برای مثال client.checkPassword("12345")
، کلمه کلیدی this
در تعریف تابع checkPassword
با داکیومنت client
جایگزین میشود. این کار رمز عبور کاربر را با رمز عبور ذخیره شده قبلی در پایگاه داده مقایسه میکند.
query builder در Mongoose یک متد سفارشی است که میتوانیم آن را روی آبجکت query تعریف کنیم تا الگوهای کوئری رایج را ساده و کپسوله کنیم. این query builderها به ما این امکان را میدهند تا منطق کوئری با قابلیت استفاده مجدد و readable بسازیم که کار با دادهها را آسانتر میکند.
یکی از با ارزشترین کاربردهای query builderها، chaining است. میتوانیم آنها را با سایر query builderهایی که ساختهایم یا با متدهای کوئری استاندارد مانند find
، sort
و غیره chain کنیم.
ما query builderها را با افزودن آنها به ویژگی query
یک Mongoose schema تعریف میکنیم.
در فایل user.model.js
، یک متد کوئری کمکی میسازیم که به ما کمک میکند تا pagination را پیادهسازی نماییم.
// query helper userSchema.query.paginate = function ({ page, limit }) { // some code const skip = limit * (page - 1); return this.skip(skip).limit(limit); };
برای پیادهسازی pagination، به دو متغیر مهم نیاز داریم: متغیر اول شماره صفحه، و متغیر دوم، تعداد مواردی است که در هر صفحه باید بازیابی کنیم.
برای پرس و جو از پایگاه داده برای تعداد مشخصی از داکیومنتها، همیشه از متدهای جستجوی داخلی skip
و limit
در mongoose
استفاده میکنیم. متد skip
برای تنظیم cursor پس از تعداد معینی از داکیومنتها استفاده میشود و پس از آن کوئری اجرا میگردد. متد limit
نیز برای بازیابی تعداد خاصی از داکیومنتها مورد استفاده قرار میگیرد.
داخل هر متد query builder، this
به خود کوئری اشاره دارد؛ و از آنجایی که query builderها chainable هستند، میتوانیم هر یک از آنها را پس از یکدیگر فراخوانی نماییم.
درنهایت، هر متد query builder باید یک mongoose query object
را return کند، به همین دلیل است که باید return this.skip(skip).limit(limit)
را بنویسیم.
به عنوان مثال:
const results = await Client.find().paginate({ page: 2, limit: 5 }); //or const results = await Seller.find().paginate({ page: 2, limit: 5 }); //or const results = await Admin.find().paginate({ page: 2, limit: 5 });
سپس میتوانیم آن را در هر کوئری فراخوانی کنیم و منتظر بمانیم که await Client.find().paginate({ page: 2, limit: 5 })
تابع paginate
را فراخوانی کرده و با استفاده از query builder کلمه کلیدی this
را با Client.find()
جایگزین کند.
ما میتوانیم pagination را با شرایط خاصی پیادهسازی نماییم، اما همیشه skip
و limit
را فراخوانی میکنیم. با تعریف query builder paginate
، منظور خود را تکرار نخواهیم کرد و در نتیجه میتوانیم منطق مورد نظر را در یک تابع واحد کپسوله کنیم.
Hookها، که به عنوان middleware نیز شناخته میشوند، توابعی هستند که در نقاط خاصی از lifecycle یک داکیومنت اجرا میشوند. آنها به ما اجازه میدهند تا رفتار سفارشی را قبل یا بعد از عملیات خاصی مانند ذخیره، بهروزرسانی یا حذف داکیومنتها اضافه نماییم.
انواع هوکها عبارتند از:
در فایل user.model.js
، یک middleware ذخیره post
اضافه میکنیم که از طریق آن میتوانیم پس از ذخیره داکیومنت user در پایگاه داده، ایمیلی برای فعالسازی حساب کاربری ارسال نماییم.
// post hook userSchema.post("save", async function (doc, next) { // send email logic // if succeeded return next(); // if failed return next(new Error("Failed to send email!")); });
پس از ایجاد یک کاربر از طریق model.create()
یا هر زمانی که متد save()
را در داکیومنت user فراخوانی کنیم، تابع callback فراخوانی میشود.
در این مثال، اگر لازم باشد که از ارسال ایمیل در save خودداری کنیم، باید یک شرط بنویسیم تا مطمئن شویم که این save
فقط مخصوص یک کاربر جدید میباشد. برای این کار، میتوانیم کدی شبیه به if (doc.createdAt.getTime() === doc.updatedAt.getTime())
را بنویسیم.
ما در این مقاله سعی کردیم یک دید کلی از ویژگیهای schema در Mongoose به دست بیاوریم. همچنین چهار مفهوم کلیدی را بررسی کردیم که عبارتند از: discriminatorها، staticها، methodها و hookها.
Discriminatorها به ما اجازه میدهند تاچندین مدل ایجاد کنیم که یک schema مشترک را به اشتراک میگذارند، که انواع مختلف داکیومنتها را قادر میسازد در یک collection واحد ذخیره شوند. این امر مدیریت دادهها و کوئریها را تسهیل میکند.
Staticها متدهای model-level هستند که قابلیت استفاده مجدد را برای کل مدل ارائه میکنند. آنها کوئریهای پیچیده و منطق دستکاری دادهها را کپسوله میکنند و باعث میشوند تا کد تمیزی در پایگاه کد خود داشته باشیم.
Methodها توابع instance-level هستند که بر روی نمونههای داکیومنت جداگانه عمل میکنند. آنها رفتارهای سفارشی و دستکاری دادهها را برای هر داکیومنت مجاز میکنند، بنابراین میتوانیم دادههای داکیومنت را به روشی خاص تغییر دهیم، مانند formatting یا محاسبه مقادیر بر اساس فیلدهای آن.
Hookها (یا middleware) به ما این امکان را میدهند تا توابع را در نقاط خاصی از lifecycle داکیومنت اجرا کنیم، مانند قبل یا بعد از ذخیره، بهروزرسانی یا حذف یک داکیومنت. این کار برای پیادهسازی اعتبارسنجی، ورود به سیستم یا هر گونه side effect دیگر مربوط به عملیات پایگاه داده مفید میباشد.
این ویژگیها باهم تطبیقپذیری و سازماندهی مدلهای Mongoose را افزایش میدهند و ساخت برنامههای قوی و قابل نگهداری با MongoDB را آسانتر میکنند.
دیدگاهها: