محمدحسن تبریزی
محمدحسن تبریزی
خواندن ۷ دقیقه·۳ سال پیش

یک کلاس برای ایجاد query که می تونه sort,paginate,limit, limitFields انجام بده در expressJS

یکی از چالش هایی که تو برنامه نویسی بک اند بهش بر می خوریم query ها و نحوه ی مدیریت اونا هست. توی این پست می خوام خیلی شیک و تمیز و با استفاده از یک کلاس، این چالش رو برای همیشه، ساده کنم.

فرض کنیم یه پایگاه داده ای از خانه ها داریم که هر خونه یکسری ویژگی ها داره و می خواهیم بر اساس این ویژگی ها کل خونه هامون رو سرچ کنیم یا به عبارتی تخصصی تر با استفاده از query string خونه هارو فیلتر کنیم. حالا queryString چیه؟

فرض کنید ما با استفاده از مسیر api/houses می تونیم همه ی خونه ها رو get کنیم ولی می خواهیم که کاربر با استفاده از یه سری اهرم ها تو قسمت فرانتمون بجای اینکه همه ی خونه ها رو get کنه بتونه بر اساس یه سری ویژگی ها این خونه ها رو get کنه.

پس کاربر با استفاده از اون اهرم ها یک queryString می سازه و به سرور ارسال می کنه. پس مسیری که کاربر میخاد بهش دسترسی پیدا کنه همونه ولی با یه سری ویژگی های جدید مثلا کاربر می خواد با استفاده از: api/houses?city=tabriz&price=500 به همه خونه هایی که تو شهر تبریز هستند و قیمتشون دقیقا 500 است دسترسی پیدا کنه.

به این قسمتی که بعد از علامت سوال هست می گیم queryString.

چطوری توی expressJs پیادش کنیم؟

برای مسیر api/houses یک کنترلر می نویسیم که اگه این مسیر از طرف کاربر خواسته شد اون کنترلر خوانده بشه و همه ی خونه ها رو تحویل کاربر بده.

Const getAllHouse = async(req,res,next) => {

Const houses = await HouseModel.find();

Res.status(200).json({

Status:”success”,

houses

})

}

حالا ما می خواییم بر اساس queryString همه ی خانه ها رو فیلتر کنیم و به کاربر بدیم پس توی این پست، فقط با این کنترلر و داخل این کنترلر فقط با const houses = await HouseModel.find سر و کار خواهیم داشت.

دو راه واسه هندل کردن این موضوع داریم.

  • راهکار اول:

داخل .find() یک آبجکت قرار بدیم تا براساس اون آبجکت find بشه:

Const houses = await HouseModel.find({

City:”tabriz”,

Price=500

});

  • راهکار دوم:

بعد از find()یکسری از متدهای مخصوص mongooseرو زنجیر کنیم.

Const houses =

await HouseModel.find().where(“city”).equals(“tabriz”).where(“price”).equals(500);

هر دو راهکار رو می تونیم برای فیلتر کردن استفاده کنیم که من از روش اول استفاده خواهم کرد.

مورد دیگه ای که باید بهش اشاره کنم اینه که یه سری از فیلتر ها هم داریم که به خود خونه ها ربط ندارن و هر جای دنیا که خواستیم یه بکی بنویسیم باید اونارو مدنظر بگیریم مثلا page واسه pagination هست sort واسه مرتب کردن اطلاعاتی هست که پیدا شده limitواسه ماکسیمم تعدادی هست که بر می گردونه fields هم برای پیدا کردن یه سری فیلدهای خاص است که در ادامه در موردشون می نویسم.

  • گام اول: در گام اول می خواهیم که اینارو یعنی همین مشهور معروفارو از queryStringامون حذف کنیم و بریم سراغ فیلدهایی که داخل اطلاعات خودمون هستند.برای مثال یک queryStringداریم api/houses?page=2&sort=date&limit=6&city=tabriz&price=500

Const queryObj = {…req.query} // querystringرو از آبجکت ریکویست بگیر => {page:2,sort:date,city:tabriz}

Const excludedFields = [“page”,”sort”,”limit”,”fields”]; // مشهورارو حذف کن

excludedFields.forEach(el => delete queryObj[el]); // queryObj => {city:tabriz,price=500}

const query = HouseModel.find(queryObj); //

