اذهب إلى المحتوى

العمل مع قواعد بيانات علاقية باستخدام Sequelize


ابراهيم الخضور

نستكشف في هذا المقال تطبيقات نود Node التي تستخدم قواعد بيانات علاقية، وسنبني خلال تقدمنا في المقال واجهةً خلفيةً تستخدم قاعدة بيانات علاقية ليتعامل معها تطبيق الملاحظات الذي عملنا عليه سابقًا. ولإكمال هذا المقال، لا بُدّ من تعميق معرفتك بقواعد البيانات العلاقية ولغة SQL. يمكنك الاطلاع على سلسلة مقالات المرجع المتقدم إلى SQL في أكاديمية حسوب، وكذلك الاطلاع على توثيق لغة SQL في موسوعة حسوب.

ستجد 24 تمرينًا في هذا المقال وعليك أن تنجزها جميعًا لتكمل الدورة التعليمية، تُسلّم الحلول إلى منظومة تسليم الحلول كما هو الحال في الأقسام السابقة ما عدا الأقسام من 0 إلى 7 والتي تُسلّم حلول التمارين فيها إلى مكان آخر.

إيجابيات وسلبيات قواعد بيانات المستندات

استخدمنا قاعدة البيانات MongoDB في جميع المقال السابقة، وهي قاعدة بيانات مستندات، ومن أهم ميزاتها أنها لا تملك أي مخططات، أي أنّ لها معرفةٌ محدودةٌ بطبيعة البيانات المخزنة في كل مجموعة. يتواجد مخطط قاعدة البيانات schema في شيفرة البرنامج فقط، إذ يفسِّر البيانات بطريقة معينة، كأن يحدد أن بعض الحقول هي مراجعٌ لكائنات في مجموعةٍ أخرى.

رأينا في المثال التطبيقي الذي يضم قاعدة بيانات تخزن الملاحظات والمستخدمين (في القسمين 3 و 4)، إذ تُخزِّن الملاحظات "notes" على النحو التالي:

[
  {
    "_id": "600c0e410d10256466898a6c",
    "content": "HTML is easy"
    "date": 2021-01-23T11:53:37.292+00:00,
    "important": false
    "__v": 0
  },
  {
    "_id": "600c0edde86c7264ace9bb78",
    "content": "CSS is hard"
    "date": 2021-01-23T11:56:13.912+00:00,
    "important": true
    "__v": 0
  },
]

وتُخزّن أيضًا بيانات المستخدمين في المجموعة "users" على النحو التالي:

[
  {
    "_id": "600c0e410d10256466883a6a",
    "username": "mluukkai",
    "name": "Matti Luukkainen",
    "passwordHash" : "$2b$10$Df1yYJRiQuu3Sr4tUrk.SerVz1JKtBHlBOARfY0PBn/Uo7qr8Ocou",
    "__v": 9,
    notes: [
      "600c0edde86c7264ace9bb78",
      "600c0e410d10256466898a6c"
    ]
  },
]

تعرِف MongoDB نوع البيانات في الحقول التي تُخزِّن كيانات البيانات، لكنها لا تعرف إلى أي مجموعة من الكيانات يشير المعرِّف المميز الذي سجّله المستخدم، ولا تهتم أيضًا بالحقول التي تمتلكها الكيانات المُخزنة في مجموعات البيانات؛ لهذا تترك MongoDB أمر التحقق من صحة المعلومات المخزّنة في قاعدة البيانات إلى المبرمج.

هناك طبعًا إيجابيات وسلبيات لعدم وجود مخطط لقاعدة البيانات، وإحدى الإيجابيات هي المرونة التي تضيفها الميزة اللا إدارية في مخطط البيانات، إذ سيسرّع انتفاء الحاجة إلى تعريف مخطط على مستوى قاعدة البيانات من عمل المطوّر في حالات عدة، كما أنه أسهل من تعريف وتعديل المخطط في حالات أخرى؛ في حين تتعلق المشاكل الناجمة عن عدم وجود المخطط غالبًا بسهولة الوقوع في الأخطاء، فكل شيء ملقىً على عاتق المبرمج، وليس لقاعدة البيانات في هذه الحالة طريقةً للتحقق من نزاهتها honest، أي إذا احتوت كل الحقول الإجبارية على القيم الصحيحة، أو إذا أشارت الحقول ذات النوع المرجعي إلى كيانات من النوع الصحيح عمومًا وهكذا.

