الجزء 13: قواعد البيانات العلائقية
استخدام قواعد البيانات العلائقية مع Sequelize
تأكد من أنك قرأت الفصل الأول: البدء!
إيجابيات قواعد البيانات المستندية وسلبياتها
استخدمنا MongoDB في جميع أقسام الدورة السابقة. Mongo قاعدة بيانات مستندية، ومن أبرز خصائصها أنها بلا مخطط (schemaless)، أي أن قاعدة البيانات لا تملك سوى إدراك محدود جداً لنوع البيانات المخزّنة في مجموعاتها. لا يوجد مخطط قاعدة البيانات إلا في شيفرة البرنامج، وهي التي تفسّر البيانات بطريقة معيّنة، مثلاً بتحديد أن بعض الحقول مراجع إلى كائنات في مجموعة أخرى.
في التطبيق المثال في الجزء 3 والجزء 4، تخزّن قاعدة البيانات ملاحظات ومستخدمين.
تبدو المجموعة التي تخزّن الملاحظات كما يلي:
[
{
"_id": "600c0e410d10256466898a6c",
"content": "HTML is easy"
"date": 2026-01-23T11:53:37.292+00:00,
"important": false
"__v": 0
},
{
"_id": "600c0edde86c7264ace9bb78",
"content": "CSS is hard"
"date": 2026-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 ما الحقول التي تملكها الكائنات المخزّنة في المجموعات. تترك MongoDB الأمر بالكامل للمبرمج ليضمن تخزين المعلومات الصحيحة في قاعدة البيانات.
لغياب المخطط إيجابيات وسلبيات. من الإيجابيات المرونة التي يوفّرها: بما أنه لا حاجة إلى تعريف مخطط على مستوى قاعدة البيانات، يمكن أن يكون تطوير التطبيقات أسرع وأسهل في حالات معيّنة، كما أن تعريف المخطط وتعديله لا يتطلبان جهداً يُذكر على أي حال. أما مشكلات غياب المخطط فتتعلق بقابلية الخطأ: فكل شيء متروك للمبرمج. لا تملك قاعدة البيانات نفسها أي وسيلة للتحقق مما إذا كانت البيانات فيها متسقة، أي ما إذا كانت جميع الحقول الإلزامية لها قيم، وما إذا كانت حقول نوع المرجع تشير إلى أنواع موجودة وصحيحة من الكائنات، إلخ.
تعتمد قواعد البيانات العلائقية التي يتركّز عليها هذا الجزء اعتماداً كبيراً على وجود مخطط، وإيجابيات قواعد البيانات ذات المخطط وسلبياتها تكاد تكون عكس تلك الخاصة بقواعد البيانات بلا مخطط.
سبب استخدام الأجزاء السابقة من الدورة لـMongoDB هو تحديداً غياب المخطط فيها، مما جعل استخدامها أسهل بعض الشيء لمن ليسوا على دراية كبيرة بقواعد البيانات العلائقية. بالنسبة لمعظم حالات الاستخدام في هذه الدورة، كنت سأختار بنفسي قاعدة بيانات علائقية.
قاعدة بيانات التطبيق
في المادة النظرية لهذا القسم، سنبني نسخة تدعم Postgres من الواجهة الخلفية لتطبيق تخزين الملاحظات الذي بُني في القسمين 3 و4.
نحتاج في تطبيقنا إلى قاعدة بيانات علائقية. هناك خيارات كثيرة، لكننا سنستخدم الحل مفتوح المصدر الأكثر شعبية حالياً PostgreSQL. يمكنك تثبيت Postgres (كما تُسمى قاعدة البيانات غالباً) على جهازك إن أردت ذلك، لكنه ليس ضرورياً.
ربما يكون الخيار الأسهل استخدام Postgres مستضاف في السحابة. هناك خيارات وفيرة، وعلى الأقل أحدها، aiven.io، يمكن استخدامه مجاناً للمشاريع الجانبية.
خيار آخر هو تطبيق ما تعلمته في الجزء 12 من الدورة واستخدام Postgres محلياً مع Docker. بعد تعليمات Postgres الخاصة بخدمات السحابة، نقدّم أيضاً تعليمات موجزة حول كيفية إعداد Postgres بسهولة مع Docker.
حل مستضاف: Aiven
في وقت كتابة هذا النص (9 فبراير 2026) توفّر Aiven طبقة مجانية تناسب أغراض هذه الدورة جيداً. اذهب إلى aiven.io وسجّل. بعد إنشاء قاعدة البيانات، تحقق مما هو رابط الاتصال:

Docker
تفترض هذه التعليمات أنك أتقنت أساسيات Docker بالقدر الذي يعلّمه مثلاً الجزء 12.
شغّل صورة Docker الخاصة بـPostgres بالأمر
docker run -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 postgres
يمكن فتح اتصال طرفية psql بقاعدة البيانات باستخدام أمر docker exec. أولاً عليك معرفة معرّف الحاوية:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ff3f49eadf27 postgres "docker-entrypoint.s…" 31 minutes ago Up 31 minutes 0.0.0.0:5432->5432/tcp great_raman
docker exec -it ff3f49eadf27 psql -U postgres postgres
psql (15.2 (Debian 15.2-1.pgdg110+1))
Type "help" for help.
postgres=#
بالتعريف بهذه الطريقة، لا تبقى البيانات المخزّنة في قاعدة البيانات محفوظة إلا طالما وُجدت الحاوية. يمكن الحفاظ على البيانات بتعريف حجم (volume) للبيانات. يمكنك الاطلاع على الجزء 12 للتفاصيل. انظر المزيد هنا.
الوصول إلى قاعدة البيانات
لا سيّما عند استخدام قاعدة بيانات علائقية، من الضروري الوصول إلى قاعدة البيانات مباشرة أيضاً. هناك طرق كثيرة لفعل ذلك. أحد الاحتمالات استخدام أداة سطر الأوامر psql الخاصة بـPostgres. إذا لم يكن psql مثبتاً لديك أو لم تكن تستخدم تثبيت Docker، نزّل وثبّت pgAdmin، وهو عميل Postgres رسومي.
فتح اتصال باستخدام psql
إذا كان psql مثبتاً على جهازك المضيف، يُفتح الاتصال بالأمر
psql postgres://userhere:passwordhere@hostnamehere.aivencloud.com:10789/defaultdb?sslmode=require
إذا كان Docker مثبتاً لديك، يمكنك فتح الاتصال بقاعدة بيانات Aiven كما يلي
docker run -it --rm postgres psql "postgres://userhere:passwordhere@hostnamehere.aivencloud.com:10789/defaultdb?sslmode=require"
تذكّر أنك تجد رابط اتصال قاعدة البيانات في لوحة تحكم Aiven.
إذا كانت قاعدة البيانات لديك داخل Docker، انظر أعلاه كيفية الوصول إليها بالأمر docker exec.
فتح اتصال باستخدام pgAdmin
ابدأ بإنشاء خادم:

املأ النموذج بالمعلومات التي تجدها في لوحة تحكم Aiven. بعد إعداد الخادم وإنشاء الاتصال، تُفتح طرفية قاعدة البيانات كما يلي:

إذا سار كل شيء على ما يرام، فأنت جاهز:

عند فتح الاتصال
عند فتح الطرفية، لنجرّب أمر psql الرئيسي \d، الذي يخبرك بمحتويات قاعدة البيانات:
psql (17.4 (Debian 17.4-1.pgdg120+2), server 17.7)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off, ALPN: postgresql)
Type "help" for help.
defaultdb=> \d
Did not find any relations.
defaultdb=>
كما قد تتوقع، لا يوجد شيء حالياً في قاعدة البيانات.
لننشئ جدولاً للملاحظات:
CREATE TABLE notes (
id SERIAL PRIMARY KEY,
content text NOT NULL,
important boolean,
date time
);
بضع نقاط: العمود id معرّف بأنه مفتاح أساسي، ما يعني أن القيمة في العمود يجب أن تكون فريدة لكل صف في الجدول، ويجب ألا تكون القيمة فارغة. نوع هذا العمود معرّف بأنه SERIAL، وهو ليس النوع الفعلي بل اختصار لعمود عدد صحيح يُسند إليه Postgres تلقائياً قيمة فريدة متزايدة عند إنشاء الصفوف. العمود المسمى content من النوع text معرّف بطريقة تجعل إسناد قيمة إليه إلزامياً.
لننظر إلى الحالة من الطرفية. أولاً الأمر \d، الذي يخبرنا ما الجداول الموجودة في قاعدة البيانات:
defaultdb=> \d
List of relations
Schema | Name | Type | Owner
--------+--------------+----------+----------
public | notes | table | username
public | notes_id_seq | sequence | username
(2 rows)
إضافة إلى جدول notes، أنشأ Postgres جدولاً فرعياً يُسمى notes_id_seq، يتتبّع القيمة المُسندة إلى عمود id عند إنشاء الملاحظة التالية.
بالأمر \d notes، يمكننا رؤية كيفية تعريف جدول notes:
defaultdb=> \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 له قيمة افتراضية تُحصل عليها باستدعاء دالة Postgres الداخلية nextval.
لنضف بعض المحتوى إلى الجدول:
insert into notes (content, important) values ('Relational databases rule the world', true);
insert into notes (content, important) values ('MongoDB is webscale', false);
ولنرَ كيف يبدو المحتوى المُنشأ:
defaultdb=> select * from notes;
id | content | important | date
----+-------------------------------------+-----------+------
1 | relational databases rule the world | t |
2 | MongoDB is webscale | f |
(2 rows)
إذا حاولنا تخزين بيانات في قاعدة البيانات لا تتوافق مع المخطط، فلن ينجح ذلك. لا يمكن أن تكون قيمة عمود إلزامي مفقودة:
defaultdb=> 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).
لا يمكن أن تكون قيمة العمود من النوع الخاطئ:
defaultdb=> 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); ^
الأعمدة غير الموجودة في المخطط لا تُقبل أيضاً:
defaultdb=> 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.js يستخدم قاعدة بيانات علائقية
لنبدأ التطبيق كالمعتاد بـ npm init ونثبّت nodemon كاعتمادية تطوير وكذلك الاعتماديات التشغيلية التالية:
npm install express dotenv pg sequelize
من بين هذه، الأخيرة sequelize هي المكتبة التي نستخدم بها Postgres. Sequelize هي مكتبة ربط الكائنات بالعلاقات (ORM) تتيح لك تخزين كائنات JavaScript في قاعدة بيانات علائقية دون استخدام لغة SQL نفسها، على غرار Mongoose الذي استخدمناه مع MongoDB.
لنجرّب أننا نستطيع الاتصال بقاعدة البيانات. أنشئ الملف index.js وأضف المحتوى التالي:
require('dotenv').config()
const { Sequelize } = require('sequelize')
const sequelize = new Sequelize(process.env.DATABASE_URL, {
dialect: 'postgres',
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()
يجب تعريف سلسلة الاتصال بقاعدة البيانات، التي تحتوي على عنوان قاعدة البيانات وبيانات الاعتماد، في الملف .env
إذا كنت تستخدم Aiven، ينبغي أن يكون محتوى الملف .env شيئاً مثل ما يلي:
$ cat .env
postgres://userhere:passwordhere@hostnamehere.aivencloud.com:10789/defaultdb
لاحظ أنه يجب عليك إزالة ?sslmode=require من سلسلة الاتصال!
إذا كنت تستخدم Docker، فسلسلة الاتصال هي:
DATABASE_URL=postgres://postgres:mysecretpassword@localhost:5432/postgres
بعد إعداد سلسلة الاتصال في الملف .env يمكننا اختبار الاتصال:
$ 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.
يبدو أن التطبيق يعمل، وتُطبع الملاحظات إلى الطرفية. لكن لننتقل الآن إلى استخدام Sequelize بدلاً من SQL، كما يُقصد به أن يُستخدم.
النموذج
عند استخدام 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'
})
const PORT = process.env.PORT || 3001
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
بضع تعليقات على الشيفرة: لا شيء مفاجئ كثيراً في تعريف النموذج Note؛ فكل عمود له نوع معرّف، وكذلك خصائص أخرى عند الحاجة، مثل ما إذا كان المفتاح الأساسي للجدول. المعامل الثاني في تعريف النموذج يحتوي على خاصية sequelize وكذلك معلومات تهيئة أخرى. عرّفنا أيضاً أن الجدول لا يلزمه استخدام عمودي الطوابع الزمنية (created_at وupdated_at).
عرّفنا أيضاً underscored: true، ما يعني أن أسماء الجداول تُشتق من أسماء النماذج بصيغة الجمع snake case. عملياً، هذا يعني أنه إذا كان اسم النموذج، كما في حالتنا، هو "Note"، فإن اسم الجدول المقابل هو صيغته الجمعية مكتوبة بحرف أول صغير، أي notes. أما إذا كان اسم النموذج "مكوّناً من جزأين"، مثل StudyGroup، فسيكون اسم الجدول study_groups. يستنتج Sequelize أسماء الجداول تلقائياً، لكنه يتيح أيضاً تعريفها صراحةً.
تنطبق سياسة التسمية نفسها على الأعمدة أيضاً. لو كنا قد عرّفنا أن الملاحظة مرتبطة بـ creationYear، أي معلومات عن السنة التي أُنشئت فيها، لعرّفناه في النموذج كما يلي:
Note.init({
// ...
creationYear: {
type: DataTypes.INTEGER,
},
})
سيكون اسم العمود المقابل في قاعدة البيانات creation_year. وفي الشيفرة، تكون الإشارة إلى العمود دائماً بالصيغة نفسها كما في النموذج، أي بصيغة "camel case".
عرّفنا أيضاً modelName: 'note'؛ اسم النموذج الافتراضي سيكون Note بحرف كبير. لكننا نريد حرفاً أولياً صغيراً، وهذا سيجعل بعض الأمور أسهل قليلاً فيما بعد.
عملية قاعدة البيانات سهلة باستخدام واجهة الاستعلام التي توفّرها النماذج. الدالة findAll تعمل تماماً كما يُفترض بها أن تعمل بحسب اسمها:
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";
بعد ذلك، لننفّذ نقطة نهاية لإنشاء ملاحظات جديدة:
app.use(express.json())
// ...
app.post('/api/notes', async (req, res) => {
console.log(req.body)
const note = await Note.create({...req.body, date: new Date()})
res.json(note)
})
يتم إنشاء ملاحظة جديدة باستدعاء دالة النموذج Note المسماة create وتمرير كائن يحدد قيم الأعمدة كمعامل.
بدلاً من دالة create، من الممكن أيضاً الحفظ في قاعدة بيانات باستخدام دالة build أولاً لإنشاء كائن Model من البيانات المطلوبة، ثم استدعاء دالة save عليه:
const note = Note.build(req.body)
await note.save()
استدعاء دالة build لا يحفظ الكائن في قاعدة البيانات بعد، لذا يظل ممكناً تعديل الكائن قبل حدث الحفظ الفعلي:
const note = Note.build(req.body)
note.important = true
await note.save()
لحالة استخدام الشيفرة المثال، دالة create أنسب، لذا لنلتزم بها.
إذا كان الكائن المُنشأ غير صالح، تنتج رسالة خطأ. مثلاً عند محاولة إنشاء ملاحظة دون محتوى، تفشل العملية وتكشف الطرفية أن السبب هو SequelizeValidationError: notNull Violation Note.content cannot be null:
(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, date: new Date()})
return res.json(note)
} catch(error) {
return res.status(400).json({ error })
}
})
1. المستودع وقاعدة البيانات
2. الاتصال عبر الطرفية
3. الاتصال من التطبيق
إنشاء جداول قاعدة البيانات تلقائياً
تطبيقنا الآن فيه عيب واحد: يفترض وجود قاعدة بيانات بمخطط صحيح تماماً، أي أن جدول notes أُنشئ بأمر create table المناسب.
بما أن شيفرة البرنامج مخزّنة في GitHub، سيكون من المنطقي تخزين الأوامر التي تنشئ قاعدة البيانات أيضاً إلى جانب شيفرة البرنامج، حتى يكون مخطط قاعدة البيانات بالتأكيد مطابقاً لما تتوقعه شيفرة البرنامج. Sequelize قادر فعلاً على توليد المخطط تلقائياً من تعريفات النماذج باستخدام دالة النموذج sync.
لندمّر الآن جدول notes من الطرفية بإدخال الأمر التالي:
drop table notes;
يكشف الأمر \d أن الجدول فُقد من قاعدة البيانات:
postgres=# \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، لأنها تُجلب بناءً على معرّف المفتاح الأساسي:
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، الفرع step1.
طباعة الكائنات التي يعيدها Sequelize إلى الطرفية
أهم أداة لدى مبرمج JavaScript (إلى جانب وكلاء الذكاء الاصطناعي) هي console.log، التي يجعل استخدامها المكثّف حتى أسوأ الأخطاء تحت السيطرة. لنضف طباعة إلى الطرفية في مسار الملاحظة الواحدة:
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: 2026-02-03T15:00:24.582Z,
},
_previousDataValues: {
id: 1,
content: 'Notes are attached to a user',
important: true,
date: 2026-02-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: 2026-02-09T13:52:58.693Z }
في حالة مجموعة من الكائنات، لا تعمل دالة toJSON مباشرةً، بل يجب استدعاؤها منفصلة لكل كائن في المجموعة:
app.get('/api/notes', 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: 2026-02-09T13:52:58.693Z },
{ id: 2,
content: 'Relational databases rule the world',
important: true,
date: 2026-02-09T13:53:10.710Z } ]
لكن ربما الحل الأفضل هو تحويل المجموعة إلى JSON للطباعة باستخدام الدالة JSON.stringify:
app.get('/api/notes', 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": "2026-02-09T13:52:58.693Z"
},
{
"id": 2,
"content": "Relational databases rule the world",
"important": true,
"date": "2026-02-09T13:53:10.710Z"
}
]
4. وُلدت خدمة الويب