حالا با فراخوانی کردن query، همه ی خونه هایی که در تبریز هستند و قیمتشون هم 500 هست رو بدست میاریم. با کد زیر

Const houses = await query;

  • گام دوم: پیاده سازی بزرگتر مساوی یا بزرگتر از یا کوچکتر از و کوچکتر مساوی

Api/houses?price[gte]=300

با این query می خواهیم که خونه هایی با قیمت بزرگتر از 300 رو پیدا کنیم. اینجا یک مشکل کوچولو داریم و اونم اینه که ما موقعی که می خواهیم با استفاده از mongoose از قابلیت gte,gt,lte,ltاستفاده کنیم از $استفاده می کنیم ولی موقعی که این مسیر رو به کنترلرمون می فرستیم و از اونجا query رو console.logمی کنیم اون علامت $ رو نمی تونیم پیدا کنیم یعنی چی?

{price:{ $gte:300}} // این همونیه که ما لازم داریم و باید بدیمش به mongoose

{price:{gte:300} // این اونیه که ما از req.query تحویل می گیریم

راهکار چیه؟

می آییم با استفاده از یه reqex قبل از gt,gtr,lt,lte یک $اضافه می کنیم.

let queryStr = JSON.stringify(queryObj); // آبجکت رو به استرینگ تبدیل کن

queryStr.replace(“/\b(gt|gte|lt|lte)\b/g, match => `$${match}`); //بعد توش بگرد دنیال اون 4تا کلمه و قبلش یه علامت دلار بزار با استفاده از template string

const newQueryObj = JSON.parse(queryStr)); // استرینگ رو که بهش علامت دلارو اضافه کردی برگردون به حالت قبلش

let query = HouseModel.find(newQueryObj);

  • گام سوم: بعد از اینکه فیلدهای داخل اطلاعاتمون رو سروسامان دادیم برگردیم به فیلدهایی که حذفشون کردیم.

مرتب کردن:

فرض کنیم اطلاعاتمون رو می خواهیم بر اساس priceمرتب کنیم.

Api/houses?sort=price

If(req.query.sort){ // اگه تو ریکویستمون sort دیدی

query = query.sort(req.query.sort) //.sort رو به کویری که ساختیم زنجیر کن

}

حالا که قابلیت مرتب کردن رو هم بهش اضافه کردیم یه سوال پیش میاد. شاید کاربر می خواهد اطلاعات رو بر اساس چندتا فیلد مرتب کنه?

Api/houses?sort=price,area,…

در این حالت میریم سراغ JSو req.query.sort رو بر اساس ویرگول از هم جدا می کنیم و سپس با فاصله خالی به همدیگه می چسبونیم.

If(req.query.sort){

Const sortBy = req.query.sort.split(“,”).join(“ “);

query = query.sort(sortBy)

}

قدم بعدی اینه که یه حالت پیش فرض به sort اضافه کنیم که اگه کاربر sort رو خالی گذاشت با توجه به اون sortکنه.

If(req.query.sort){

Const sortBy = req.query.sort.split(“,”).join(“ “);

query = query.sort(sortBy) //chaining new method SORT to the query

}else{ ///این قسمت رو بهش اضافه می کنیم

query = query.sort(“-createdAt”);

}

پروجکشن:

با استفاده از fieldLimitting می خواهیم به کاربر این قابلیت رو بدیم که خودش انتخاب کنه که چه فیلدهایی رو می خواهد دریافت کنه. مثلا انتخاب می کنه که فقط قیمت، منطقه و طبقات خونه هایی رو که می خواهد ببینه رو براش نشون بده.

Api/houses?fields=area,price,floors

If(req.query.fields){

Const fields = req.query.fields.split(“,”).join(“ “);

query = query.select(fields);

}else{ // اگه کاربر چیزی وارد نکرد همه چی رو بهش نشون بده غیر از ورژن

Query = query.select(-__V); // don’t show version

}

پجینیشن:

با استفاده از این ویژگی این قابلیت رو ایجاد می کنیم که کاربر در هر صفحه به تعداد مشخصی از اطلاعات دسترسی داشته باشه.

Api/houses?page=2&limit=10 //مثلا صفحه ی دوم رو نشون بدهکه هر صفحه 10 تا داده داشته باشه

Const page = req.query.page * 1 || 1;