تعتمد قاعدة البيانات العلاقية التي ستكون محور حديثنا في هذا المقال بشدة على وجود مخطط، وتكاد تكون الإيجابيات والسلبيات في استخدامها على نقيض تلك المتعلقة بقاعدة بيانات المستندات.

يعود السبب الرئيسي في استخدام قاعدة البيانات MongoDB تحديدًا في الأقسام السابقة إلى طبيعتها التي لا تحتاج مخططات، الأمر الذي يسهّل استخدامها بالنسبة لذوي المعرفة المحدودة بقواعد البيانات العلاقية، لكن نفترض بالنسبة لكل الحالات التي عرضناها في منهاجنا أن القواعد العلاقية هي الأنسب.

قاعدة بيانات التطبيق

نحتاج في تطبيقنا إلى قاعدة بيانات علاقية، وهناك خيارات عديدة، لكننا سنستخدم حاليًا الحل الأكثر شعبيةً والمفتوح المصدر PostgreSQL؛ ويمكنك تثبيت Postgres (هكذا تُدعى قاعدة البيانات هذه غالبًا) على جهازك إذا أردت؛ والأسهل من هذا هو استخدامها بمثابة خدمة سحابية، مثل ElephantSQL. بإمكانك أيضًا الاستفادة مما جاء في جزئية سابقة من السلسلة لاستخدام Postgres محليًا من خلال دوكر Docker.

اخترنا في هذا المقال الاستفادة من إمكانية إنشاء قاعدة بيانات Postgres للتطبيق على خدمة "Heroku" السحابية التي ألفنا العمل معها في المقالين 3 و4. سنبني في الجزء النظري من هذا المقال نسخةً مرتبطة بقاعدة البيانات Postgres عن الواجهة الخلفية لتطبيق تخزين الملاحظات الذي بنيناه في المقالين 3 و4.

لننشئ أولًا مجلدًا مناسبًا ضمن تطبيق Heroku ونضيف إليه قاعدة بيانات، ثم نستخدم الأمر heroku config للحصول على "السلسلة النصية connect string" اللازمة للاتصال مع القاعدة:

heroku create
# heroku يعيد اسم التطبيق للتطبيق الذي أنشأته في 

heroku addons:create heroku-postgresql:hobby-dev -a <app-name>
heroku config -a <app-name>
=== cryptic-everglades-76708 Config Vars
DATABASE_URL: postgres://<username>:<password>@<host-of-postgres-addon>:5432/<db-name>

يُعد الوصول إلى قاعدة البيانات مباشرةً من الأمور الأساسية التي ينبغي الانتباه إليها تحديدًا في قواعد البيانات العلاقية، نظرًا لوجود أساليب عدة لتنفيذ الأمر، ووجود عدة واجهات مستخدم رسومية، مثل pgAdmin، لكن سنستخدم أداة سطر الأوامر psql الخاصة بالقاعدة Postgres.

يمكن الوصول إلى قاعدة البيانات من خلال تنفيذ أمر psql على خادم Heroku على النحو التالي (لاحظ كيف تعتمد معاملات الأمر على عنوان url للاتصال بقاعدة بيانات Heroku):

heroku run psql -h <host-of-postgres-addon> -p 5432 -U <username> <dbname> -a <app-name>

بعد إدخال كلمة المرور، سننفذ أمر psql الأساسي وهو d\، الذي يخبرك بمحتوى قاعدة البيانات:

