نستعرض في هذا المقال طريقة هيكلة التطبيق الذي عملنا عليه في المقال السابق، والاستعلام عن معلومات متنوعة تضمها قاعدة البيانات العلاقية.
هيكلية التطبيق
لقد كتبنا حتى اللحظة كامل الشيفرة في نفس الملف، لهذا سنحاول الآن إعطاء التطبيق هيكلًا أوضح. لننشئ إذًا المجلدات والملفات وفق الهيكلية التالية:
index.js util config.js db.js models index.js note.js controllers notes.js
أما محتوى الملفات فستكون على النحو التالي:
- الملف "util/config.js": يهتم بالتعامل مع متغيرات البيئة environment variables:
require('dotenv').config() module.exports = { DATABASE_URL: process.env.DATABASE_URL, PORT: process.env.PORT || 3001, }
- الملف "index.js": ويهتم بتهيئة وتشغيل التطبيق:
const express = require('express') const app = express() const { PORT } = require('./util/config') const { connectToDatabase } = require('./util/db') const notesRouter = require('./controllers/notes') app.use(express.json()) app.use('/api/notes', notesRouter) const start = async () => { await connectToDatabase() app.listen(PORT, () => { console.log(`Server running on port ${PORT}`) }) } start()
يختلف تشغيل التطبيق قليلًا عما رأيناه سابقًا، لأننا نريد التأكُّد من نجاح الاتصال بقاعدة البيانات قبل أن يبدأ التطبيق العمل الفعلي.
- الملف "util/d b.js": ويضم الشيفرة التي تُهيئ قاعدة البيانات:
const Sequelize = require('sequelize') const { DATABASE_URL } = require('./config') const sequelize = new Sequelize(DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }); const connectToDatabase = async () => { try { await sequelize.authenticate() console.log('connected to the database') } catch (err) { console.log('failed to connect to the database') return process.exit(1) } return null } module.exports = { connectToDatabase, sequelize }
- الملف "models/note.js": وتُخزّن فيه الملاحظات في النموذج المقابل للجدول الذي سيُحفظ.
const { Model, DataTypes } = require('sequelize') const { sequelize } = require('../util/db') class Note extends Model {} Note.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, content: { type: DataTypes.TEXT, allowNull: false }, important: { type: DataTypes.BOOLEAN }, date: { type: DataTypes.DATE } }, { sequelize, underscored: true, timestamps: false, modelName: 'note' }) module.exports = Note
- الملف "models/index.js": لا يُستخدم حاليًا تقريبًا بسبب وجود نموذج واحد فقط في التطبيق، لكنه سيصبح أكثر فائدةً عندما نبدأ إضافة نماذج جديدة، إذ سيلغي الحاجة إلى إدراج ملفات منفصلة تُعرِّف بقية النماذج:
const Note = require('./note') Note.sync() module.exports = { Note }
- الملف "controllers/notes.js": ويضم الوجهات المرتبطة بالملاحظات، أي مسار التوجيه إلى ملاحظة:
const router = require('express').Router() const { Note } = require('../models') router.get('/', async (req, res) => { const notes = await Note.findAll() res.json(notes) }) router.post('/', async (req, res) => { try { const note = await Note.create(req.body) res.json(note) } catch(error) { return res.status(400).json({ error }) } }) router.get('/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { res.json(note) } else { res.status(404).end() } }) router.delete('/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { await note.destroy() } res.status(204).end() }) router.put('/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { note.important = req.body.important await note.save() res.json(note) } else { res.status(404).end() } }) module.exports = router
هكذا يبدو هيكل التطبيق جيدًا الآن، لكننا نلاحظ أنّ معالجات التوجيه route handlers الذي يتعامل مع ملاحظة واحدة يضم قليلًا من الشيفرات المكررة، فجميعها تبدأ بالسطر الذي يبحث عن الملاحظة التي يتعامل معها:
const note = await Note.findByPk(req.params.id)
لنعيد كتابة الشيفرة على شكل أداة وسطية middleware خاصةٍ بنا، ونطبّقها في معالجات التوجيه:
const noteFinder = async (req, res, next) => { req.note = await Note.findByPk(req.params.id) next() } router.get('/:id', noteFinder, async (req, res) => { if (req.note) { res.json(req.note) } else { res.status(404).end() } }) router.delete('/:id', noteFinder, async (req, res) => { if (req.note) { await req.note.destroy() } res.status(204).end() }) router.put('/:id', noteFinder, async (req, res) => { if (req.note) { req.note.important = req.body.important await req.note.save() res.json(req.note) } else { res.status(404).end() } })
تستقبل معالجات الوجهة ثلاثة معاملات: الأول نصي يُعرّف الوجهة، والثاني الأداة الوسطية noteFinder
المُعرفة مُسبقًا والتي تستخلص الملاحظة من قاعدة البيانات وتضعها في الخاصية note
للكائن req
.
بإمكانك إيجاد الشيفرة الحالية للتطبيق كاملةً في المستودع المخصص على GitHub ضمن الفرع part13-2.
التمرينات 13.5 إلى 13.7
حاول إنجاز التمارين التالية
التمرين 13.5
غيّر هيكل تطبيقك ليشابه المثال السابق أو اتبع هيكليةً أخرى واضحة وملائمة.
التمرين 13.6
قدِّم طريقةً تدعم تغيير عدد الإعجابات بمدوّنة في تطبيقك مستخدمًا العملية "PUT /api/blogs/:id"، إذ ينبغي أن يصل العدد الجديد للإعجابات مع الطلب:
{ likes: 3 }
التمرين 13.7
استخدم أداةً وسطيةً للتحكم المركزي بمعالجة الأخطاء كما فعلنا في القسم 3 كما يمكنك استخدام الأداة الوسطية express-async-errors كما فعلنا في القسم 4. لا تهتم للبيانات المُعادة في سياق رسالة الخطأ.
حتى اللحظة لا تتطلب سوى حالتين في التطبيق معالجةً للأخطاء، هما: إضافة مدوّنة جديدة وتغيير عدد الإعجابات، لذلك تأكد من قدرة معالج الأخطاء على التعامل مع هاتين الحالتين بما يلائمهما.
إدارة المستخدمين
سنضيف تاليًا جدول قاعدة بيانات يُدعى "users" تُخزّن في فيه بيانات مستخدمي التطبيق، كما سنضيف أيضًا وظيفةً لإضافة مستخدمين جدد وآلية تسجيل دخول مبنية على مفاتيح الاستيثاق كما فعلنا في القسم 4، ولكي نبسط العمل، سنُعدِّل ما أنجزناه سابقًا كي يكون لجميع المستخدمين كلمة المرور ذاتها وهي "secret".
محتوى الملف "models/user.js" الذي يُعرّف المستخدمين واضحٌ تمامًا:
const { Model, DataTypes } = require('sequelize') const { sequelize } = require('../util/db') class User extends Model {} User.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, username: { type: DataTypes.STRING, unique: true, allowNull: false }, name: { type: DataTypes.STRING, allowNull: false }, }, { sequelize, underscored: true, timestamps: false, modelName: 'user' }) module.exports = User
سيكون حقل اسم المستخدم "username" ذا قيم فريدة، لكن وعلى الرغم من إمكانية جعله المفتاح الرئيسي للجدول، إلا أننا قررنا أن ننشئ حقلًا field منفصلًا "id" ذا قيم صحيحة ليكون المفتاح الرئيسي.
سيتوسع الملف "models/index.js" قليلًا:
const Note = require('./note') const User = require('./user') Note.sync() User.sync() module.exports = { Note, User }
لا يضم الملف "controllers/users.js" الذي يحتوي معالجات الوجهة التي تهتم بإنشاء مستخدمين جدد أي شيء مهم حاليًا سوى عرض كل المستخدمين:
const router = require('express').Router() const { User } = require('../models') router.get('/', async (req, res) => { const users = await User.findAll() res.json(users) }) router.post('/', async (req, res) => { try { const user = await User.create(req.body) res.json(user) } catch(error) { return res.status(400).json({ error }) } }) router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id) if (user) { res.json(user) } else { res.status(404).end() } }) module.exports = router
أما معالج الوجهة الذي يتحكم بتسجيل الدخول (الملف "controllers/login.js") فسيكون على النحو التالي:
const jwt = require('jsonwebtoken') const router = require('express').Router() const { SECRET } = require('../util/config') const User = require('../models/user') router.post('/', async (request, response) => { const body = request.body const user = await User.findOne({ where: { username: body.username } }) const passwordCorrect = body.password === 'secret' if (!(user && passwordCorrect)) { return response.status(401).json({ error: 'invalid username or password' }) } const userForToken = { username: user.username, id: user.id, } const token = jwt.sign(userForToken, SECRET) response .status(200) .send({ token, username: user.username, name: user.name }) }) module.exports = router
سيُرفق اسم المستخدم وكلمة المرور مع طلب POST، ويُستخلص الكائن المتعلق باسم المستخدم أولًا من قاعدة البيانات باستخدام التابع findOne العائد للنموذج User
:
const user = await User.findOne({ where: { username: body.username } })
يمكنك أن تلاحظ من خلال الطرفية أن تعليمة SQL المتعلقة باستدعاء التابع السابق هي:
SELECT "id", "username", "name" FROM "users" AS "User" WHERE "User". "username" = 'mluukkai';
إن وُجد المستخدم وكانت كلمة المرور صحيحة (وهي "secret" لجميع المستخدمين)، يُعاد مفتاح الاستيثاق "jsonwebtoken" متضمنًا معلومات المستخدم مع الاستجابة. لهذا سنُثبّت الاعتمادية "jsonwebtoken":
npm install jsonwebtoken
سيتوسع الملف "index.js" قليلًا:
const notesRouter = require('./controllers/notes') const usersRouter = require('./controllers/users') const loginRouter = require('./controllers/login') app.use(express.json()) app.use('/api/notes', notesRouter) app.use('/api/users', usersRouter) app.use('/api/login', loginRouter)
يمكنك الحصول على شيفرة التطبيق بوضعها الحالي من المستودع المخصص على GitHub ضمن الفرع part13-3.
الاتصال بين الجداول
يمكن الآن إضافة مستخدمين إلى التطبيق، كما يمكن للمستخدمين تسجيل الدخول، لكنها ليست ميزات مفيدة جدًا في وضعها الحالي، لذلك سنضيف ميزات لا يمكن للمستخدم الاستفادة منها ما لم يُسجل دخوله مثل إضافة ملاحظة جديدة، إذ ترتبط هذه الملاحظة بالمستخدم الذي أنشأها، ولهذا لا بُدّ من إضافة مفتاح خارجي foreign key إلى جدول "notes".
يمكن تعريف المفتاح الخارجي عند استخدام مكتبة Sequelize بتعديل الملف "models/index.js" على النحو التالي:
const Note = require('./note') const User = require('./user') User.hasMany(Note) Note.belongsTo(User) Note.sync({ alter: true }) User.sync({ alter: true }) module.exports = { Note, User }
وهكذا نكون قد عرّفنا علاقة واحد-إلى-متعدد one-to-many تصل بين جدولي المستخدمين "users" والملاحظات "notes"، كما عدّلنا الخيارات في استدعاءات sync
لتتطابق جداول قاعدة البيانات مع التغييرات التي حدثت على تعريفات النموذج. سيبدو مخطط قاعدة البيانات على شاشة الطرفية على النحو التالي:
username=> \d users Table "public.users" Column | Type | Collation | Nullable | Default ----------+------------------------+-----------+----------+----------------------------------- id | integer | not null | nextval('users_id_seq'::regclass) username | character varying(255) | | not null | name | character varying(255) | | not null | Indexes: "users_pkey" PRIMARY KEY, btree (id) Referenced by: TABLE "notes" CONSTRAINT "notes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE SET NULL username=> \d notes Table "public.notes" Column | Type | Collation | Nullable | Default -----------+--------------------------+-----------+----------+----------------------------------- id | integer | not null | nextval('notes_id_seq'::regclass) content | text | | not null | important | boolean | | | | date | timestamp with time zone | | | | user_id | integer | | | | Indexes: "notes_pkey" PRIMARY KEY, btree (id) Foreign-key constraints: "notes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE SET NULL
يشير المفتاح الخارجي user_id
الذي أُنشئ في الجدول notes
إلى أسطر في الجدول users
.
لنربط الآن كل ملاحظة جديدة بالمستخدم الذي أنشأها، ولكن قبل تقديم الأمر (ربط الملاحظة بمفتاح استيثاق المستخدم الذي استخدمه لتسجيل دخوله)، لا بُد من كتابة الملاحظة التي ترتبط بالمستخدم الأول الذي يُعثر عليه يدويًا في الشيفرة:
router.post('/', async (req, res) => { try { const user = await User.findOne() const note = await Note.create({...req.body, userId: user.id}) res.json(note) } catch(error) { return res.status(400).json({ error }) } })
انتبه إلى وجود العمود user_id
في جدول الملاحظات "notes" على مستوى قاعدة البيانات، ويُشار إلى اسم كل صف في قاعدة البيانات بالطريقة التقليدية للمكتبة Sequelize، وذلك بكتابته على نقيض أسلوب سنام الجمل "userId"، أي كما تُكتب تمامًا في الشيفرة (حروف صغيرة).
من السهل تنفيذ استعلامات مشتركة في Sequelize، لهذا سنغيّر الوجهة التي تعيد كل المستخدمين لتعرض كل ملاحظات المستخدم:
router.get('/', async (req, res) => { const users = await User.findAll({ include: { model: Note } }) res.json(users) })
يُنفَّذ الاستعلام المشترك باستخدام الخيار include مثل معامل استعلام، أما تعليمة SQL المولّدة من الاستعلام، فستُطبع على شاشة الطرفية على النحو التالي:
SELECT "User". "id", "User". "username", "User". "name", "Notes". "id" AS "Notes.id", "Notes". "content" AS "Notes.content", "Notes". "important" AS "Notes.important", "Notes". "date" AS "Notes.date", "Notes". "user_id" AS "Notes.UserId" FROM "users" AS "User" LEFT OUTER JOIN "notes" AS "Notes" ON "User". "id" = "Notes". "user_id";
ستبدو النتيجة النهائية كما تتوقع:
الإضافة الملائمة للملاحظات
لنحاول تغيير طريقة إضافة الملاحظات لتعمل بالطريقة التي عملت بها من قبل، بحيث ينجح إنشاء الملاحظات فقط إذا حمل طلب إنشائها مفتاح استيثاق صحيح عند تسجيل الدخول. تُخزّن بعدها الملاحظة ضمن قائمة الملاحظات التي أنشأها المستخدم المُعرّف بواسطة مفتاح الاستيثاق:
const tokenExtractor = (req, res, next) => { const authorization = req.get('authorization') if (authorization && authorization.toLowerCase().startsWith('bearer ')) { try { req.decodedToken = jwt.verify(authorization.substring(7), SECRET) } catch{ res.status(401).json({ error: 'token invalid' }) } } else { res.status(401).json({ error: 'token missing' }) } next() } router.post('/', tokenExtractor, async (req, res) => { try { const user = await User.findByPk(req.decodedToken.id) const note = await Note.create({...req.body, userId: user.id, date: new Date()}) res.json(note) } catch(error) { return res.status(400).json({ error }) } })
يُستخلص مفتاح الاستيثاق من ترويسة الطلب ويفُك تشفيره ويوضع ضمن الكائن req
بواسطة الأداة الوسطية tokenExtractor
. يُضاف زمن الإنشاء إلى الحقل date
أيضًا عند إنشاء الملاحظة.
ضبط الواجهة الخلفية
تعمل الواجهة الخلفية حتى اللحظة بنفس الطريقة التي تعمل بها في نسخة القسم 4 من التطبيق ماعدا فكرة معالجة الأخطاء. لنغيّر الآن وجهات إحضار جميع الملاحظات وجميع المستخدمين قليلًا قبل توسيع هذه الواجهة.
سنضيف إلى كل ملاحظة بعض المعلومات المتعلقة بالمستخدم الذي أنشأها:
router.get('/', async (req, res) => { const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: User, attributes: ['name'] } }) res.json(notes) })
كنا قد قيّدنا سابقًا القيم التي يأخذها الحقل المطلوب، إذ كنا نعيد جميع حقول البيانات الخاصة بكل ملاحظة بما في ذلك اسم المستخدم "name" الذي يرتبط بالملاحظة لكن باستثناء حقل المعرّف الفريد للمستخدم "userId".
لنجرِ التغيير ذاته على الوجهة route التي تحضر جميع المستخدمين والملاحظات وذلك بإزالة الحقل userId
غير الضروري من الملاحظات المرتبطة بمستخدم معين على النحو التالي:
router.get('/', async (req, res) => { const users = await User.findAll({ include: { model: Note, attributes: { exclude: ['userId'] } } }) res.json(users) })
يمكنك الحصول على شيفرة التطبيق بوضعها الحالي من المستودع المخصص على GitHub ضمن الفرع part13-4.
قليل من الانتباه إلى تعريفات النماذج
رغم وجود العمود user_id
، إلا أننا لم نغيّر النموذج الذي يعرّف الملاحظات، ولكن يمكننا مع ذلك إضافة مستخدم إلى كائن الملاحظة:
const user = await User.findByPk(req.decodedToken.id) const note = await Note.create({ ...req.body, userId: user.id, date: new Date() })
يعود السبب وراء ذلك إلى أننا لم نحدّد وجود علاقة واحد-إلى-متعدد في الاتصال بين جدولي المستخدمين "users" والملاحظات "notes" ضمن الملف "models/index.js":
const Note = require('./note') const User = require('./user') User.hasMany(Note) Note.belongsTo(User) // ...
تُنشئ المكتبة Sequelize تلقائيًا سمةً تُدعى userId
في النموذج Note
تمنح وصولًا إلى العمود user_id
عندما يُشار إليه. وتذكّر أنه يمكنك إنشاء ملاحظة باستخدام التابع build:
const user = await User.findByPk(req.decodedToken.id) // إنشاء ملاحظة دون تخزينها بعد const note = Note.build({ ...req.body, date: new Date() }) // للملاحظة المُنشأة userId وضع المعرف الفريد للمستخدم في خاصية note.userId = user.id // تخزين كائن الملاحظة في قاعدة البيانات await note.save()
هكذا نرى صراحة أن userId
هي سمة attribute لكائن الملاحظات، وقد كان بالإمكان تعريف النموذج على النحو التالي للحصول على النتيجة ذاتها:
Note.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, content: { type: DataTypes.TEXT, allowNull: false }, important: { type: DataTypes.BOOLEAN }, date: { type: DataTypes.DATE }, userId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, } }, { sequelize, underscored: true, timestamps: false, modelName: 'note' }) module.exports = Note
لا يوجد داعٍ للتعريف على مستوى الصنف في النموذج كما فعلنا سابقًا:
User.hasMany(Note) Note.belongsTo(User)
وبدلًا من ذلك يمكننا تحقيق الأمر بهذه الطريقة، ولا بُد من استخدام إحدى هاتين الطريقتين وإلا لن تُدرك مكتبة Sequelize كيف يتصل الجدولين ببعضهما على مستوى الشيفرة.
التمرينات 13.8 إلى 13.11
حاول إنجاز التمارين الآتية:
التمرين 13.8
زِد دعم التطبيق لمستخدميه، إذ لا بُد أن يضم جدول المستخدمين الحقول التالية إضافةً إلى الحقل "ID":
- name: ذو قيمة نصية (لا يمكن أن يكون فارغًا).
- username: ذو قيمة نصية (لا يمكن أن يكون فارغًا).
وعلى خلاف ما أوردنا في الشروحات النظرية، لا تمنع مكتبة Sequelize حاليًا إنشاء البصمتين الزمنيتين create_dat و update_dat لجدول المستخدمين.
يمكن إعطاء كلمة المرور نفسها لجميع المستخدمين، وكذلك اختيار طريقة التحقق من كلمة المرور كما في القسم 4، وعليك أيضًا إنجاز الوجهات التالية:
- "POST api/users": لإضافة مستخدم جديد.
- "GET api/users": عرض جميع المستخدمين.
- "PUT api/users/:username": لتغيير اسم المستخدم وليس المعرّف "id".
تأكد من إدراج البصمات الزمنية تلقائيًا من قِبل مكتبة Sequelize وعلى النحو الصحيح عند إضافة مستخدم أو تغيير اسمه.
التمرين 13.9
تزودنا مكتبة Sequelize بمجموعة قواعد تحقق معرّفةٌ مسبقًا للتأكد من قيم حقول النموذج تُنفّذها قبل تخزين الكائنات في قاعدة البيانات. ولأننا نريد تغيير سياسة إنشاء مستخدم جديد تتطلب أن يكون البريد الإلكتروني المُدخل صحيحًا كما هو حال اسم المستخدم، أنجِز طريقةً للتحقق من ذلك أثناء إنشاء المستخدم.
عدّل أيضًا بالأداة الوسطية المستخدمة في معالجة الأخطاء لتعرض وصفًا أكثر وضوحًا لرسالة الخطأ مثل استخدام رسائل خطأ Sequelize على سبيل المثال.
{ "error": [ "Validation isEmail on username failed" ] }
التمرين 13.10
وسّع التطبيق لكي تربط كل مستخدم سجّل دخوله بنجاح عبر مفتاح الاستيثاق بكل مدونة يضيفها. لا بُد من تقديم وصلة endpoint تسجيل دخول "POST /api/login" تُعيد مفتاح الاستيثاق.
التمرين 13.11
تأكد أن المستخدم قادرٌ فقط على حذف المدونات التي يضيفها.
التمرين 13.12
عدّل الوجهات كي تكون قادرًا على استخلاص جميع المدوّنات والمستخدمين كي تعرض كل مدوّنة المستخدم الذي أضافها وأن يعرض كل مستخدم المدوّنات التي أضافها.
استعلامات أوسع
لا يزال تطبيقنا حتى اللحظة بسيطًا من ناحية الاستعلامات التي يجريها، إذ اقتصر البحث على صف واحد بناءً على مفتاح رئيسي من خلال التابع "findByPk"، أو البحث عن كل الصفوف باستخدام التابع "findAll". كانت هذه الاستعلامات كافيةً بالنسبة للواجهة الأمامية في نسخة القسم 5 من التطبيق، لكننا سنوّسع الواجهة الخلفية لنتمكن من تنفيذ استعلامات أعقد قليلًا.
لننجز أولًا طريقة للحصول على ملاحظات مصنّفة على أنها مهمة أو غير مهمة فقط، باستخدام معامل الاستعلام important
:
router.get('/', async (req, res) => { const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: user, attributes: ['name'] }, where: { important: req.query.important === "true" } }) res.json(notes) })
وهكذا ستتمكن الواجهة الخلفية من استخلاص الملاحظات المهمة عند وصول الطلب:
http://localhost:3001/api/notes?important=true
والملاحظات غير المهمة عند وصول الطلب:
http://localhost:3001/api/notes?important=false
يتضمن استعلام SQL الذي ولّدته Sequelize العبارة WHERE
، التي تُرشّح الصفوف المُعادة في الحالة الطبيعية:
SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id" WHERE "note". "important" = true;
وبالطبع لن ينفع إنجاز الأمر بهذه الطريقة ما لم يهتم الاستعلام بأهمية الملاحظة، كأن يكون على النحو التالي:
http://localhost:3001/api/notes
يمكن تدارك الموضوع بطرق عدة، أولها -وقد لا تكون الطريقة الأفضل- على النحو التالي:
const { Op } = require('sequelize') router.get('/', async (req, res) => { let important = { [Op.in]: [true, false] } if ( req.query.important ) { important = req.query.important === "true" } const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: user, attributes: ['name'] }, where: { important } }) res.json(notes) })
يُخزّن الكائن important
الآن شرط الاستعلام، وسيكون الشكل الافتراضي لهذا الاستعلام على النحو التالي:
where: { important: { [Op.in]: [true, false] } }
أي أن العمود قد يحمل إحدى القيمتين true
أو false
باستخدام العامل Op.in، وهو أحد عوامل Sequelize العديدة؛ فإذا حددنا قيمة للمعامل req.query.important
، سيتغير الاستعلام ليصبح بأحد الشكلين التاليين:
where: { important: true }
أو
where: { important: false }
بناءً على قيمة معامل الاستعلام.
يمكن زيادة القدرة الوظيفية للتطبيق بالسماح للمستخدم بتخصيص كلمة مفتاحية لإحضار البيانات، إذ يعيد الطلب التالي:
http://localhost:3001/api/notes?search=database
الملاحظات التي تشير إلى الكلمة "database". كما يعيد الطلب التالي:
http://localhost:3001/api/notes?search=javascript&important=true
جميع الملاحظات التي حُددت أنها مهمة "important" وتشير إلى الكلمة "javascript".
سيكون تنفيذ الأمر على النحو التالي:
router.get('/', async (req, res) => { let important = { [Op.in]: [true, false] } if ( req.query.important ) { important = req.query.important === "true" } const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: user, attributes: ['name'] }, where: { important, content: { [Op.substring]: req.query.search ? req.query.search : '' } } }) res.json(notes) })
يوّلد التابع Op.substring الاستعلام الذي نريده باستخدام كلمة "LIKE"، فإذا أنشأت الاستعلام التالي مثلًا:
http://localhost:3001/api/notes?search=database&important=true
فسيولّد استعلام SQL ما نتوقعه تمامًا:
SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id" WHERE "note". "important" = true AND "note". "content" LIKE '%database%';
لكن لا تزال هناك ثغرة مزعجة تتمثل في أن الطلب التالي:
http://localhost:3001/api/notes
والذي يعني أننا نريد الحصول على جميع الملاحظات، سيتسبب في وجود عبارة WHERE
غير الضرورية في الاستعلام، والتي قد تؤثر في فعاليته وفقًا لآلية عمل محرك قاعدة البيانات:
SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id" WHERE "note". "important" IN (true, false) AND "note". "content" LIKE '%%';
لنحسّن الشيفرة لكي تُستخدم عبارة WHERE
عند الحاجة فقط:
router.get('/', async (req, res) => { const where = {} if (req.query.important) { where.important = req.query.important === "true" } if (req.query.search) { where.content = { [Op.substring]: req.query.search } } const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: user, attributes: ['name'] }, where }) res.json(notes) })
وإذا احتوى الطلب على شرط للبحث مثل:
http://localhost:3001/api/notes?search=database&important=true
سيتولد استعلام يضم عبارة WHERE
:
SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id" WHERE "note". "important" = true AND "note". "content" LIKE '%database%';
وإذا لم يحتوي شرطًا، فلن يكون لوجود WHERE
حاجة:
SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id";
يمكنك الحصول على شيفرة التطبيق بوضعها الحالي من المستودع المخصص على GitHub ضمن الفرع part13-5.
التمرينات 13.13 إلى 13.16
حاول إنجاز التمرينات التالية:
التمرين 13.13
قدِّم آليةً لترشيح filtering النتائج من خلال كلمة مفتاحية ضمن الوجهة التي تعيد كل المدوّنات، وينبغي أن تعمل الآلية على النحو التالي:
-
"GET /api/blogs?search=react": تعيد كل المدونات التي تضم الكلمة "react" في الحقل علمًا أن البحث حساس لحالة الأحرف.
-
"GET /api/blogs": يعيد جميع المدوّنات.
يمكنك الاستفادة من عوامل Sequelize لتنفيذ التمرين.
التمرين 13.14
وسّع آلية الترشيح للبحث عن كلمة مفتاحية في أحد الحقلين "field" أو "author"، إذ سيعيد الاستعلام:
GET /api/blogs?search=jami
كل المدوّنات التي تضم الكلمة في أحد الحقلين "field" أو "author".
التمرين 13.15
عدّل وجهات المدوّنات لتعيد المدوّنات بناءً على عدد الإعجابات بترتيب تنازلي. اطلع من خلال توثيق Sequelize على تعليمات ترتيب نتائج الاستعلام.
التمرين 13.16
أنشئ الوجهة "api/authors/" كي تعيد عدد المدونات التي أضافها المؤلف والعدد الكلي للإعجابات. قدّم العملية على مستوى قاعدة البيانات. قد تحتاج إلى وظيفة التجميع group by ودالة التجميع sequelize.fn.
قد تبدو المعلومات المُعادة بتنسيق JSON على غرار المثال الآتي:
[ { author: "Jami Kousa", articles: "3", likes: "10" }, { author: "Kalle Ilves", articles: "1", likes: "2" }, { author: "Dan Abramov", articles: "1", likes: "4" } ]
مهمة لعلامة إضافية: رتِّب البيانات المُعادة بناءً على عدد الإعجابات، على أن تُنفّذ عملية الترتيب من خلال استعلام قاعدة البيانات.
ترجمة -وبتصرف- للفصل Join tables and queries من سلسلة Deep Dive Into Modern Web Development
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.