Const limit = req.query.limit * 1 || 100; //تعدا داده هایی که تو هر صفحه نشون داده میشه

Const skip = (page - 1) * limit; // این تعداد از داده هارو بپپپر

query = query.skip(skip).limit(limit); // skip و limit رو به کویری مون زنجیر می کنیم

چالشی که اینجا داریم اینه که وقتی کاربر صفحه ای رو خواست که تعداد اطلاعاتمون کم بود و تو اون صفحه اطلاعات نداشتیم چیکار کنیم؟

If(req.query.page){

Const maxHouseCount = await HouseModel.countDocument(); // با استفغاده از یه متد mongoose تعداد کل خونه هامون رو ازش می گیریم

If(skip>= maxHouseCount) Throw new Error();

}

کارمون تقریبا تمومه.

می تونیم یکسری مسیرها هم برای ویژگی های خاص یا عمومی ایجاد کنیم مثلا ارزان ترین ها گران ترین ها و ....

Api/houses/cheapest

در این حالت یک کنترلر جدا واسه این مسیر می نویسیم که داخل اون کنترلر باید به reqآبجکت، query اضافه کنیم و سپس با استفاده از next اون req رو به کنترلر اصلی بفرستیم.

Router(‘/houses/cheapest”,cheapestControlle,getAllHouseController);// اول برو تو کنترلر مخصوص ارزانترین ها اونجا req رو دستکاری کن بعد برو تو کنترلر اصلی

Const cheapestController = (req,res,next) => {

Req.query.sort(“-price);

Next();

}

به این ترتیب می تونیم انواع مسیرهای مشهور برای سرچ کردن اضافه کنیم.

ریفکتورینگ کدمون:

این کد ها رو داخل کنترلر اصلیمون نوشتیم و کاملا صحیح داره عملیاتش رو انجام می ده. حالا واسه اینکه از این کد واسه مدل های دیگه مون هم استفاده کنیم چیکار کنیم؟

خیلی شیک و مجلسی یه کلاس درست می کنیم که دوتا ورودی می گیره که اولی مدلمون هست و دومی آبجکت queryاست.

Class APIQueryFeatures{

constructor(query,queryString){

//اولی همون HouseModel امون بود که حالا می تونیم انواع مدل ها رو بهش بدیم

this.query = query;

//دومی هم همون req.query مون بود

this.queryString = queryString;

}

// فیلتر کن اون چهارتا فیلد مشهور معروفی که همه جا داریمش

filter(){

const queryObj = { ...this.queryString};

const excludedFields = ['page', 'sort', 'limit', 'fields'];// این چهارتارو می گم

excludedFields.forEach((el) => delete queryObj[el]);

//add $ to query object

let queryStr = JSON.stringify(queryObj);

queryStr = queryStr.replace(

/\b(gte|gt|lte|lt)\b/g,

(match) => `$${match}`

);

this.query = this.query.find(JSON.parse(queryStr));

return this;

}

sort(){

if (this.queryString.sort) {

const sortBy = this.queryString.sort.toString().split(',').join(' ');

this.query = this.query.sort(sortBy);

} else {

this.query = this.query.sort('-createdAt');

}

return this;

}

limitFields(){

if (this.queryString.fields) {

const fields = this.queryString.fields.toString().split(',').join(' ');

this.query = this.query.select(fields);

}

return this;

}

paginate(){

const page = this.queryString.page * 1 || 1;

const limit = this.queryString.limit * 1 || 10;

const skip = (page - 1) * limit;

this.query = this.query.skip(skip).limit(limit);

return this;

}

}

Module.exports = APIQueryFeatures;

خوب حالا یه کلاس داریم که می تونه کلی کارای خفن انجام بده.

واسه فراخوانیش هم بصورت زیر عمل می کنیم.



const features = new APIQueryFeatures(Model.find(), req.query)

.filter()

.sort()

.limitFields()

.paginate();

//این چهارتا متدی که واسه کلاسمون نوشتیم رو زنجیر می کنیم. و بعد فراخوانیش می کنیم. البته مواقعی که نمی خواهیم از یکی از این چهارتا استفاده کنیم می تونیم اونو زنجیر نکنیم.

const document = await features.query;


expressnodequerypaginationfilter
شاید از این پست‌ها خوشتان بیاید