Password for user <username>:
psql (13.4 (Ubuntu 13.4-1.pgdg20.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

username=> \d
Did not find any relations.

وكما ترى، لا يوجد شيء حاليًا في قاعدة البيانات. لننشئ إذًا جدولًا للملاحظات:

CREATE TABLE notes (
    id SERIAL PRIMARY KEY,
    content text NOT NULL,
    important boolean,
    date time
);

يمكننا ملاحظة بعض النقاط:

  • يُعرِّف العمود id مفتاحًا أساسيًا primary key، ويعني ذلك أن قيم هذا العمود لا بُدّ أن تكون فريدةً لكل سطر في الجدول ولا ينبغي أن تكون قيمتها فارغة.
  • يُعرّف نوع العمود id على أنه تسلسلي SERIAL، وهو ليس نوع حقيقي بل تمثيل لعمود ذي قيم صحيحة تعيّن Postgres قيمه الفريدة تلقائيًا وتزيد هذه القيمة بمقدار "واحد" عند إنشاء سطر جديد.
  • يكون العمود المُسمى content من النوع النصي ويُعُرِّف كي تعيّن له قيمةٌ في كل سطر.

لنلق نظرةً على الوضع الآن من خلال الطرفية، ولننفّذ أولًا الأمر d\ الذي يعرض جداول قاعدة البيانات:

username=> \d
                 List of relations
 Schema | Name | Type | Owner
--------+--------------+----------+----------------
 public | notes | table | username
 public | notes_id_seq | sequence | username
(2 rows)

إذ تُنشئ Postgres إضافةً إلى الجدول notes جدولًا فرعيًا يُدعى notes_id_seq يتتبع القيم المُسندة إلى العمود id عند إنشاء ملاحظة جديدة. وبتنفيذ الأمر d notes\ يمكننا رؤية طريقة تعريف الجدول notes:

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 | time without time zone | | | |
Indexes:
    "notes_pkey" PRIMARY KEY, btree (id)

إذًا، للعمود id قيم افتراضية تُستخرج من تنفيذ الدالة الداخلية nextval في Postgres. لنُضِف بعض المحتوى إلى الجدول:

insert into notes (content, important) values ('Relational databases rule the world', true);
insert into notes (content, important) values ('MongoDB is webscale', false);

لنرى الآن كيف يبدو المحتوى الذي أضفناه:

username=> select * from notes;
 id | content | important | date
----+-------------------------------------+-----------+------
  1 | relational databases rule the world | t |
  2 | MongoDB is webscale | f |
(2 rows)

إذا حاولنا تخزين البيانات في قاعدة البيانات دون العودة إلى مخطط، فلن ينجح الأمر، لأنّ قيم الأعمدة الإجبارية لا بُد أن تكون موجودةً:

username=> insert into notes (important) values (true);
ERROR: null value in column "content" of relation "notes" violates not-null constraint
DETAIL: Failing row contains (9, null, t, null).

ولا يمكن أن تكون قيمة العمود من النوع الخاطئ:

username=> insert into notes (content, important) values ('only valid data can be saved', 1);
ERROR: column "important" is of type boolean but expression is of type integer
LINE 1: ...tent, important) values ('only valid data can be saved', 1); ^

ولا يمكن القبول بأعمدة غير موجودة في المخطط:

username=> insert into notes (content, important, value) values ('only valid data can be saved', true, 10);
ERROR: column "value" of relation "notes" does not exist
LINE 1: insert into notes (content, important, value) values ('only ...

سننتقل تاليًا إلى طريقة الدخول إلى قاعدة البيانات من التطبيق.

تطبيق Node يستخدم قاعدة بيانات علاقية

لنشغّل التطبيق كما جرت العادة من خلال التعليمة npm init ونثبّت "nodemon" على أنه اعتمادية تطوير development dependency وكذلك اعتماديات زمن التشغيل التالية:

npm install express dotenv pg sequelize

نجد من بين هذه الاعتماديات sequelize، وهي المكتبة التي يمكننا من خلالها استخدام Postgres؛ وتنتمي هذه المكتبة إلى مكتبات الربط العلاقي للكائنات Object relational mapping -أو اختصارًا ORM-، التي تسمح بتخزين كائنات جافا سكربت JavaScript في قاعدة بيانات علاقية دون استخدام لغة SQL بحد ذاتها وبصورةٍ مشابهة للمكتبة "Mongoose" التي استخدمناها مع قاعدة البيانات MongoDB.

لنختبر الآن قدرتنا على الاتصال الناجح بقاعدة البيانات، لهذا أنشئ الملف index.js وأضف إليه الشيفرة التالية:

require('dotenv').config()
const { Sequelize } = require('sequelize')

const sequelize = new Sequelize(process.env.DATABASE_URL, {
  dialectOptions: {
    ssl: {
      require: true,
      rejectUnauthorized: false
    }
  },
})

const main = async () => {
  try {
    await sequelize.authenticate()
    console.log('Connection has been established successfully.')
    sequelize.close()
  } catch (error) {
    console.error('Unable to connect to the database:', error)
  }
}

main()

ينبغي تخزين سلسلة الاتصال النصية connect string بقاعدة البيانات التي أظهرها الأمر heroku config في ملفٍ له الامتداد env.، وينبغي أن تكون مشابهةً لما يلي:

$ cat .env
DATABASE_URL=postgres://<username>:<password>@ec2-54-83-137-206.compute-1.amazonaws.com:5432/<databasename>

لنتحقق من نجاح الاتصال:

$ node index.js
Executing (default): SELECT 1+1 AS result
Connection has been established successfully.

إذا عمل الاتصال، يمكننا حينها تشغيل الاستعلام الأول. لنعدّل البرنامج على النحو التالي:

require('dotenv').config()
const { Sequelize, QueryTypes } = require('sequelize')

const sequelize = new Sequelize(process.env.DATABASE_URL, {
  dialectOptions: {
    ssl: {
      require: true,
      rejectUnauthorized: false
    }
  },
});

const main = async () => {
  try {
    await sequelize.authenticate()

   const notes = await sequelize.query("SELECT * FROM notes", { type: QueryTypes.SELECT })
   console.log(notes)
   sequelize.close()
 } catch (error) {
    console.error('Unable to connect to the database:', error)
  }
}

main()

ينبغي أن يطبع تنفيذ البرنامج ما يلي:

Executing (default): SELECT * FROM notes
[
  {
    id: 1,
    content: 'Relational databases rule the world',
    important: true,
    date: null
  },
  {
    id: 2,
    content: 'MongoDB is webscale',
    important: false,
    date: null
  }
]

وعلى الرغم من كون Sequelize مكتبة ربط علاقي للكائنات ORM، أي أنها تحتاج إلى قليلٍ فقط من شيفرة SQL التي تكتبها بنفسك، فقد استخدمنا شيفرة SQL مباشرةً مع تابع Sequelize الذي يُدعى query.

طالما أنّ كل شيء يعمل على ما يرام، لنحوّل التطبيق إلى تطبيق ويب.

require('dotenv').config()
const { Sequelize, QueryTypes } = require('sequelize')

const express = require('express')
const app = express()

const sequelize = new Sequelize(process.env.DATABASE_URL, {
  dialectOptions: {
    ssl: {
      require: true,
      rejectUnauthorized: false
    }
  },
});


app.get('/api/notes', async (req, res) => {
 const notes = await sequelize.query("SELECT * FROM notes", { type: QueryTypes.SELECT })
 res.json(notes)
})
const PORT = process.env.PORT || 3001
app.listen(PORT, () => {
 console.log(`Server running on port ${PORT}`)
})

يبدو أن التطبيق يعمل جيدًا. لنتحول الآن إلى استخدام Sequelize بدلًا من SQL.

النماذج في Sequelize

يُمثَّل كل جدول من جداول قاعدة البيانات عند استخدام Sequelize بنموذج model، والذي يُعدُّ صنف JavaScript الخاص بالجدول. لنعرِّف الآن النموذج Note المتعلق بالجدول notes من التطبيق وذلك بتغيير الشيفرة على النحو التالي:

require('dotenv').config()
const { Sequelize, Model, DataTypes } = require('sequelize')
const express = require('express')
const app = express()

const sequelize = new Sequelize(process.env.DATABASE_URL, {
  dialectOptions: {
    ssl: {
      require: true,
      rejectUnauthorized: false
    }
  },
});

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'
})

app.get('/api/notes', async (req, res) => {
  const notes = await Note.findAll()  
  res.json(notes)
})

const PORT = process.env.PORT || 3001
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

لا يوجد شيء جديد في تعريف النموذج، فكل عمودٍ له نوع معرَّف، إضافةً إلى خاصيات أخرى عند الضرورة كأن يكون العمود مفتاحًا أساسيًا للجدول. يضم المعامل الثاني في تعريف النموذج السمة sequelize إضافةً إلى غيرها من معلومات التهيئة. كما عرّفنا أن الجدول لا ينبغي أن يضم أعمدة لها بصمات زمنية timestamps، مثل created_atأنشئ عند و updated_at حُدِّث عند.

عرّفنا أيضًا القيمة true إلى underscored، والتي تعني أن أسماء الجدول مشتقةٌ من أسماء النموذج لكن بصيغة الجمع وباستخدام تنسيق الأفعى snake_case، الذي تُوضع فيه الشرطة السفلية بدلًا من الفراغات بين الكلمات وتُكتب بأحرف صغيرة؛ ويعني هذا عمليًا أنه إذا كان اسم النموذج "Note"، فسيكون اسم الجدول المقابل "notes". ولو كان اسم النموذج مكونًا من جزئين مثل "StudyGroup"، فسيكون اسم الجدول المقابل "study_groups". وبدلًا من الاستدلال على أسماء الجداول تلقائيًا، تسمح لك المكتبة Sequelize بتعريف أسماء الجداول صراحةً أيضًا.

تُطبق سياسة التسمية هذه على أسماء الأعمدة أيضًا. فلو عرّفنا أن ملاحظةً ما سترتبط بعام الإنشاء creationYear أي بالمعلومات المتعلقة بعام إنشاء الملاحظة، سنعرّفها في النموذج على النحو التالي:

Note.init({
  // ...
  creationYear: {
    type: DataTypes.INTEGER,
  },
})

سيكون اسم العمود المقابل في قاعدة البيانات creation_year، لكن الإشارة إلى العمود في الشيفرة تكون دائمًا باستخدام تنسيق النموذج نفسه أي طريقة سنام الجمل camel case. وعرّفنا كذلك السمة modelName التي أُسندت إليها القيمة note، وستكون القيمة الافتراضية لاسم النموذج بحرف بداية كبير "Note"، وسترى أنّ هذا أكثر ملاءمةً لاحقًا.

يسهل التعامل مع قاعدة البيانات من خلال واجهة الاستعلام التي يؤمنها النموذج، إذ يعمل التابع works تمامًا كما يوحي اسمه:

app.get('/api/notes', async (req, res) => {
  const notes = await Note.findAll()
  res.json(notes)
})

تخبرك الطرفية أن التابع ()Note.findAll يُنفِّذ الاستعلام التالي:

Executing (default): SELECT "id", "content", "important", "date" FROM "notes" AS "note";

سننجز تاليًا وصلة endpoint بغرض إنشاء ملاحظات جديدة:

app.use(express.json())

// ...

app.post('/api/notes', async (req, res) => {
  console.log(req.body)
  const note = await Note.create(req.body)
  res.json(note)
})

تُضاف الملاحظة الجديدة باستدعاء التابع create الذي يوفّره النموذج "Note" بعد تمرير كائن يعرِّف قيم الأعمدة لسطر الملاحظة الجديدة على أنه وسيطٌ لهذا التابع.

وبإمكانك أيضًا حفظ قاعدة البيانات بتنفيذ التابع build أولًا لإنشاء كائن نموذج من البيانات المطلوبة، ثم استدعاء التابع save وذلك بدلًا من التابع create:

const note = Note.build(req.body)
await note.save()

لا يؤدي استدعاء التابع build إلى حفظ الكائن في قاعدة البيانات، وبالتالي من الممكن تعديله قبل تنفيذ أمر الحفظ الفعلي:

const note = Note.build(req.body)
note.important = true
await note.save()

يُعد استخدام التابع create في المثال السابق أكثر ملاءمةً لما نريده من التطبيق، لذلك سنلتزم باستخدامه من الآن فصاعدًا.

إذا لم يكن الكائن المُنشأ صالحًا، ستظهر رسالة خطأ، لذلك إذا حاولت إضافة ملاحظة جديدة دون محتوى، ستخفق العملية وتكشف لك الطرفية عن السبب بأن "المحتوى لا يمكن أن يكون فارغًا":

(node:39109) UnhandledPromiseRejectionWarning: SequelizeValidationError: notNull Violation: Note.content cannot be null
    at InstanceValidator._validate (/Users/mluukkai/opetus/fs-psql/node_modules/sequelize/lib/instance-validator.js:78:13)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)

لنضف آليةً بسيطةً لاصطياد الأخطاء عند إضافة ملاحظةٍ جديدة:

app.post('/api/notes', async (req, res) => {
  try {
    const note = await Note.create(req.body)
    return res.json(note)
  } catch(error) {
    return res.status(400).json({ error })
  }
})

التمرينات 13.1 إلى 13.3

سنبني في هذه التمرينات واجهة خلفية لتطبيق مدوّنات يشابه ما فعلنا في القسم 4، وينبغي أن يتوافق مع الواجهة الأمامية التي أنشأناها في القسم 5 باستثناء معالجة الأخطاء. سنضيف أيضًا ميزات مختلفة إلى الواجهة الخلفية لا يمكن للواجهة الأمامية في القسم 5 التعامل معها.

التمرين13.1

أنشئ مستودع غيت هاب GitHub مخصص للتطبيق، ثم أنشئ تطبيق Heroku خاص به إضافةً إلى قاعدة بيانات Postgres، وتأكد من قدرتك على تأسيس اتصال بين التطبيق وقاعدة البيانات.

التمرين 13.2

أنشئ باستخدام سطر الأوامر الجدول "blogs" الذي يضم الأعمدة التالية:

  • id: يمثل قيمةً فريدةً تزداد باستمرار.
  • author: قيمة نصية.
  • url: قيمة نصية لا يمكن أن تكون فارغة.
  • title: قيمة نصية لا يمكن أن تكون فارغة.
  • likes: قيمة صحيحة تبدأ من الصفر افتراضيًا.

أضف مدونتين على الأقل إلى قاعدة البيانات. احفظ بعد ذلك الأوامر التي استخدمتها في ملف يُدعى "commands.sql" في جذر التطبيق.

التمرين 13.3

أضف إلى تطبيقك وظيفة طباعة المدوّنات الموجودة في قاعدة البيانات باستخدام سطر الأوامر كما في المثال التالي:

$ node cli.js
Executing (default): SELECT * FROM blogs
Dan Abramov: 'On let vs const', 0 likes
Laurenz Albe: 'Gaps in sequences in PostgreSQL', 0 likes

إنشاء جداول قاعدة البيانات تلقائيا

يوجد في تطبيقنا الحالي جانب غير مرغوب فهو يفترض وجود قاعدة بيانات مع المخطط المعين، أي أنّ الجدول "notes" قد أُنشأ بتنفيذ الأمر create table.

تُخزّن شيفرة البرنامج على غيت هاب GitHub، لذلك من المنطقي تخزين الأوامر التي أنشأت بها قاعدة البيانات ضمن سياق الشيفرة لكي يبقى مخطط قاعدة البيانات نفسه كما تتوقعه شيفرة البرنامج. يُمكن للمكتبة أن تولّد تلقائيًا مخططًا انطلاقًا من تعريف النموذج من خلال التابع sync.

لندمر الآن قاعدة البيانات الموجودة من خلال أوامر الطرفية على النحو التالي:

drop table notes;

تأكد من تدمير القاعدة بتنفيذ الأمر d\:

username=> \d
Did not find any relations.

لن يعمل التطبيق الآن، لهذا سننفِّذ الأمر التالي مباشرةً بعد تعريف النموذج "Note":

Note.sync()

عندما يعمل التطبيق سيظهر ما يلي على شاشة الطرفية:

Executing (default): CREATE TABLE IF NOT EXISTS "notes" ("id" SERIAL , "content" TEXT NOT NULL, "important" BOOLEAN, "date" TIMESTAMP WITH TIME ZONE, PRIMARY KEY ("id"));

وهكذا، عندما يعمل التطبيق، تُنفَّذ التعليمة:

CREATE TABLE IF NOT EXISTS "notes"...

التي تُنشئ الجدول "notes" إن لم يكن موجودًا.

خيارات أخرى

لنكمل تطبيقنا بإضافة عدة خيارات أخرى.

بإمكاننا البحث عن ملاحظة محددة باستخدام التابع findByPk لأنه يبحث ضمن قيم id التي تمثّل المفتاح الرئيسي لقاعدة البيانات:

app.get('/api/notes/:id', async (req, res) => {
  const note = await Note.findByPk(req.params.id)
  if (note) {
    res.json(note)
  } else {
    res.status(404).end()
  }
})

يؤدي البحث عن ملاحظة محددة إلى تنفيذ أمر SQL التالي:

Executing (default): SELECT "id", "content", "important", "date" FROM "notes" AS "note" WHERE "note". "id" = '1';

في حال عدم وجود أية ملاحظات، سيعيد البحث القيمة null (لا شيء)، وسيعطي رمز الحالة المناسب.

تُعدَّل الملاحظة على النحو التالي، ولا يمكن تعديل سوى الحقل important لأن الواجهة الأمامية لا تحتاج أي شيء آخر:

app.put('/api/notes/: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()
  }
})

يُستخرج الكائن المتعلق بسطر قاعدة البيانات باستخدام التابع findByPk، ويُعدّل بعدها الكائن وتُخزَّن النتيجة باستدعاء التابع save.

بإمكانك إيجاد شيفرة التطبيق كاملةً في المستودع المخصص على GitHub ضمن الفرع "part13-1".

طباعة الكائن الذي تعيده Sequelize على الطرفية

تُعد الأداة console.log أفضل أدوات مبرمجي JavaScript إذ يمكّنهم استعمالها المتكرر من التقاط أسوأ الثغرات. لهذا سنطبع الملاحظات على الطرفية:

app.get('/api/notes/:id', async (req, res) => {
  const note = await Note.findByPk(req.params.id)
  if (note) {
    console.log(note)
    res.json(note)
  } else {
    res.status(404).end()
  }
})

لاحظ أنّ النتيجة النهائية ليست ما نتوقعه تمامًا:

note {
  dataValues: {
    id: 1,
    content: 'Notes are attached to a user',
    important: true,
    date: 2021-10-03T15:00:24.582Z,
  },
  _previousDataValues: {
    id: 1,
    content: 'Notes are attached to a user',
    important: true,
    date: 2021-10-03T15:00:24.582Z,
  },
  _changed: Set(0) {},
  _options: {
    isNewRecord: false,
    _schema: null,
    _schemaDelimiter: '',
    raw: true,
    attributes: [ 'id', 'content', 'important', 'date' ]
  },
  isNewRecord: false
}

فكل الأشياء التي يحتويها الكائن إضافةً إلى البيانات قد طُبعت على الطرفية، لهذا يمكن الوصول إلى النتيجة المرجوة باستدعاء التابع toJSON العائد إلى كائن النموذج:

app.get('/api/notes/:id', async (req, res) => {
  const note = await Note.findByPk(req.params.id)
  if (note) {
    console.log(note.toJSON())
    res.json(note)
  } else {
    res.status(404).end()
  }
})

ها هي النتيجة الآن كما هو متوقع.

{ id: 1,
  content: 'MongoDB is webscale',
  important: false,
  date: 2021-10-09T13:52:58.693Z }

في الحالة التي نريد فيها طباعة مجموعة من الكائنات، لن يعمل التابع toJSON مباشرةً، بل يجب أن يُستدعى بصورةٍ مستقلة لكل كائن في المجموعة:

router.get('/', async (req, res) => {
  const notes = await Note.findAll()

  console.log(notes.map(n=>n.toJSON()))

  res.json(notes)
})

وتبدو النتيجة على النحو التالي:

[ { id: 1,
    content: 'MongoDB is webscale',
    important: false,
    date: 2021-10-09T13:52:58.693Z },
  { id: 2,
    content: 'Relational databases rule the world',
    important: true,
    date: 2021-10-09T13:53:10.710Z } ]

وربما من الأفضل أن تحوّل المجموعة إلى تنسيق JSON لطباعتها باستخدام التابع JSON.stringify:

router.get('/', async (req, res) => {
  const notes = await Note.findAll()

  console.log(JSON.stringify(notes))

  res.json(notes)
})

تُعد هذه الطريقة أكثر ملاءمةً خاصةً إذا احتوت الكائنات على كائنات أخرى، كما أنها مفيدةٌ لتنسيق الكائنات على الشاشة بطريقة تُسهِّل القراءة، ويُنفَّذ ذلك من خلال الأمر التالي:

console.log(JSON.stringify(notes, null, 2))

وتبدو النتيجة على النحو التالي:

[
  {
    "id": 1,
    "content": "MongoDB is webscale",
    "important": false,
    "date": "2021-10-09T13:52:58.693Z"
  },
  {
    "id": 2,
    "content": "Relational databases rule the world",
    "important": true,
    "date": "2021-10-09T13:53:10.710Z"
  }
]

التمرين 13.4

حوّل تطبيقك إلى تطبيق ويب يدعم العمليات التالية:

  • الحصول على كل المدونات: GET api/blogs.
  • إضافة مدوّنة جديدة: POST api/blogs.
  • حذف مدوّنة: DELETE api/blogs/:id.

ترجمة -وبتصرف- للفصل Using relational databases with Sequelize من سلسلة Deep Dive Into Modern Web Development.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...