-
المساهمات
36 -
تاريخ الانضمام
-
تاريخ آخر زيارة
-
عدد الأيام التي تصدر بها
3
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو Mostafa Ata العايش
-
تعرفنا في بداية هذا الدليل إلى بعض أوامر صدفة psql الأساسية، ولكننا يجب أن نتعرف إلى بقية هذه الأوامر، والتي يوجد العديد منها لمساعدتنا في أمور قد تبدو صعبة التنفيذ بطرق أخرى. نذكر في هذا الفصل أوامر للاتصال بقاعدة بيانات، استعراض الفهارس، تنسيق المخرجات، كيفية استخدام الأوامر الشرطية وكيفية تنفيذ أوامر نظام التشغيل من داخل صدفة psql وغيرها من الأوامر التي تُعتبر الأكثر أهمية من بين الأوامر المتقدمة. أصناف أوامر psql تنقسم أوامر psql إلى المجموعات التالية: أوامر مساعدة، لعرض معلومات مساعدة في كيفية استخدام أوامر أخرى. أوامر التحكم بمخزن الاستعلامات، هو ملف مؤقت يمكن كتابة الاستعلامات فيه عن طريق محرر النصوص، لتسهيل كتابة الاستعلامات الطويلة قبل تنفيذها، أو كتابة عدة أوامر واستعلامات وتحرير كل ذلك قبل التنفيذ. أوامر الإدخال والإخراج، تسمح بتنفيذ تعليمات مخزنة في ملفات خارجية، أو كتابة مخرجات التعليمات والاستعلامات إلى ملفات خارجية. الأوامر الشرطية، تسمح باستخدام if و else للتحكم بسير سلسلة من التعليمات. أوامر استعراض المعلومات، لعرض معلومات عن قاعدة البيانات، الجداول، المخططات، وغيرها. أوامر تنسيق البيانات، تسمح بتنسيق طريقة إظهار المخرجات. أوامر الاتصال، للاتصال بقاعدة بيانات أخرى. أوامر نظام التشغيل، تسمح بتنفيذ أوامر نظام التشغيل من داخل الصدفة أوامر التحكم بالمتغيرات، لتحديد قيمة متغير أو إزالتها أو إدخالها من قبل المستخدم أوامر الكائنات الكبيرة، وهي الكائنات التي لا يتم تخزينها في قاعدة البيانات مباشرةً بل تُخزن في ملفات مستقلة، ويتم الإشارة إليها باستخدام Object id. سنتعرف في هذه الفقرة إلى الأوامر الأكثر أهمية فقط، إلا أن اطلاعك على باقي الأوامر لا شك سيتيح لك استخدامًا أكثر حرية واحترافية لصدفة psql، كما سيسمح لك في كتابة استعلامات معقدة حتى لو لم تكن محترفًا في SQL. أوامر الاتصال تُعتبر أوامر الاتصال من أهم أوامر صدفة psql، فهي التي تسمح بالاتصال بقاعدة بيانات معينة، أو استعراض المعلومات الخاصة بها. استعراض معلومات الاتصال يمكن استخدام الأمر '\conninfo' لاستعراض المعلومات الرئيسية للاتصال الحالي (اسم قاعدة البيانات، اسم المستخدم، رقم المنفذ للعملية postgres): hsoubguide=# \conninfo You are connected to database "hsoubguide" as user "postgres" via socket in "/var/run/postgresql" at port "5432". الاتصال بقاعدة بيانات أخرى إن لم تقم بالدخول إلى الصدفة محددًّا اسم قاعدة البيانات التي ترغب بالاتصال بها، فيمكنك الاتصال من داخل الصدفة بالأمر '\connect' hsoubguide=# \connect postgres You are now connected to database "postgres" as user "postgres". postgres=# \connect hsoubguide You are now connected to database "hsoubguide" as user "postgres". أوامر استعراض أخرى مهمة سنذكر فيما يلي كيفية استعراض التوابع في صدفة psql وكيفية استعراض الفهارس المخزنة في قاعدة البيانات، وقد ذكرنا العديد من أوامر الاستعراض في الفصل الرابع من هذا الدليل، استعرضنا فيها الجداول وقواعد البيانات، فيمكن الرجوع للفصل الرابع لأوامر الاستعراض الأساسية. استعراض جميع التوابع المتاحة يعرض الأمر '\df' قائمة بجميع التوابع التي يٌمكن استخدامها في الصدفة، مع نوع القيمة المعادة من كل تابع، ونوع الوسطاء الممررة لكل منهم: hsoubguide=# \df List of functions Schema | Name | Result data type | Argument data types | Type --------+--------------------------+------------------+---------------------------------------------------------+------ public | akeys | text[] | hstore | func public | avals | text[] | hstore | func public | defined | boolean | hstore, text | func public | delete | hstore | hstore, hstore | func public | delete | hstore | hstore, text | func public | delete | hstore | hstore, text[] | func public | each | SETOF record | hs hstore, OUT key text, OUT value text | func public | exist | boolean | hstore, text | func public | exists_all | boolean | hstore, text[] | func public | exists_any | boolean | hstore, text[] | func public | fetchval | text | hstore, text | func ... ... (58 rows) كما يمكن الحصول على المزيد من المعلومات التي تخص كل تابع بإضافة الرمز + في نهاية الأمر السابق. استعراض الفهارس المخزنة في قاعدة البيانات يعرض الأمر '\di' قائمة بأهم الفهارس (indexes) المخزنة في قاعدة البيانات، كما يمكن استخدام الرمز + لاستعراض معلومات إضافية، منها الحجم الذي تحجزه هذه الفهارس في الذاكرة. hsoubguide=# \di+ List of relations Schema | Name | Type | Owner | Table | Size | Description --------+----------------------------+-------+----------+-------------+------------+------------- public | basket_a_pkey | index | postgres | basket_a | 16 kB | public | basket_b_pkey | index | postgres | basket_b | 16 kB | public | departments_department_key | index | postgres | departments | 16 kB | public | departments_pkey | index | postgres | departments | 16 kB | public | employees_pkey | index | postgres | employees | 16 kB | public | marks_pkey | index | postgres | marks | 16 kB | public | names_pkey | index | postgres | names | 16 kB | public | phones_pkey | index | postgres | phones | 16 kB | public | products_pkey | index | postgres | products | 16 kB | public | purchases_pkey | index | postgres | purchases | 40 kB | public | student_pkey | index | postgres | student | 16 kB | public | table1_pkey | index | postgres | table1 | 16 kB | public | table2_pkey | index | postgres | table2 | 16 kB | public | test_table_pkey | index | postgres | test_table | 16 kB | public | users2_pkey | index | postgres | users2 | 8192 bytes | public | users_pkey | index | postgres | users | 16 kB | (16 rows) أوامر التنسيق تسمح هذه الأوامر بتغيير التنسيق الافتراضي للجداول الناتجة من الاستعلامات، وتتيح ميزة كبيرة قد تختصر عليك الكثير من الوقت والجهد الذي يلزم لتحويل التنسيق الافتراضي يدويًّا إلى شكل أكثر فائدة. تنسيق المحاذاة يمكن استخدام الأمر '\a' لتشغيل محاذاة محتوى العمود مع اسمه، أو لإيقاف ذلك. hsoubguide=# \a Output format is unaligned. hsoubguide=# SELECT * FROM products LIMIT 4; id|title|price|created_at|deleted_at|tags 1|Dictionary|9.99|2011-01-01 22:00:00+02||{Book} 2|Python Book|29.99|2011-01-01 22:00:00+02||{Book,Programming,Python} 3|Ruby Book|27.99|2011-01-01 22:00:00+02||{Book,Programming,Ruby} 4|Baby Book|7.99|2011-01-01 22:00:00+02||{Book,Children,Baby} (4 rows) hsoubguide=# \a Output format is aligned. hsoubguide=# SELECT * FROM products LIMIT 4; id | title | price | created_at | deleted_at | tags ----+-------------+-------+------------------------+------------+--------------------------- 1 | Dictionary | 9.99 | 2011-01-01 22:00:00+02 | | {Book} 2 | Python Book | 29.99 | 2011-01-01 22:00:00+02 | | {Book,Programming,Python} 3 | Ruby Book | 27.99 | 2011-01-01 22:00:00+02 | | {Book,Programming,Ruby} 4 | Baby Book | 7.99 | 2011-01-01 22:00:00+02 | | {Book,Children,Baby} (4 rows) كما يمكنك تغيير المحرف الفاصل بين الأعمدة باستخدام الأمر '\f' مع تمرير المحرف المُراد، ويمكنك كذلك منع طباعة أسماء الأعمدة والاكتفاء بالمحتويات باستخدام الأمر '\t'. تنسيق HTML يمكنك استخدام الأمر '\H' لتغيير حالة المخرجات من النمط المكتوب إلى نمط HTML وبالعكس: hsoubguide=# SELECT * FROM products LIMIT 4; id | title | price | created_at | deleted_at | tags ----+-------------+-------+------------------------+------------+--------------------------- 1 | Dictionary | 9.99 | 2011-01-01 22:00:00+02 | | {Book} 2 | Python Book | 29.99 | 2011-01-01 22:00:00+02 | | {Book,Programming,Python} 3 | Ruby Book | 27.99 | 2011-01-01 22:00:00+02 | | {Book,Programming,Ruby} 4 | Baby Book | 7.99 | 2011-01-01 22:00:00+02 | | {Book,Children,Baby} (4 rows) hsoubguide=# \H Output format is html. hsoubguide=# SELECT * FROM products LIMIT 4; <table border="1"> <tr> <th align="center">id</th> <th align="center">title</th> <th align="center">price</th> <th align="center">created_at</th> <th align="center">deleted_at</th> <th align="center">tags</th> </tr> <tr valign="top"> <td align="right">1</td> <td align="left">Dictionary</td> <td align="right">9.99</td> <td align="left">2011-01-01 22:00:00+02</td> <td align="left"> </td> <td align="left">{Book}</td> </tr> <tr valign="top"> <td align="right">2</td> <td align="left">Python Book</td> <td align="right">29.99</td> <td align="left">2011-01-01 22:00:00+02</td> <td align="left"> </td> <td align="left">{Book,Programming,Python}</td> </tr> <tr valign="top"> <td align="right">3</td> <td align="left">Ruby Book</td> <td align="right">27.99</td> <td align="left">2011-01-01 22:00:00+02</td> <td align="left"> </td> <td align="left">{Book,Programming,Ruby}</td> </tr> <tr valign="top"> <td align="right">4</td> <td align="left">Baby Book</td> <td align="right">7.99</td> <td align="left">2011-01-01 22:00:00+02</td> <td align="left"> </td> <td align="left">{Book,Children,Baby}</td> </tr> </table> <p>(4 rows)<br /> </p> hsoubguide=# \H Output format is aligned. استعراض تاريخ الاستعلامات وحفظه يمكن استعراض تاريخ الاستعلامات وأوامر الصدفة التي نُفّذت من قبل باستخدام الأمر \s، كما يمكنك حفظ هذا السجل بإضافة اسم الملف الذي ترغب بحفظ السجل فيه. hsoubguide=# \s /tmp/psql_history.txt Wrote history to file "/tmp/psql_history.txt". أوامر التعامل مع المتغيرات تسمح psql بتعريف متغيرات داخلها، واستخدامها لاحقًا ضمن التعليمات أو الاستعلامات اللاحقة. يمكن تعريف متغير باستخدام الأمر set\ كما يمكن حذفه باستخدام unset\، وفي حال أردنا استخدام قيمة هذا المتغير، علينا أن نضع قبل اسمه الرمز :. hsoubguide=# \set NAME mostafa hsoubguide=# \set rate 5.9 hsoubguide=# \echo :rate 5.9 hsoubguide=# \echo :NAME mostafa hsoubguide=# SELECT * FROM test_table WHERE number > :rate; id | number | name ----+--------+------- 4 | 22 | hel.. 1 | 10 | 10 2 | 13 | 13 (3 rows) كما يمكن استخدام الأمر prompt\ لطلب إدخال قيمة من المستخدم، وذلك بعد طباعة عبارة الطلب: hsoubguide=# \prompt 'Please enter your name: ' NAME Please enter your name: Mostafa hsoubguide=# \echo :NAME Mostafa يمكنك استخدام قيم هذه المتغيرات ضمن الاستعلامات أو ضمن أوامر psql أخرى. الأوامر الشرطية بعد أن تعرفنا على الأوامر الخاصة بالمتغيرات، يجدر بنا أن نعرف أن psql تتيح استخدام التعليمات الشرطية if\ و elif\ و else\ للتحكم بكيفية سير التنفيذ، ويوضح المثال التالي المأخوذ من التوثيق كيفية استخدامها: SELECT EXISTS(SELECT 1 FROM customer WHERE customer_id = 123) as is_customer, EXISTS(SELECT 1 FROM employee WHERE employee_id = 456) as is_employee \gset \if :is_customer SELECT * FROM customer WHERE customer_id = 123; \elif :is_employee \echo 'is not a customer but is an employee' SELECT * FROM employee WHERE employee_id = 456; \else \if yes \echo 'not a customer or employee' \else \echo 'this will never print' \endif \endif أوامر نظام التشغيل تسمح لنا صدفة psql بتنفيذ أوامر من خارج إمكانيات الصدفة، وهذا يسمح لنا بالبقاء ضمنها، والتعامل مع نظام التشغيل من داخلها مباشرةً. تنفيذ أوامر صدفة bash ضمن psql تسمح psql بتنفيذ أوامر صدفة bash بكتابتها بعد الأمر !\ كما يلي: postgres=# \! echo Hello Hello postgres=# \! pwd /usr/pgsql-12/bin تشغيل توقيت الاستعلام في الحالة الافتراضية لا يكون توقيت تنفيذ الاستعلام مُتاحًا للعرض، ولكن يمكننا تفعيله من خلال الأمر التالي: hsoubguide=# SELECT * FROM products LIMIT 1; id | title | price | created_at | deleted_at | tags ----+------------+-------+------------------------+------------+-------- 1 | Dictionary | 9.99 | 2011-01-01 22:00:00+02 | | {Book} (1 row) Time: 0.723 ms حيث سيتيح ذلك الأمر إظهار توقيت الاستعلام بالميلي ثانية. الخروج من صدفة postgres قد تقضي وقتًا طويلًا داخل صدفة psql، ليس حبًّا بها، ولكن لعدم معرفة كيفية الخروج منها، لذلك لا تنسَ أن الأمر \q هو الذي يُخرجك من صدفة psql. hsoubguide=# \q bash-4.2$ خلاصة تعرفنا في هذا الفصل إلى جميع أنواع أوامر صدفة psql، وذكرنا أكثرها أهمية، بما يسمح لك بزيادة كفاءة تعاملك مع قواعد بيانات Postgres، وتذكر دومًا أنه يمكنك استعراض العديد من الأوامر الأخرى من خلال الأمر \?. اقرأ أيضًا المقال السابق: كيفية إدارة الأداء في قواعد بيانات Postgres النسخة الكاملة لكتاب الدليل العملي إلى قواعد بيانات PostgreSQL كيف تستخدم تقنية بحث النصوص الكاملة Full-Text Search في PostgreSQL على خادم أوبنتو
-
سنتعرف في هذا الفصل إلى طريقة تتبع أداء الاستعلامات في Postgres، وذلك لمعرفة الزمن المتوقع والحقيقي للاستعلام، كما يذكر كيفية عمل قيود على استخدام الفهارس، ثم سنتطرق لفهم ذاكرة التخزين المؤقتة Cache في Postgres. خطة التنفيذ (Execution plan) لدى Postgres قدرة كبيرة على إظهار كيفية تنفيذ الاستعلامات خلف الكواليس، وهذا ما يُسمى بخطة التنفيذ ونستخدم لإظهار ذلك التعليمة explain، وإن فهمك لهذه المعلومات يساعدك في تحسين قاعدة بياناتك باستخدام الفهارس لرفع الكفاءة. سننشئ جدولًا صغيرًا لنقوم عليه ببعض التجارب: hsoubguide=# CREATE TABLE test_explain(msg varchar(20)); CREATE TABLE جميع الاستعلامات في Postgres يكون لها خطة تنفيذ عندما يتم تنفيذها، وهناك ثلاث أشكال لتنفيذ التعليمة explain كما يلي: الشكل العام: باستخدام EXPLAIN، يقوم بعرض توقّع لما سيحدث تقريبًا، دون أن يتم التنفيذ الفعلي للتعليمة hsoubguide=# EXPLAIN INSERT INTO test_explain(msg) VALUES('Hsoub'); QUERY PLAN ----------------------------------------------------------- Insert on test_explain (cost=0.00..0.01 rows=1 width=58) -> Result (cost=0.00..0.01 rows=1 width=58) (2 rows) hsoubguide=# SELECT * FROM test_explain ; msg ----- (0 rows) الشكل التحليلي: باستخدام EXPLAIN ANALYZE، يقوم بتنفيذ الاستعلام ثم يعرض شرحًا لما حدث خلال التنفيذ. hsoubguide=# EXPLAIN ANALYZE INSERT INTO test_explain(msg) VALUES('Hsoub'); QUERY PLAN ----------------------------------------------------------------------------------------------------- Insert on test_explain (cost=0.00..0.01 rows=1 width=58) (actual time=0.898..0.898 rows=0 loops=1) -> Result (cost=0.00..0.01 rows=1 width=58) (actual time=0.003..0.004 rows=1 loops=1) Planning Time: 0.067 ms Execution Time: 0.952 ms (4 rows) hsoubguide=# SELECT * FROM test_explain ; msg ------- Hsoub (1 row) الشكل المستفيض (verbose)، يزيد عن الشكل التحليلي بالقليل من المعلومات، وهنا يمكن استخدام EXPLAIN VERBOSE للشرح دون التنفيذ: hsoubguide=# EXPLAIN VERBOSE INSERT INTO test_explain(msg) VALUES('Hsoub2'); QUERY PLAN ------------------------------------------------------------------ Insert on public.test_explain (cost=0.00..0.01 rows=1 width=58) -> Result (cost=0.00..0.01 rows=1 width=58) Output: 'Hsoub2'::character varying(20) (3 rows) hsoubguide=# SELECT * FROM test_explain ; msg ------- Hsoub (1 row) أو استخدام EXPLAIN ANALYZE VERBOSE للشرح المستفيض مع التنفيذ: hsoubguide=# EXPLAIN ANALYZE VERBOSE INSERT INTO test_explain(msg) VALUES('Hsoub2'); QUERY PLAN ------------------------------------------------------------------------------------------------------------ Insert on public.test_explain (cost=0.00..0.01 rows=1 width=58) (actual time=0.044..0.045 rows=0 loops=1) -> Result (cost=0.00..0.01 rows=1 width=58) (actual time=0.003..0.004 rows=1 loops=1) Output: 'Hsoub2'::character varying(20) Planning Time: 0.074 ms Execution Time: 0.086 ms (5 rows) hsoubguide=# SELECT * FROM test_explain ; msg ------- Hsoub Hsoub2 (2 rows) غالبًا يتم استخدام التعليمة explain مع عبارات SELECT، إلا أنه يمكن استخدامها أيضًا مع التعليمات: INSERT UPDATE DELETE EXECUTE DECLARE استخدام التعليمة Explain لشرح الاستعلامات نستعلم في المثال التالي عن الأسماء الأخيرة للموظفين ذوي الرواتب التي تبدأ من 50000 فما فوق، كما يلي: hsoubguide=# SELECT last_name FROM employees WHERE salary >= 50000; last_name ----------- Adams Smith (2 rows) يمكننا تفحّص كيفية قيام Postgres بتنفيذ الاستعلام السابق كما يلي: hsoubguide=# EXPLAIN SELECT last_name FROM employees WHERE salary >= 50000; QUERY PLAN ----------------------------------------------------------- Seq Scan on employees (cost=0.00..1.06 rows=2 width=128) Filter: (salary >= 50000) (2 rows) كما يمكن فهم أداء تنفيذ الاستعلام الحقيقي عن طريق ANALYZE كما يلي: hsoubguide=# EXPLAIN ANALYZE SELECT last_name FROM employees WHERE salary >= 50000; QUERY PLAN ----------------------------------------------------------------------------------------------------- Seq Scan on employees (cost=0.00..1.06 rows=2 width=128) (actual time=0.032..0.036 rows=2 loops=1) Filter: (salary >= 50000) Rows Removed by Filter: 3 Planning Time: 0.142 ms Execution Time: 0.084 ms (5 rows) فهم خطط التنفيذ إن نفّذنا التعليمة EXPLAIN ANALYZE على جدول مشابه للجدول السابق ولكن مع احتوائه على مليوني سطر، يمكن أن نحصل على المخرجات التالية: تَوضّح الصورة التالية معاني هذه الأرقام المكتوبة كما يلي: تشير الكلمة Seq Scan إلى أن عملية البحث التي تجري هي البحث التسلسلي. أما العبارة التالية: (cost=0.00..35811.00 rows=1 width=6) فهي التقدير التقريبي (وليس الحقيقي) لما يُتوقّع أن يستغرقه تنفيذ الاستعلام، حيث يعبر الزمن 0.00 عن الزمن اللازم لبدء الاستعلام، والزمن 35811.00 هو الزمن المتوقع لإنهاء الاستعلام، أما القيمة rows=1 هي عدد الأسطر التي يُتوقّع أن تُعطى في المخرجات، والقيمة width=6 هي الحجم التقريبي لمحتوى الأسطر التي يُتوقع أن تُعطى في المخرجات. ولأننا قُمنا بتنفيذ التعليمة EXPLAIN ANALYZE فإننا لم نحصل فقط على التقدير المتوقع للتنفيذ، بل على الوقت الحقيقي المستغرق كذلك كما تبيّن الصورة التالية: يُمكننا بذلك رؤية الوقت الكبير المستهلك في المسح المتتالي، وسنقارنه مع الوقت المستغرق عند إضافة فهرس واختبار النتائج: CREATE INDEX idx_emps on employees (salary); وبذلك خفضنا زمن الاستعلام من 295 ميللي ثانية إلى 1.7 ميللي ثانية فقط كما يوضح الشكل التالي: كانت هذه مقدمة في استخدام التعليمة EXPLAIN ولا بد أن تجربها على استعلامات أكبر حجمًا لاستكشاف ما يمكنها إظهاره. قيود شرطية على إنشاء الفهارس قد نحتاج أحيانًا إلى وضع بعض القيود على البيانات التي نرغب بالفعل في فهرستها، فمثلًا قد لا نرغب بحذف مستخدم ما من قاعدة البيانات ولكن نريد أن نظهره على أنه محذوف بحيث يمكن إرجاع بياناته لو أراد العودة للموقع بعد شهر مثلًا، ولكن في الوقت نفسه لا نريد أن يزداد حجم البيانات التي نقوم بفهرستها، ولذلك نستخدم الفهارس الجزئية. سنستخدم الفهارس الجزئية في المثال التالي لوضع فهرس فريد فقط للمستخدمين غير المحذوفين: CREATE UNIQUE INDEX user_email ON users (email) WHERE deleted_at IS NULL; ذاكرة التخزين المؤقتة (Cache) إن معظم التطبيقات تتعامل مع جزء صغير من البيانات بشكل متكرر، ومثل العديد من الأمور الأخرى فإن البيانات يمكن أن تتبع قاعدة 80/20 حيث أن 20% من البيانات هي التي يتم قراءتها في 80% من الحالات، وأحيانا يكون هذا الرقم أكبر من ذلك. تقوم Postgres بتتبع أنماط استخدامك للبيانات وتحاول الإبقاء على البيانات الأكثر وصولًا في ذاكرة التخزين المؤقتة. عمومًا، سترغب في أن يكون نسبة نجاح الوصول إلى ذاكرة التخزين المؤقتة هي 99%، ويمكنك معرفة هذه النسبة باستخدام التعليمات التالية: SELECT sum(heap_blks_read) as heap_read, sum(heap_blks_hit) as heap_hit, (sum(heap_blks_hit) - sum(heap_blks_read)) / sum(heap_blks_hit) as ratio FROM pg_statio_user_tables; إن وجدت أن نسبة نجاح الوصول إلى التخزين المؤقتة عندك أخفض من 99% بشكل كبير، فربما يتوجب عليك زيادة حجمها المخصص لقاعدة البيانات. فهم استخدام الفهارس تُعد الفهارس الطريقة الأساسية الأخرى لزيادة كفاءة قاعدة البيانات، حيث تضيف العديد من بيئات العمل الفهارس إلى المفاتيح الرئيسية في الجداول، ولكن إن كنت تُجري عمليات البحث على حقول أخرى أو تقوم بالربط بين الجداول فربما عليك إضافة الفهارس يدويًّا إلى هذه الأعمدة. الفهارس هي الأهم في الجداول الكبيرة، فرغم أن الوصول إلى الجداول في ذاكرة التخزين المؤقتة أسرع من الوصول إليها على القرص الصلب، إلا أنه حتى البيانات فيها يمكن أن تكون بطيئة إن كان يتوجب على Postgres أن تحلل مئات آلاف الأسطر لمعرفة إن كانت تحقق شرطًا ما. يمكنك استخدام التعليمة التالية لتوليد قائمة بالجداول مع النسبة المئوية لاستخدام الفهارس فيها: SELECT relname, 100 * idx_scan / (seq_scan + idx_scan) percent_of_times_index_used, n_live_tup rows_in_table FROM pg_stat_user_tables WHERE seq_scan + idx_scan \> 0 ORDER BY n_live_tup DESC; إن لم تكن قريبًا من النسبة 99% في الجداول التي تحوي 10 آلاف سطرًا فأكثر، فربما يجب عليك إضافة المزيد من الفهارس، ولكن عليك معرفة العمود المناسب الذي يجب اعتباره فهرسًا، وذلك عن طريق معرفة نوع الاستعلامات التي تتم على الجداول. عمومًا، قد يكون من المناسب وضع الأعمدة التي تحوي المعرّفات ID أو على الأعمدة تقوم بالتصفية على أساسها غالبًا مثل created_at. نصيحة احترافية: إن كنت تضيف فهرسًا في قاعدة بيانات قيد العمل استخدم CREATE INDEX CONCURRENTLY لتقوم ببناء الفهرس في الخلفية دون أن يتم قفل الجدول ومنع الاستعلامات عليه. يمكن أن يستغرق الإنشاء الآني للفهارس 2-3 أضعاف الوقت المستغرق في العادة، ولا يمكن تنفيذها على دفعات، ولكن هذه المقايضة بين الوقت وتجربة المستخدم تستحق ذلك، فلا شك أنك لا تريد توقّف الموقع الخاص بك كلما قمت بإنشاء فهرس جديد في جدول كبير الحجم. مثال باستخدام بيانات حقيقية عند النظر إلى بيانات حقيقية من واجهة Heroku التي تم إطلاقها مؤخرًا، يمكن تنفيذ الاستعلام التالي ورؤية النتائج كما يلي: SELECT relname, 100 * idx_scan / (seq_scan + idx_scan) percent_of_times_index_used, n_live_tup rows_in_table FROM pg_stat_user_tables ORDER BY n_live_tup DESC; relname | percent_of_times_index_used | rows_in_table ---------------------+-----------------------------+--------------- events | 0 | 669917 app_infos_user_info | 0 | 198218 app_infos | 50 | 175640 user_info | 3 | 46718 rollouts | 0 | 34078 favorites | 0 | 3059 schema_migrations | 0 | 2 authorizations | 0 | 0 delayed_jobs | 23 | 0 يمكننا أن نرى أن جدول events فيه 700 ألف سطرًا تقريبًا وليس فيه فهارس تم استخدامها، ومن هنا يمكننا التحقق ضمن تطبيقنا لنرى بعض الاستعلامات الشائعة التي تُستخدم، وأحد هذه الأمثلة هو جلب الأحداث لمدوّنة ما. يمكنك أن ترى خطة التنفيذ باستخدام التعليمة EXPLAIN ANALYZE التي تعطيك فكرة أفضل عن الاستعلام: EXPLAIN ANALYZE SELECT * FROM events WHERE app_info_id = 7559; QUERY PLAN ------------------------------------------------------------------- Seq Scan on events (cost=0.00..63749.03 rows=38 width=688) (actual time=2.538..660.785 rows=89 loops=1) Filter: (app_info_id = 7559) Total runtime: 660.885 ms وبما أن طريقة البحث ضمن هذا الجدول هي المسح التسلسلي، فيمكننا تحسين ذلك باستخدام فهرس، وسنضيف الفهرس الخاص بنا آنيًّا لمنع قفل الجدول، ثم سنرى كيف تتغير كفاءة الجدول: CREATE INDEX CONCURRENTLY idx_events_app_info_id ON events(app_info_id); EXPLAIN ANALYZE SELECT * FROM events WHERE app_info_id = 7559; ---------------------------------------------------------------------- Index Scan using idx_events_app_info_id on events (cost=0.00..23.40 rows=38 width=688) (actual time=0.021..0.115 rows=89 loops=1) : Index Cond: (app_info_id = 7559) Total runtime: 0.200 ms يتضح من التعليمة السابقة التحسن الذي أدى إليه استخدام الفهرس، ولكن يمكننا أيضًا أن نحلل النتيجة باستخدام الإضافة New Relic لنرى أننا خفّضنا بشكل كبير من الوقت المستغرق في المعالجة لقاعدة البيانات بإضافة هذا الفهرس وفهارس أخرى. معدل نجاح الوصول إلى الفهارس في ذاكرة التخزين المؤقت أخيرًا لدمج الأمرين معًا، فيمكنك استخدام التعليمة التالية في حال كنت ترغب بمعرفة معدل نجاح الوصول إلى الفهارس المخزنة ضمن ذاكرة التخزين المؤقت: SELECT sum(idx_blks_read) as idx_read, sum(idx_blks_hit) as idx_hit, (sum(idx_blks_hit) - sum(idx_blks_read)) / sum(idx_blks_hit) as ratio FROM pg_statio_user_indexes; عمومًا يمكنك أن تتوقع أن يكون هذا المعدل بقيمة 99% بشكل مشابه لمعدل نجاح الوصول إلى الكاش المعتاد. خلاصة تعرفنا في هذا الفصل إلى طريقة تتبع أداء الاستعلامات في Postgres عن طريق التعليمة EXPLAIN، كما عرفنا أهمية ذاكرة التخزين المؤقتة Cache في Postgres، ويُعد هذا الفصل خطوتك الأولى في إدارة الأداء عمومًا أثناء عملك مع قواعد بيانات Postgres. اقرأ أيضًا المقال التالي: أوامر متقدمة في صدفة psql المقال السابق: أساسيات إدارة الذاكرة في قواعد بيانات Postgres النسخة الكاملة من كتاب الدليل العملي إلى قواعد بيانات PostgreSQL كيف تنقل مجلد البيانات في PostgreSQL إلى مسار مختلف في خادم أوبنتو
-
سنتعرف في هذا الفصل على الملفات التي تُخزّن فيها قواعد البيانات في Postgres، وكيف نستعرض المساحات التخزينية التي تحجزها الجداول والفهارس. مسارات تخزين البيانات لعله من البديهي أنه لا بد من وجود مكان لتخزين البيانات لأي قواعد بيانات، وهذا المكان هو ملفات موجودة في مكان ما من الخادوم الذي يشغّل عملية postgres. لمعرفة مكان تخزين البيانات من داخل صدفة psql نستخدم التعليمة التالية: hsoubguide=# SELECT name, setting FROM pg_settings WHERE category = 'File Locations'; + name | setting -------------------+---------------------------------------- config_file | /var/lib/pgsql/12/data/postgresql.conf data_directory | /var/lib/pgsql/12/data external_pid_file | hba_file | /var/lib/pgsql/12/data/pg_hba.conf ident_file | /var/lib/pgsql/12/data/pg_ident.conf (5 rows) تبيّن مخرجات التعليمة السابقة مسارات ملفات الإعدادات، كما تشير إلى المسار الخاص بالبيانات data_directory فإذا انتقلنا إلى هذا المسار واستعرضنا محتوياته سنجد ما يلي: -bash-4.2$ cd /var/lib/pgsql/12/data -bash-4.2$ ls -l total 64 drwx------. 6 postgres postgres 54 Jun 30 00:16 base -rw------- 1 postgres postgres 30 Aug 14 14:01 current_logfiles drwx------. 2 postgres postgres 4096 Aug 14 15:23 global drwx------. 2 postgres postgres 188 Jun 15 00:30 log drwx------. 2 postgres postgres 6 Jun 5 16:10 pg_commit_ts drwx------. 2 postgres postgres 6 Jun 5 16:10 pg_dynshmem -rw-------. 1 postgres postgres 4269 Jun 5 16:10 pg_hba.conf -rw-------. 1 postgres postgres 1636 Jun 5 16:10 pg_ident.conf drwx------. 4 postgres postgres 68 Aug 14 14:06 pg_logical drwx------. 4 postgres postgres 36 Jun 5 16:10 pg_multixact drwx------. 2 postgres postgres 18 Aug 14 14:01 pg_notify drwx------. 2 postgres postgres 6 Jun 5 16:10 pg_replslot drwx------. 2 postgres postgres 6 Jun 5 16:10 pg_serial drwx------. 2 postgres postgres 6 Jun 5 16:10 pg_snapshots drwx------. 2 postgres postgres 6 Aug 14 14:01 pg_stat drwx------. 2 postgres postgres 63 Aug 14 15:28 pg_stat_tmp drwx------. 2 postgres postgres 18 Jun 5 16:10 pg_subtrans drwx------. 2 postgres postgres 6 Jun 5 16:10 pg_tblspc drwx------. 2 postgres postgres 6 Jun 5 16:10 pg_twophase -rw-------. 1 postgres postgres 3 Jun 5 16:10 PG_VERSION drwx------. 3 postgres postgres 92 Jun 29 23:21 pg_wal drwx------. 2 postgres postgres 18 Jun 5 16:10 pg_xact -rw-------. 1 postgres postgres 88 Jun 5 16:10 postgresql.auto.conf -rw-------. 1 postgres postgres 26632 Jun 5 16:10 postgresql.conf -rw-------. 1 postgres postgres 58 Aug 14 14:01 postmaster.opts -rw------- 1 postgres postgres 102 Aug 14 14:01 postmaster.pid يمكنك اكتشاف الكثير عن آلية عمل postgres من الداخل في هذا المسار، ولكننا مهتمون بمعرفة مساحتها في الذاكرة، ولذا سنقوم بكتابة الأمر التالي: -bash-4.2$ du -sh /var/lib/pgsql/12/data 66M /var/lib/pgsql/12/data يُخبرنا ذلك بأن حجم جميع بيانات postgres هي 66 ميغا بايت. ولكننا قد نحتاج إلى معرفة حجم قاعدة بيانات أو جدول أو فهرس على حدة، ولذلك توفر Postgres طريقة مناسبة لمعرفة هذه المعلومات عن طريق الاستعلام من داخل صدفة psql أو عن طريق تعليمات الصدفة psql أيضًا. معرفة حجم قاعدة البيانات يمكن معرفة حجم قاعدة البيانات بشكل سهل عن طريق التوجيه \l+ الذي يعرض قائمة لقواعد البيانات مع أحجامها في العمود Size ; hsoubguide=# \l+ List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges | Size | Tablespace | Description ------------+----------+----------+-------------+-------------+-----------------------+---------+------------+---------------------------------------- ---- hsoubguide | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | | 9353 kB | pg_default | postgres | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | | 8201 kB | pg_default | default administrative connection datab ase template0 | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | =c/postgres +| 8049 kB | pg_default | unmodifiable empty database | | | | | postgres=CTc/postgres | | | template1 | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | =c/postgres +| 8049 kB | pg_default | default template for new databases | | | | | postgres=CTc/postgres | | | (4 rows) كما يمكننا تنفيذ الاستعلام التالي للحصول على حجم قاعدة بيانات محددة، وواحدة هذا الحجم هي البايت: hsoubguide=# SELECT pg_database_size('hsoubguide'); pg_database_size ------------------ 9577327 (1 row) كما يمكننا استخدام التابع pg_size_pretty لإظهار الحجم بواحدة مقروءة للمستخدم مثل kB أو MB كما يلي: hsoubguide=# SELECT pg_size_pretty(pg_database_size('hsoubguide')); pg_size_pretty ---------------- 9353 kB (1 row) معرفة حجم الجدول يمكن استخدام التعليمة \dt+ من صَدَفة psql لتظهر لك كل الجداول مع أحجامها: hsoubguide=# \dt+ List of relations Schema | Name | Type | Owner | Size | Description --------+----------------------+-------+----------+------------+------------- public | basket_a | table | postgres | 8192 bytes | public | basket_b | table | postgres | 8192 bytes | public | departments | table | postgres | 8192 bytes | public | employee_departments | table | postgres | 8192 bytes | public | employees | table | postgres | 8192 bytes | public | marks | table | postgres | 8192 bytes | public | names | table | postgres | 8192 bytes | public | phones | table | postgres | 8192 bytes | public | products | table | postgres | 16 kB | public | purchase_items | table | postgres | 328 kB | public | purchases | table | postgres | 120 kB | public | student | table | postgres | 16 kB | public | table1 | table | postgres | 8192 bytes | public | table2 | table | postgres | 8192 bytes | public | test_table | table | postgres | 8192 bytes | public | users | table | postgres | 16 kB | public | users2 | table | postgres | 8192 bytes | (17 rows) أو يمكنك استخدام الاستعلام التالي للحصول على حجم جدول محدد: hsoubguide=# SELECT pg_size_pretty(pg_relation_size('users')); pg_size_pretty ---------------- 8192 bytes (1 row) معرفة حجم الفهرس (index) يمكن تطبيق التعليمة السابقة لمعرفة حجم الفهرس كما يلي: hsoubguide=# SELECT pg_size_pretty(pg_relation_size('users_pkey')); pg_size_pretty ---------------- 16 kB (1 row) قياس حجم الجدول مع الفهارس تُخزّن الفهارس بمعزل عن الجداول، لكن يمكنك معرفة الحجم الكلي للجدول بالإضافة للفهارس بالتعليمة التالية: hsoubguide=# SELECT pg_size_pretty(pg_total_relation_size('users')); pg_size_pretty ---------------- 32 kB (1 row) ماذا تعني هذه الأرقام؟ عندما تُخبرنا Postgres بأن حجم الجدول 32KB فإن هذا الرقم ليس هو بالفعل حجم البيانات المخزنة فيه، ولكنه الحجم الذي يحجزه الجدول في الذاكرة. ولفهم ذلك، سنقوم بإنشاء جدول بسيط ومراقبة تغير حجمه مع زيادة البيانات فيه أو نقصها: hsoubguide=# CREATE TABLE size_calc(msg VARCHAR(255)); CREATE TABLE حجم الجدول الآن في الذاكرة، هو 0 بايت : hsoubguide=# SELECT pg_size_pretty(pg_total_relation_size('size_calc')); pg_size_pretty ---------------- 0 bytes (1 row) سنقوم بإضافة نص من 64 حرفًا: hsoubguide=# INSERT INTO size_calc(msg) VALUES ('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ..'); INSERT 0 1 وسنجد أن حجم الجدول قفز إلى 8kB فورًا: hsoubguide=# SELECT pg_size_pretty(pg_total_relation_size('size_calc')); pg_size_pretty ---------------- 8192 bytes (1 row) ولكن ماذا لو قمنا بحذف محتويات الجدول؟ hsoubguide=# SELECT * FROM size_calc; msg ------------------------------------------------------------------ 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.. (1 row) hsoubguide=# DELETE FROM size_calc; DELETE 1 hsoubguide=# SELECT * FROM size_calc; msg ----- (0 rows) رأينا أن الجدول فارغٌ الآن من المحتوى، ولكن حجمه في الذاكرة لا زال 8kB كما نرى في الاستعلام التالي: hsoubguide=# SELECT pg_size_pretty(pg_total_relation_size('size_calc')); pg_size_pretty ---------------- 8192 bytes (1 row) إلا أنه يمكننا استخدام المكنسة الكهربائية لقواعد البيانات، التعليمة VACUUM FULL التي تنظّف الذاكرة من الملفات الفارغة، أو تلك التي تمددت في وقت ما، ثم لم يعد هناك حاجة إلى حجمها الكبير، كما في المثال التالي: hsoubguide=# VACUUM FULL; VACUUM hsoubguide=# SELECT pg_size_pretty(pg_total_relation_size('size_calc')); pg_size_pretty ---------------- 0 bytes (1 row) خلاصة لا شك أن هذا الفصل قد ساعدك في التعرف إلى كيفية تتبع استخدام الذاكرة في قاعدة بياناتك، وقد أصبح بإمكانك معرفة الحجم الذي تستهلكه الفهارس، والحجم الذي تستهلكه الجداول عمومًا، مما سيساعدك لاحقًا في إدارة ذاكرة التخزين لقاعدة البيانات ككل. اقرأ أيضًا المقال التالي: كيفية إدارة الأداء في قواعد بيانات Postgres المقال السابق: إدارة النسخ الاحتياطي في قواعد بيانات Postgres النسخة الكاملة من كتاب الدليل العملي إلى قواعد بيانات PostgreSQL
-
نتعرف في هذا الفصل إلى كيفية أخذ نسخة احتياطية عن قاعدة البيانات الخاصة بنا، ثم نقوم باستعادتها، كما نتعرف إلى الأمر \copy وكيف نستخدمه لتحديد نمط البيانات في النسخة الاحتياطية. النسخ الاحتياطي والاستعادة النسخ الاحتياطي هو أخذ نسخة كاملة عن مخططات الجداول وبيانات قاعدة البيانات، أما الاستعادة فهي القدرة على استخدام هذه البيانات التي تم نسخها احتياطيًا وتحميلها إلى قاعدة البيانات الخاصة بك أو قاعدة بيانات اخرى. ملاحظة: تتم عملية النسخ الاحتياطي والاستعادة على قاعدة بيانات بأكملها أو على جدول بأكمله وليس الهدف منها استخلاص أجزاء من البيانات فقط، ففي تلك الحالة نستخدم النسخ (copy) الذي سنتحدث عنه لاحقًا. إجراء النسخ الاحتياطي لأخذ نسخة احتياطية من قاعدة البيانات نستخدم الأداة pg_dump، وعلينا تحديد بعض الإعدادات لتحديد نتيجة عملية النسخ، ومنها: هل نريد أن تكون النتيجة على شكل نص عادي (قابل للقراءة ولكنه كبير الحجم) أو بصيغة ثنائية (غير قابلة للقراءة صغيرة الحجم) أو بصيغة مضغوطة tarball (مثالي للقيام بعملية الاستعادة). هل نرغب بنسخ كل قاعدة البيانات أم مخططات (schema) أو جداول معينة. قبل البدء بالنسخ الاحتياطي قد ترغب باستعراض قواعد البيانات المخزنة لديك، باستخدام الأمر التالي: bash-4.2$ psql -l List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges -------------+----------+----------+-------------+-------------+----------------------- hsoubguide | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | hsoubguide2 | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | postgres | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | template0 | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | =c/postgres + | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | =c/postgres + | | | | | postgres=CTc/postgres (5 rows) لاحظ أن العمل يتم ضمن صدفة bash في هذه الفقرة. يمكنك القيام بعملية النسخ الاحتياطي باستخدام التعليمة التالية: pg_dump database_name_here > database.sql فمثلًا سنقوم بأخذ نسخة احتياطية من قاعدة البيانات hsoubguide التي كنا نعمل عليها خلال هذا الدليل كما يلي: bash-4.2$ pg_dump hsoubguide > /tmp/hsoubguide.sql خزّنّا نسخة قاعدة البيانات في ملف اسمه hsoubguide.sql ضمن المجلد /tmp، يمكنك مراجعة دليل كيف تفهم هيكلية نظام الملفات في لينكس للتعرف أكثر إلى دور المجلد tmp وغيره في نظام لينكس. تنتج التعليمة السابقة نسخة عن قاعدة البيانات على شكل نص عادي، سنستعرض أول 50 سطرًا منها من باب الاطلاع: bash-4.2$ cat /tmp/hsoubguide.sql | head -50 -- -- PostgreSQL database dump -- -- Dumped from database version 12.3 -- Dumped by pg_dump version 12.3 SET statement_timeout = 0; SET lock_timeout = 0; SET idle_in_transaction_session_timeout = 0; SET client_encoding = 'UTF8'; SET standard_conforming_strings = on; SELECT pg_catalog.set_config('search_path', '', false); SET check_function_bodies = false; SET xmloption = content; SET client_min_messages = warning; SET row_security = off; -- -- Name: hstore; Type: EXTENSION; Schema: -; Owner: - -- CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA public; -- -- Name: EXTENSION hstore; Type: COMMENT; Schema: -; Owner: -- COMMENT ON EXTENSION hstore IS 'data type for storing sets of (key, value) pairs'; SET default_tablespace = ''; SET default_table_access_method = heap; -- -- Name: basket_a; Type: TABLE; Schema: public; Owner: postgres -- CREATE TABLE public.basket_a ( id integer NOT NULL, fruit character varying(100) NOT NULL ); ALTER TABLE public.basket_a OWNER TO postgres; -- -- Name: basket_b; Type: TABLE; Schema: public; Owner: postgres ولكن بما أن النسخة الاحتياطية مكتوبة بشكلها المقروء، فإن حجمها ليس بالقليل: bash-4.2$ du -h /tmp/hsoubguide.sql 212K /tmp/hsoubguide.sql بلغ حجم النسخة الاحتياطية 212 كيلو بايت تقريبًا. لإنشاء نسخة احتياطية أكثر ملاءمة للتخزين الدائم، يمكنك استخدام الضغط لحفظ قاعدة البيانات بشكلها الثنائي bash-4.2$ pg_dump --format=c hsoubguide > /tmp/hsoubguide.bak bash-4.2$ du -h /tmp/hsoubguide.bak 76K /tmp/hsoubguide.bak تمكّنّا من ضغط قاعدة البيانات 2.8 مرات تقريبا، وذلك بسبب استخدام الراية format مع الحرف c الذي يدل على الكلمة custome. للتوسع في استخدام التطبيق pg_dump يمكنك الرجوع إلى توثيق Postgres الرسمي. استعادة البيانات من نسخة احتياطية ما رأيك أن نبدأ هذه الفقرة بحركة خطيرة، يمكن أن تؤدي إلى طردك من العمل؟ سنقوم بحذف قاعدة البيانات! bash-4.2$ dropdb hsoubguide في حال لم يكن لديك نسخة احتياطية من قاعدة البيانات، فيمكن أن يكون الأمر السابق هو آخر ما تقوم به في عملك، ولكنك قد أخذت نسخة احتياطية بالفعل باستخدام pg_dump في الفقرة السابقة، فلنقم الآن بالاستعادة. ملاحظة: يجدر بك تعلّم كيفية عمل نسخ احتياطي دوري تلقائي عن طريق مهام cron في لينكس، وقد يفيدك مقال 10 أمثلة لجدولة المهام باستخدام Cron في الأكاديمية. هناك العديد من الخيارات عند استعادة قاعدة البيانات، ولكن عملية الاستعادة لا يمكنها العمل دون وجود قاعدة بيانات بالفعل ضمن العنقود الخاص بك، لذا عليك إنشاءها ثم الاستعادة إليها كما يلي: bash-4.2$ createdb hsoubguide bash-4.2$ pg_restore --format=c --dbname=hsoubguide /tmp/hsoubguide.bak للتوسع في استخدام التطبيق pg_restore يمكنك الرجوع إلى توثيق Postgres الرسمي. النسخ (Copy) يُرفق Postgres بالعديد من الأدوات المساعدة لنقل البيانات أشهرها pg_dump و pg_restore لأخذ نسخة احتياطية من قاعدة البيانات واستعادتها التي تعرفنا عليها في الفقرة السابقة. كما أن هناك أداة مشابهة بنفس القدر من الأهمية إلا أنها أقل شهرة هي أداة Postgres للنسخ التي تسمح بنسخ البيانات من الجداول في قاعدة البيانات وإليها، وتدعم هذه الأداة عدة أنماط، منها: النمط الثنائي نمط الجدولة باستخدام tab نمط csv، للجدولة باستخدام الفاصلة , قد تحتاج إلى هذه الأداة يومًا ما سواءً لتحميل كتل من البيانات للتجربة، أو القيام ببعض من عمليات ETL، أو حتى لاستخراج البيانات لإرسالها إلى جهة ما. أمثلة عملية لاستخدام الأداة Copy سنبين في هذه الفقرة بعض الأمثلة لشرح طريقة استخدام هذه الأداة، فمثلًا لاستخراج جميع الموظفين إلى ملف مُجدول بواسطة tab: hsoubguide=# \copy (SELECT * FROM employees) TO '~/employees.tsv'; COPY 5 hsoubguide=# quit bash-4.2$ ls -l total 52 drwx------. 4 postgres postgres 51 Jun 15 02:22 12 -rw-rw-r--. 1 postgres postgres 75 Jun 30 06:12 employees.tsv -rw-r--r--. 1 postgres postgres 46429 Jun 5 16:48 example.dump bash-4.2$ cat employees.tsv 1 Jones 45000 2 Adams 50000 3 Johnson 40000 4 Williams 37000 5 Smith 55000 استخراج جميع الموظفين إلى ملف csv: hsoubguide=# \copy (SELECT * FROM employees) TO '~/employees.csv' WITH (FORMAT CSV); COPY 5 hsoubguide=# quit bash-4.2$ ls -l employees.csv -rw-rw-r--. 1 postgres postgres 75 Jun 30 06:13 employees.csv bash-4.2$ cat employees.csv 1,Jones,45000 2,Adams,50000 3,Johnson,40000 4,Williams,37000 5,Smith,55000 استخراج جميع الموظفين إلى ملف ثنائي (لاحظ علامات التنصيص المزدوجة حول الكلمة Binary): hsoubguide=# \copy (SELECT * FROM employees) TO '~/employees.dat' WITH (FORMAT "binary"); COPY 5 hsoubguide=# quit bash-4.2$ ls -l employees.dat -rw-rw-r--. 1 postgres postgres 161 Jun 30 06:16 employees.dat bash-4.2$ hexdump employees.dat 0000000 4750 4f43 5950 ff0a 0a0d 0000 0000 0000 0000010 0000 0000 0003 0000 0004 0000 0001 0000 0000020 4a05 6e6f 7365 0000 0400 0000 c8af 0300 0000030 0000 0400 0000 0200 0000 0500 6441 6d61 0000040 0073 0000 0004 c300 0050 0003 0000 0004 0000050 0000 0003 0000 4a07 686f 736e 6e6f 0000 0000060 0400 0000 409c 0300 0000 0400 0000 0400 0000070 0000 0800 6957 6c6c 6169 736d 0000 0400 0000080 0000 8890 0300 0000 0400 0000 0500 0000 0000090 0500 6d53 7469 0068 0000 0004 d600 ffd8 00000a0 00ff 00000a1 ولتحميل البيانات من الملفات إلى جدول، فالأسطر التالية تعاكس العمليات السابقة بالترتيب: \copy employees FROM '~/employees.tsv'; \copy employees FROM '~/employees.csv' WITH CSV; \copy employees FROM '~/employees.dat' WITH BINARY; لتجربة الأوامر السابقة سنقوم بحذف محتويات الجدول واستعادتها كما يلي: hsoubguide=# DELETE FROM employees; DELETE 5 hsoubguide=# \copy employees FROM '~/employees.csv' WITH CSV; COPY 5 hsoubguide=# SELECT * from employees; id | last_name | salary ----+-----------+-------- 1 | Jones | 45000 2 | Adams | 50000 3 | Johnson | 40000 4 | Williams | 37000 5 | Smith | 55000 (5 rows) إن هذه التعليمة \copy مهمة خصوصًا عند الحاجة إلى إنشاء الجدول خارج صدفة psql عن طريق الكتابة على ملف ما ثم إدخال هذه البيانات مباشرةً إلى جدول في قاعدة البيانات. ولكن تجدر الإشارة إلى أنه لا يمكن استعادة جدول إلا إذا كان الجدول معرّفًا من قبل في قاعدة البيانات، فكما نرى، فإن البيانات المخزنة في الملفات ليست سوى بيانات الجدول دون تعريف لمخطط الجدول. خلاصة إن إدارة النسخ الاحتياطي لقاعدة البيانات أمر شديد الأهمية، ولن يقدّر أحد أهميته كما يقدرها من فقد بياناته دون أخذه مسبقًا لنسخة احتياطية، وقد تعرفنا في هذا الفصل على الأدوات المساعدة على ذلك، فاستخدمها بشكل دوري، ولا تعرض بياناتك لخطر الزوال. اقرأ أيضًا المقال التالي: أساسيات إدارة الذاكرة في قواعد بيانات Postgres المقال السابق: أنواع بيانات خاصة في قواعد بيانات Postgres النسخة الكاملة من كتاب الدليل العملي إلى قواعد بيانات PostgreSQL شرح التكرار في نظام قواعد البيانات PostgreSQL على توزيعة أوبنتو
-
تتميز Postgres بإضافة عدة أنواع بيانات مميزة، نتحدث عنها في هذا الفصل، وهي المصفوفات Arrays والنوع Hstore والنوع JSONB وهي تساهم بشكل أساسي بالسماح بتخزين هيكل بيانات أكبر من مجرد قيمة واحدة في العمود في الجدول، كما سنستخدم الأنواع التعدادية ENUM لتحديد قيم مخصصة في أعمدة جداولنا. المصفوفات (Arrays) تتيح Postgres تخزين بيانات على شكل مصفوفات متغيرة الطول ضمن عمود واحد، حيث يمكن أن يكون نوع المصفوفات من الأنواع الأساسية، أو نوعًا جديدًا يحدده المستخدم أو من الأنواع التعدادية (enumerated). لتحديد عمود ما لتخزين مصفوفة، نقوم بوضع قوسين [] بعد اسم النوع كما يلي: hsoubguide=# CREATE TABLE hsoub_team hsoubguide-# ( hsoubguide(# team_name text, hsoubguide(# team_members text[] hsoubguide(# ); CREATE TABLE يُنشئ الأمر السابق جدولًا اسمه hsoub_team له عمودان، أحدهما نص نخزّن فيه اسم الفريق، وعمود آخر team_members يخزن مصفوفة أحادية البعد لتخزين أسماء أعضاء الفريق. إدخال قيم المصفوفات hsoubguide=# INSERT INTO hsoub_team hsoubguide-# VALUES hsoubguide-# ('postgres_team', hsoubguide(# '{"mostafa","jamil","abood"}' hsoubguide(# ); INSERT 0 1 لاحظ أن السلاسل النصية بداخل المصفوفة تكون محصورة بعلامات تنصيص مزدوجة، وذلك لأن قيمة المصفوفة كاملة هي التي تُكتب بين علامات تنصيص مفردة. سيُظهر استعلام الجدول ما يلي: hsoubguide=# SELECT * FROM hsoub_team ; team_name | team_members ---------------+----------------------- postgres_team | {mostafa,jamil,abood} (1 row) كما يمكن بناء المصفوفات بطريقة ثانية، عن طريق استخدام باني المصفوفات (Constructor) كما يلي: hsoubguide=# INSERT INTO hsoub_team hsoubguide-# VALUES hsoubguide-# ('C++ team', hsoubguide(# ARRAY['mostafa','yougharta'] hsoubguide(# ); INSERT 0 1 عند استخدام باني المصفوفات يتم حصر السلاسل النصية بعلامات تنصيص مفردة، ولا شك أن هذه الطريقة أكثر وضوحًا وأسهل مقروئيّة. يُعطي استعلام الجدول الآن ما يلي: hsoubguide=# SELECT * FROM hsoub_team ; team_name | team_members ---------------+----------------------- postgres_team | {mostafa,jamil,abood} C++ team | {mostafa,yougharta} (2 rows) ولكن لماذا نستخدم المصفوفات؟ ألم يكن بإمكاننا تخزين محتويات المصفوفة كلها كنص عادي؟ إن ميزة المصفوفات تأتي من القدرة على اختيار عنصر محدد من المصفوفة والوصول إليه، كما سنرى في الفقرة التالية. الوصول إلى عناصر من المصفوفة يمكن اختيار عناصر المصفوفة عن طريق رقم العنصر للوصول إلى عنصر مفرد، أو باستخدام المجال (من العنصر: إلى العنصر) كما يلي: hsoubguide=# SELECT * FROM hsoub_team WHERE team_members[2] = 'jamil'; team_name | team_members ---------------+----------------------- postgres_team | {mostafa,jamil,abood} (1 row) hsoubguide=# SELECT * FROM hsoub_team WHERE team_members[2:3] = ARRAY['jamil','abood']; team_name | team_members ---------------+----------------------- postgres_team | {mostafa,jamil,abood} (1 row) hsoubguide=# SELECT team_members[2:3] FROM hsoub_team ; team_members --------------- {jamil,abood} {yougharta} (2 rows) ملاحظة: يجب الانتباه إلى أن ترقيم عناصر المصفوفة يبدأ من 1 وليس من 0 كما في بعض لغات البرمجة. تعديل قيم عناصر المصفوفات يمكن تعديل قيمة عنصر واحد في المصفوفة، كما يمكن تعديل المصفوفة كاملةً، أو مجالًا محدّدًا منها. فلتعديل عنصر واحد في المصفوفة، نستخدم الوصول إلى العنصر المُراد تعديله كما يلي: hsoubguide=# UPDATE hsoub_team SET team_members[3]='new_member'; UPDATE 2 hsoubguide=# SELECT * FROM hsoub_team ; team_name | team_members ---------------+-------------------------------- postgres_team | {mostafa,jamil,new_member} C++ team | {mostafa,yougharta,new_member} (2 rows) يمكن الوصول إلى عنصر خارج حدود المصفوفة كما في المثال السابق، ولكن يمكن كذلك تجاوز هذا الحد لتعديل العنصر رقم 5 مثلًا في مصفوفة من 3 عناصر فقط، عندها يتم ملء العناصر الفارغة بقيمة NULL كما في المثال التالي: hsoubguide=# UPDATE hsoub_team SET team_members[5]='new_member2'; UPDATE 2 hsoubguide=# SELECT * FROM hsoub_team ; team_name | team_members ---------------+------------------------------------------------ postgres_team | {mostafa,jamil,new_member,NULL,new_member2} C++ team | {mostafa,yougharta,new_member,NULL,new_member2} (2 rows) أما لتعديل المصفوفة كاملةً فلا نستخدم الوصول إلى عنصر مفرد، بل نقوم بتغيير قيمة العمود كاملةً: hsoubguide=# UPDATE hsoub_team SET team_members = ARRAY ['a','b'] WHERE team_name ='C++ team'; UPDATE 1 hsoubguide=# SELECT * FROM hsoub_team ; team_name | team_members ---------------+-------------------------------------------- postgres_team | {mostafa,jamil,new_member,NULL,new_member} C++ team | {a,b} (2 rows) كما يمكن تغيير قيمة مجالٍ من المصفوفة كما يلي: hsoubguide=# UPDATE hsoub_team SET team_members[2:3] = ARRAY['x','y'] WHERE team_name LIKE 'postgres%'; UPDATE 1 hsoubguide=# SELECT * FROM hsoub_team ; team_name | team_members ---------------+------------------------------- C++ team | {a,b} postgres_team | {mostafa,x,y,NULL,new_member} (2 rows) ملاحظة: يجب أن يكون حجم المجال المُستبدل مساويًا أو أصغر من طول المصفوفة الجديدة، فلو حاولنا استبدال مجالٍ بمجالٍ أصغر منها سيظهر الخطأ التالي: hsoubguide=# UPDATE hsoub_team SET team_members[2:4] = ARRAY['one_member'] WHERE team_name LIKE 'postgres%'; ERROR: source array too small أما إن كان حجم المصفوفة الجديدة أكبر من الأصلية، فيتم أخذ عدد من العناصر مساوٍ للمجال الأصلي، كما يلي: hsoubguide=# UPDATE hsoub_team SET team_members[2:4] = ARRAY['one_member','a','b','c','d','e'] WHERE team_name LIKE 'postgres%'; UPDATE 1 hsoubguide=# SELECT * FROM hsoub_team ; team_name | team_members ---------------+------------------------------------- C++ team | {a,b} postgres_team | {mostafa,one_member,a,b,new_member} (2 rows) البحث ضمن المصفوفات للبحث عن عنصر معين ضمن المصفوفة نستخدم الكلمة المفتاحية ANY كما يوضح المثال التالي: hsoubguide=# SELECT * FROM hsoub_team WHERE 'mostafa' = ANY(team_members); team_name | team_members ---------------+------------------------------------- postgres_team | {mostafa,one_member,a,b,new_member} (1 row) ويمكن البحث للتحقق من كون كل قيم المصفوفة تطابق قيمة معينة باستخدام الكلمة ALL. hsoubguide=# INSERT INTO hsoub_team hsoubguide-# VALUES hsoubguide-# ('team1', hsoubguide(# ARRAY['programmer1','programmer1','programmer1'] hsoubguide(# ); INSERT 0 1 hsoubguide=# SELECT * FROM hsoub_team WHERE 'programmer1' = ALL(team_members); team_name | team_members -----------+--------------------------------------- team1 | {programmer1,programmer1,programmer1} (1 row) كما يمكن استخدام ALL مع تحديد المجال ضمن المصفوفة كما يلي: hsoubguide=# INSERT INTO hsoub_team VALUES ('team7', ARRAY['programmer1','programmer1','another_programmer'] ); INSERT 0 1 hsoubguide=# SELECT * FROM hsoub_team WHERE 'programmer1' = ALL(team_members[1:2]); team_name | team_members -----------+---------------------------------------------- team1 | {programmer1,programmer1,programmer1} team7 | {programmer1,programmer1,another_programmer} (2 rows) أنواع البيانات التعدادية (Enumerated Data Types) توفر Postgres نوع بيانات تعدادية enums تُستخدم لحصر قيم عمود ما في مجموعة قيم محددة مسبقًا من القيم. سنقوم في المثال التالي بحصر قيم العمود contact_method بمجموعة القيم Email و SMS و Phone، وذلك عن طريق تعريف التعداد كما يلي: hsoubguide=# CREATE TYPE e_contact_method AS ENUM ( hsoubguide(# 'Email', hsoubguide(# 'Sms', hsoubguide(# 'Phone'); CREATE TYPE ومن ثم نرفق نوع التعداد الجديد بالعمود الذي نريد حصر قيمه كما يلي: hsoubguide=# CREATE TABLE contact_method_info ( hsoubguide(# contact_name text, hsoubguide(# contact_method e_contact_method, hsoubguide(# value text hsoubguide(# ); CREATE TABLE استخدام الأنواع التعدادية سنحاول في هذا المثال إدخال قيم في العمود الذي يستخدم النوع التعدادي، لنرى ما يحصل عند الخروج عن القيم المحددة مسبقًا: hsoubguide=# INSERT INTO contact_method_info hsoubguide-# VALUES ('Jamil', 'Email', 'jamil@mail.com'); INSERT 0 1 hsoubguide=# SELECT * FROM contact_method_info ; contact_name | contact_method | value --------------+----------------+---------------- Jamil | Email | jamil@mail.com (1 row) لا يمكن إدراج قيمة للعمود contact_method غير موجودة سلفًا ضمن التعداد e_contact_method وسيظهر خطأ كما في المثال التالي: hsoubguide=# INSERT INTO contact_method_info VALUES ('Jamil', 'Fax', '4563456'); ERROR: invalid input value for enum e_contact_method: "Fax" LINE 1: INSERT INTO contact_method_info VALUES ('Jamil', 'Fax', '456... عرض وتعديل قيم التعداد يمكننا عرض قائمة القيم في التعداد بالاستعانة بالجداول pg_type و pg_enum التي تُخزّن إعدادات الأنواع والتعدادات، وذلك كما يلي: hsoubguide=# SELECT pg_type.typname, pg_enum.enumlabel hsoubguide-# FROM pg_type,pg_enum hsoubguide-# WHERE pg_type.oid = pg_enum.enumtypid; typname | enumlabel ------------------+----------- e_contact_method | Email e_contact_method | Sms e_contact_method | Phone (3 rows) كما يمكن إضافة قيم للتعدادات الموجودة مسبقًا كما يلي: hsoubguide=# ALTER TYPE e_contact_method hsoubguide-# ADD VALUE 'Facebook' AFTER 'Phone'; ALTER TYPE hsoubguide=# SELECT pg_type.typname, pg_enum.enumlabel hsoubguide-# FROM pg_type,pg_enum hsoubguide-# WHERE pg_type.oid = pg_enum.enumtypid; typname | enumlabel ------------------+----------- e_contact_method | Email e_contact_method | Sms e_contact_method | Phone e_contact_method | Facebook (4 rows) يتم حفظ ترتيب القيم داخل التعدادات بنفس الترتيب الذي تم إدخال القيم به، ولكن يمكن إدخال قيم جديدة وتحديد مكانها قبل قيمة معينة أو بعدها، كما في المثال التالي: hsoubguide=# ALTER TYPE e_contact_method hsoubguide-# ADD VALUE 'Twitter' BEFORE 'Sms'; ALTER TYPE hsoubguide=# SELECT pg_type.typname,pg_enum.enumlabel,pg_enum.enumsortorder hsoubguide-# FROM pg_type, pg_enum hsoubguide-# WHERE pg_type.oid = pg_enum.enumtypid hsoubguide-# ORDER BY pg_enum.enumsortorder; typname | enumlabel | enumsortorder ------------------+-----------+--------------- e_contact_method | Email | 1 e_contact_method | Twitter | 1.5 e_contact_method | Sms | 2 e_contact_method | Phone | 3 e_contact_method | Facebook | 4 (5 rows) لا تسمح Postgres بإزالة قيم من التعدادات ولا بتغيير ترتيبها، وللقيام بذلك علينا حذف التعداد عن طريق التعليمة DROP TYPE، ولكن انتبه إلى أنه لا يمكنك حذف التعداد إذا كان هناك أعمدة تستخدم هذا النوع، لكن يمكنك حذف التعداد مع جميع الأعمدة المرتبطة به باستخدام الكلمة المفتاحية CASCADE، ولتوضيح ذلك لدينا المثال التالي: hsoubguide=# DROP TYPE e_contact_method CASCADE; NOTICE: drop cascades to column contact_method of table contact_method_info DROP TYPE hsoubguide=# SELECT pg_type.typname, pg_enum.enumlabel FROM pg_type,pg_enum WHERE pg_type.oid = pg_enum.enumtypid; typname | enumlabel ---------+----------- (0 rows) hsoubguide=# SELECT * FROM contact_method_info ; contact_name | value --------------+---------------- Jamil | jamil@mail.com (1 row) نوع البيانات HStore HStore هو أسلوب تخزين (مفتاح، قيمة) ضمن Postgres يُستخدم مثل القاموس، لكنه مخصص لعمود في سطر ما. تفعيل HStore لتفعيل HStore في قاعدة البيانات قم بتنفيذ الأمر التالي: hsoubguide=# CREATE EXTENSION hstore; CREATE EXTENSION إنشاء عمود HStore لإنشاء حقل في جدول ذو نوع بيانات HStore استخدم HStore كنوع للعمود ببساطة كما يلي: hsoubguide=# CREATE TABLE students ( hsoubguide-# id serial PRIMARY KEY, hsoubguide-# name varchar, hsoubguide-# attributes hstore hsoubguide-# ); CREATE TABLE إدخال بيانات من نوع HStore لإدخال البيانات عليك كتابتها ضمن علامات تنصيص مفردة. الفرق في HStore هو بنية إضافية لتوضيح كيفية إنشاء القاموس: hsoubguide=# INSERT INTO students (name,attributes) VALUES( hsoubguide(# 'mostafa', hsoubguide(# 'nickname => mayesh, hsoubguide'# grade => 12, hsoubguide'# school => "Hsoub Academy", hsoubguide'# weight => 82' hsoubguide(# ); INSERT 0 1 إن استخدام النوع HStore كان من أوائل المحاولات في الخروج عن هيكلة قواعد البيانات، ومن ثم فهو من أوائل أنواع NoSQL التي ظهرت، فليس هناك محددات للعناصر التي يمكننا تخزينها في HStore. الوصول إلى بيانات من نوع HStore يمكننا استخدام العملية <- للوصول إلى عناصر في داخل العنصر من النوع Hstore، وذلك بتحديد اسم المفتاح كما يلي: hsoubguide=# SELECT name,attributes->'school' hsoubguide=# FROM students hsoubguide=# WHERE attributes->'nickname' = 'mayesh'; name | ?column? ---------+--------------- mostafa | Hsoub Academy (1 row) لتغيير اسم العمود بدلًا من ?column? استخدم AS كما يلي: hsoubguide=# SELECT name,attributes->'school' AS school FROM students WHERE attributes->'nickname' = 'mayesh'; name | school ---------+--------------- mostafa | Hsoub Academy (1 row) بما أنه لا توجد قيود على المفاتيح المخزنة بداخل النوع Hstore فإنه من الممكن أن يوجد مفتاحُ ما في سطر ما، ولا يوجد في سطر آخر، وعندها يُتعامل معه على أنه موجود بقيمة خالية، كما في المثال التالي: hsoubguide=# INSERT INTO students (name,attributes) VALUES( hsoubguide(# 'Jamil', hsoubguide(# 'grade => 13 hsoubguide'# , hsoubguide'# weight => 72'); INSERT 0 1 hsoubguide=# SELECT * FROM students; id | name | attributes ----+---------+-------------------------------------------------------------------------------- 1 | mostafa | "grade"=>"12", "school"=>"Hsoub Academy", "weight"=>"82", "nickname"=>"mayesh" 2 | Jamil | "grade"=>"13", "weight"=>"72" (2 rows) hsoubguide=# SELECT name,attributes->'school' AS school FROM students; name | school ---------+--------------- mostafa | Hsoub Academy Jamil | (2 rows) يتيح استخدام Hstore مرونة عالية في قاعدة البيانات، ولكنها أصبحت تقنية قديمة مقارنةً بتقنية النوع JSONB التي سنتحدث عنها في الفقرة التالية. بيانات بصيغة JSON ظهر استخدام JSON في postgres بدءًا من الإصدار 9.2، لكن الإصدار الحقيقي ظهر باسم JSONB في postgres 9.4. JSNOB هي الصيغة الثنائية لتعابير JSON للتخزين الدائم، فهي أكثر كفاءة في التخزين والفهرسة. إنشاء أعمدة JSONB لإنشاء أعمدة من النوع JSONB حدد النوع JSONB ضمن تعليمة CREATE TABLE كما يلي: hsoubguide=# CREATE TABLE school ( hsoubguide(# id serial PRIMARY KEY, hsoubguide(# name varchar, hsoubguide(# attributes JSONB hsoubguide(# ); CREATE TABLE إدخال البيانات من النوع JSONB يُفترض أن يكون إدخال عمود يحتوي على صيغة JSON سهلًا ومباشرًا، ونوضّحه بالمثال التالي: hsoubguide=# INSERT INTO school (name,attributes) VALUES ( hsoubguide(# 'Hsoub', '{ hsoubguide'# "manager" : "Agha", hsoubguide'# "classes" : 7, hsoubguide'# "teachers": 12}' hsoubguide(# ); INSERT 0 1 hsoubguide=# SELECT * FROM school; id | name | attributes ----+-------+--------------------------------------------------- 1 | Hsoub | {"classes": 7, "manager": "Agha", "teachers": 12} (1 row) الوصول إلى قيم المفاتيح في JSONB يمكننا استخدام العملية <- للوصول إلى القيم عن طريق أسماء مفاتيحها، كما يلي: hsoubguide=# SELECT name,attributes->'manager' AS manager FROM school; name | manager -------+--------- Hsoub | "Agha" (1 row) ويمكننا استخدامها داخل شروط التصفية كذلك: hsoubguide=# SELECT * FROM school WHERE attributes->'classes' = '7'; id | name | attributes ----+-------+--------------------------------------------------- 1 | Hsoub | {"classes": 7, "manager": "Agha", "teachers": 12} (1 row) هناك العديد من الأمور المتقدمة التي يمكن القيام بها في JSONB، ولكن يجب أن نعرف أنها مخصصة لتخزين العناصر والوصول إليها، ولكنها لم تصمم لتعديل العناصر بعد تخزينها. التعامل مع التاريخ والوقت يمكن تخزين التاريخ والوقت في Postgres باستخدام عدة أنواع، كما تتيح العديد من الطرق للتعامل المرن مع التواريخ والأوقات، النوع الأساسي لتخزين التاريخ هو DATE ولتخزين الوقت TIME، ويمكننا حفظ التاريخ مع الوقت باستخدام النوع TIMESTAMP. سننشئ جدولًا جديدًا لتعلم التعامل مع هذه الأنواع الجديدة: hsoubguide=# CREATE TABLE date_example( hsoubguide(# mydate DATE NOT NULL DEFAULT CURRENT_DATE, hsoubguide(# mytime TIME NOT NULL DEFAULT CURRENT_TIME, hsoubguide(# mytimestamp TIMESTAMP NOT NULL DEFAULT NOW() hsoubguide(# ); CREATE TABLE hsoubguide=# SELECT * FROM date_example; mydate | mytime | mytimestamp --------+--------+------------- (0 rows) أنشأنا في الجدول السابق ثلاث أعمدة، لكل منها قمنا بمنع تخزين القيمة الخالية عن طريق NOT NULL، واستخدمنا القيم الافتراضية التالية: التاريخ الحالي، باستخدام CURRENT_DATE الوقت الحالي، باستخدام CURRENT_TIME التاريخ والوقت الحالي، باستخدام التابع NOW() سندخل الآن سطرًا مميزًا، حيث لن نحدد فيه أي قيمة، بل سنستخدم التوجيه DEFAULT VALUES كي يتم إدخال جميع القيم الافتراضية كما يلي: hsoubguide=# INSERT INTO date_example DEFAULT VALUES; INSERT 0 1 hsoubguide=# SELECT * FROM date_example; mydate | mytime | mytimestamp ------------+-----------------+---------------------------- 2020-08-22 | 01:24:08.241482 | 2020-08-22 01:24:08.241482 (1 row) كما يظهر استعلام الجدول السابق، يتم إظهار التاريخ بالتنسيق yyyy-mm-dd. استخدام لتابع NOW() يمكننا استخدام التابع NOW() للاستعلام عن التاريخ والوقت كما يلي: hsoubguide=# SELECT NOW(); now ------------------------------- 2020-08-22 01:26:48.875054+03 (1 row) hsoubguide=# SELECT NOW()::DATE; now ------------ 2020-08-22 (1 row) hsoubguide=# SELECT NOW()::TIME; now ----------------- 01:26:55.072126 (1 row) استخدام التابع TO_CHAR() كما يمكننا تحديد النسق الذي نرغب بعرض التاريخ فيه من خلال التابع TO_CHAR() كما يلي: hsoubguide=# SELECT TO_CHAR(NOW()::DATE,'dd ++ mm ++ yyyy'); to_char ------------------ 22 ++ 08 ++ 2020 (1 row) hsoubguide=# SELECT TO_CHAR(NOW()::DATE,'Month dd/mm/yyyy'); to_char ---------------------- August 22/08/2020 (1 row) يمكننا استخدام التابع AGE() لحساب فارق التاريخ وإظهاره بنفس تنسيق التاريخ كما يلي: hsoubguide=# SELECT AGE(CURRENT_DATE,'25-07-1993'); age ------------------ 27 years 28 days (1 row) كما يمكننا استخدام عملية الطرح للتواريخ - للحصول على الفرق بالأيام كما يلي: hsoubguide=# SELECT CURRENT_DATE-'25-07-1993' AS days; days ------ 9890 (1 row) للحصول على فارق الوقت كذلك علينا تحديد نوع البيانات التي نقوم بطرحها على أنها بيانات وقت: hsoubguide=# SELECT time '10:57:18' - time '02:17:17' AS result; result ---------- 08:40:01 (1 row) ملاحظة: انتبه إلى إضافة النوع time قبل الوقت المطروح، وإلا سيظهر الخطأ التالي: hsoubguide=# SELECT '10:57:18' - '02:17:17' AS result; ERROR: operator is not unique: unknown - unknown LINE 1: SELECT '10:57:18' - '02:17:17' AS result; ^ HINT: Could not choose a best candidate operator. You might need to add explicit type casts. استخدام التابع EXTRACT() لعل أحد أكثر التوابع فائدةً هو التابع EXTRACT() كما يمكننا استخلاص السنة والشهر واليوم من التاريخ كما يلي: hsoubguide=# SELECT EXTRACT(YEAR FROM TIMESTAMP '2016-12-31 13:30:15'); date_part ----------- 2016 (1 row) hsoubguide=# SELECT EXTRACT(MONTH FROM TIMESTAMP '2016-12-31 13:30:15'); date_part ----------- 12 (1 row) hsoubguide=# SELECT EXTRACT(DAY FROM TIMESTAMP '2016-12-31 13:30:15'); date_part ----------- 31 (1 row) hsoubguide=# SELECT EXTRACT(HOUR FROM TIMESTAMP '2016-12-31 13:30:15'); date_part ----------- 13 (1 row) hsoubguide=# SELECT EXTRACT(MINUTES FROM TIMESTAMP '2016-12-31 13:30:15'); date_part ----------- 30 (1 row) hsoubguide=# SELECT EXTRACT(SECONDS FROM TIMESTAMP '2016-12-31 13:30:15'); date_part ----------- 15 (1 row) تخزين المدد الزمنية INTERVAL تتيح كذلك Postgres استخدام نوع مميز لتخزين المدة الزمنية INTERVAL، وسننشئ جدولًا صغيرًا لتعلّم كيفية استخدامه: hsoubguide=# CREATE TABLE date_example2( hsoubguide(# myinterval INTERVAL hsoubguide(# ); CREATE TABLE يمكننا تخزين مدة زمنية كسنوات كما يلي: hsoubguide=# INSERT INTO date_example2(myinterval) VALUES ('2 years'); INSERT 0 1 أو كسنوات وأشهر: hsoubguide=# INSERT INTO date_example2(myinterval) VALUES ('2 years 3 months'); INSERT 0 1 كما يمكن تخزين مدة زمنية في الماضي باستخدام الكلمة ago: hsoubguide=# INSERT INTO date_example2(myinterval) VALUES ('2 years 3 months ago'); INSERT 0 1 يمكننا كذلك تخزين التاريخ مع الوقت كمدة زمنية: hsoubguide=# INSERT INTO date_example2(myinterval) VALUES ('2 years 3 months 5 days 33 minutes'); INSERT 0 1 hsoubguide=# INSERT INTO date_example2(myinterval) VALUES ('2 years 3 months 5 days 33 minutes ago'); INSERT 0 1 وعند الاستعلام عن كل هذه المدد الزمنية سنجد أنها مخزنة كما أدخلناها، باستثناء المدد التي أضفنا لها كلمة ago قد أضيف لها إشارة السالب: hsoubguide=# SELECT * FROM date_example2; myinterval ------------------------------------ 2 years 2 years 3 mons -2 years -3 mons 2 years 3 mons 5 days 00:33:00 -2 years -3 mons -5 days -00:33:00 (5 rows) تكمن فائدة استخدام المدد الزمنية عند الحاجة إلى الرجوع بتاريخ معين إلى مدة زمنية محددة كما يلي: hsoubguide=# SELECT NOW() - INTERVAL '1 year 2 months 3 days 4 hours 5 minutes 6 seconds' AS "1 year,2 months,3 days,04h:05m:06s ago"; 1 year,2 months,3 days,04h:05m:06s ago ---------------------------------------- 2019-06-18 22:36:02.945923+03 (1 row) خلاصة تتميز Postgres عن الأصل SQL بوجود أنواع البيانات الخاصة Arrays و JSONB و HSTORE التي تسمح بتخزين مجموعات مرتبة من البيانات ضمن عمود واحد، وقد تعرفنا إلى كل منها في هذا الفصل، مما يعطينا القدرة على تخزين البيانات بطريقة أكثر احترافية باستخدام Postgres، وقد تعرفنا كذلك في هذا الفصل إلى النوع ENUM الذي يحدد القيمة المخزنة ضمن العمود بقيم معرفة مسبقًا. اقرأ أيضًا المقال التالي: إدارة النسخ الاحتياطي في قواعد بيانات Postgres المقال السابق: مزايا متقدمة في Postgres النسخة الكاملة لكتاب الدليل العملي إلى قواعد بيانات PostgreSQL
-
نتعرف في هذا الفصل إلى العرض View وتعابير الجداول الشائعة CTE التي تساهم في تحسين طريقة كتابة الاستعلامات، وذلك يجعلها أقصر وأسهل قراءةً، وسنتعلم استخدام دوال النوافذ Window Functions التي تسمح بإجراء عمليات تجميعية على مجموعة جزئية من الأسطر، كما سنتعرف على الفهارس وأهميتها في تسريع البحث ضمن البيانات. العرض View هو جدول منطقي يقوم بوصل أجزاء البيانات الأساسية تلقائيًا، فهو لا يقوم بتكرار البيانات ولا الاحتفاظ فيها، بل يعيد ترتيبها لعرضها فقط. وتكمن فوائد استخدام العرض في تبسيط نموذج البيانات عند الحاجة إلى تقديمه للآخرين للعمل عليه، كما يمكن استخدامه لتبسيط العمل بالبيانات الخاصة بك. وإذا وجدت نفسك تدمج نفس المجموعة من البيانات بشكل متكرر فيمكن للعرض تسهيل هذه العملية بدلًا من تكرارها عدة مرات. وكذلك عند العمل مع أشخاص غير تقنيين ليسوا على دراية بلغة SQL يكون العرض هو الطريقة المُثلى لتقديم البيانات غير المنتظمة. طريقة عمل العرض فنلقِ نظرةً على الجداول التالية أولًا: جدول أقسام الشركة: الجدول الذي يحدد انتماء الموظف إلى قسم معين: جدول الموظفين: يمكنك نسخ التعليمات التالية لإنشاء الجداول وإدخال البيانات المذكورة في الصور السابقة. CREATE TABLE departments( id serial PRIMARY KEY, department VARCHAR (50) UNIQUE NOT NULL ); INSERT INTO departments (department) VALUES ('Accounting'),('Marketing'),('Sales'); CREATE TABLE employees( id serial PRIMARY KEY, last_name VARCHAR (50) NOT NULL, salary int NOT NULL ); INSERT INTO employees (last_name,salary) VALUES ('Jones',45000),('Adams',50000),('Johnson',40000),('Williams',37000),('Smith',55000); CREATE TABLE employee_departments( employee_id int NOT NULL, department_id int NOT NULL ); INSERT INTO employee_departments VALUES (1,1),(2,3),(3,2),(4,1),(5,3); وللتأكد من أن الجداول أُنشئت على الوجه الصحيح وأُدخلت البيانات كما هو مخطط، فسنستعرض الجداول كما يلي: hsoubguide=# SELECT * FROM departments; id | department ----+------------ 1 | Accounting 2 | Marketing 3 | Sales (3 rows) hsoubguide=# SELECT * FROM employees; id | last_name | salary ----+-----------+-------- 1 | Jones | 45000 2 | Adams | 50000 3 | Johnson | 40000 4 | Williams | 37000 5 | Smith | 55000 (5 rows) hsoubguide=# SELECT * FROM employee_departments ; employee_id | department_id -------------+--------------- 1 | 1 2 | 3 3 | 2 4 | 1 5 | 3 (5 rows) نهدف في المثال التالي إلى عرض أسماء الموظفين مع رواتبهم والقسم الذي ينتمون إليه، وللقيام بذلك سنكتب الاستعلام التالي: SELECT employees.last_name, employees.salary, departments.department FROM employees, employee_departments, departments WHERE employees.id = employee_departments.employee_id AND departments.id = employee_departments.department_id; وتكون المخرجات كما يلي: last_name | salary | department -----------+--------+------------ Jones | 45000 | Accounting Adams | 50000 | Sales Johnson | 40000 | Marketing Williams | 37000 | Accounting Smith | 55000 | Sales (5 rows) إذا أمعنت النظر في الجدول السابق في المخرجات، قد تتمنى لو أنه مخزّن في الذاكرة بالفعل، فبدلًا من القيام بالعديد من عمليات الربط من هذا الجدول أو ذاك، كان يمكن أن نتعامل مع هذا الجدول مباشرةً بدلًا من كتابة الاستعلام الطويل ذاك في كل مرة نريد استعراض الموظفين مع رواتبهم وأقسامهم، ولكن في الحقيقة، يمكننا إنشاء جدول وهمي عن طريق العرض View لتحقيق هذه الأمنية، كما يلي: CREATE OR REPLACE VIEW employee_view AS SELECT employees.last_name, employees.salary, departments.department FROM employees, employee_departments, departments WHERE employees.id = employee_departments.employee_id AND departments.id = employee_departments.department_id; يمكن الآن القيام باستعلام على الجدول الافتراضي الجديد مباشرًة: SELECT * FROM employee_view; وستظهر البيانات بشكل مشابه لعملية الربط التي قمنا بها سابقًأ كما يلي: last_name | salary | department -----------+--------+------------ Jones | 45000 | Accounting Adams | 50000 | Sales Johnson | 40000 | Marketing Williams | 37000 | Accounting Smith | 55000 | Sales (5 rows) كما يمكننا التعامل مع هذا الجدول المنطقي وكأنه جدول حقيقي، فمثلًا يمكننا الاستعلام عن أسطر هذا الجدول مع ترشيحها أو القيام بأي عملية أخرى تتم على الجداول العادية المخزنة كما في المثال التالي: hsoubguide=# SELECT * FROM employee_view WHERE salary > 40000; last_name | salary | department -----------+--------+------------ Jones | 45000 | Accounting Adams | 50000 | Sales Smith | 55000 | Sales (3 rows) والآن صار بالإمكان تبسيط الاستعلامات الطويلة المتكررة والمتداخلة عن طريق استخدام العرض. عبارات الجداول الشائعة (CTE) تشبه عبارات الجداول الشائعة CTE العرض View في بعض النواحي، فهي نتيجة مؤقتة لاستعلام سابق، يمكننا القيام بعمليات استعلام عليها مرة آخرى دون أن يكون لها وجود دائم في الذاكرة. فهي تشكل جدولًا افتراضيًّا جديدًا ناتجًا عن الاستعلام الأول، ويمكننا القيام بعمليات على هذا الجدول، دون أن يكون له تخزين دائم في الذاكرة. الاستخدام الأساسي لها هو تبسيط الاستعلامات المعقدة، أو الاستعلامات التي تربط عدة جداول معًا، وذلك بعمل هذا الجدول الافتراضي الوسيط أولًا، ثم القيام بالعمليات عليه بعد ذلك أو عمل جدول وسيط آخر حتى الوصول إلى النتائج المطلوبة. سنبدأ باستعلام بسيط من الجدول products عن المنتجات التي توجد ضمن تصنيف Book أو TV كما يلي: hsoubguide=# SELECT title,price hsoubguide-# FROM products hsoubguide-# WHERE tags[1]='Book' OR tags[2]='TV'; title | price ---------------+-------- Dictionary | 9.99 Python Book | 29.99 Ruby Book | 27.99 Baby Book | 7.99 Coloring Book | 5.99 42" LCD TV | 499.00 42" Plasma TV | 529.00 Python Book | 29.99 (8 rows) والآن، لنفترض أننا لما نظرنا إلى الجدول الناتج عن الاستعلام، شعرنا بالحاجة إلى المزيد من التصفية، فأردنا مثلًا الاستعلام عن العناصر التي سعرها أكبر من 10 مع ترتيبها حسب الاسم، وإظهار 5 عناصر فقط. عندها سنضيف العبارة التالية إلى الاستعلام السابق: AND price>10 ORDER BY title LIMIT 5; ليصبح الاستعلام الكامل: hsoubguide=# SELECT title,price hsoubguide=# FROM products hsoubguide=# WHERE (tags[1]='Book' OR tags[2]='TV') hsoubguide=# AND price>10 hsoubguide=# ORDER BY title hsoubguide=# LIMIT 5; title | price ---------------+-------- 42" LCD TV | 499.00 42" Plasma TV | 529.00 Python Book | 29.99 Python Book | 29.99 Ruby Book | 27.99 (5 rows) يمكننا تحويل الاستعلام السابق إلى مرحلتين، الأولى هي عبارة جداول شائعة CTE، والثانية هي استعلام من الجدول الناتج عن عبارة الجداول الشائعة، وذلك كما يلي: hsoubguide=# WITH small_cte AS( hsoubguide-# SELECT title,price hsoubguide-# FROM products hsoubguide-# WHERE (tags[1]='Book' OR tags[2]='TV') hsoubguide-# ) hsoubguide-# SELECT * FROM small_cte hsoubguide-# WHERE price>10 hsoubguide-# ORDER BY title hsoubguide-# LIMIT 5; title | price ---------------+-------- 42" LCD TV | 499.00 42" Plasma TV | 529.00 Python Book | 29.99 Python Book | 29.99 Ruby Book | 27.99 (5 rows) بعد أن فهمنا طريقة استخدام تعابير الجداول الشائعة، لا بد أن سؤالًا مهمًا يخطر لنا، وهو الفرق بينها وبين العرض View، والفرق بسيط، فتعابير الجداول الشائعة تُنشأ أثناء الاستعلام، بينما تُنشأ العروض مسبقًا، وتُخزّن ككائنات في قاعدة البيانات (دون تخزين الجدول الناتج عنها بالطبع كما ذكرنا من قبل) حيث يمكن استخدامها مرات عديدة، لذلك يُطلق على تعابير الجداول الشائعة أحيانًا العروض المؤقتة أو العروض الآنية. استخدام عدة تعابير شائعة معًا سننشئ جدولين بسيطين لتوضيح هذه الفكرة كما يلي: hsoubguide=# CREATE TABLE test_1( hsoubguide(# id INT, hsoubguide(# value varchar(20) hsoubguide(# ); CREATE TABLE hsoubguide=# CREATE TABLE test_2( hsoubguide(# id INT, hsoubguide(# test_1_id INT, hsoubguide(# value varchar(20) hsoubguide(# ); CREATE TABLE في كل من الجدولين، عمود رقم معرف، وعمود للقيمة، وفي الجدول الثاني عمود فيه ربط مع أحد أسطر الجدول الأول من خلال الرقم المعرف الخاص به. hsoubguide=# INSERT INTO test_1 hsoubguide-# VALUES(1,'aaa'),(2,'bbb'),(3,'ccc'); INSERT 0 3 hsoubguide=# INSERT INTO test_2 hsoubguide-# VALUES(1,3,'AAA'),(2,1,'BBB'),(3,3,'CCC'); INSERT 0 3 hsoubguide=# SELECT test_1.value as v1,test_2.value as v2 hsoubguide-# FROM test_1,test_2 hsoubguide-# WHERE test_1.id=test_2.test_1_id; v1 | v2 -----+----- aaa | BBB ccc | CCC ccc | AAA (3 rows) يمكن ربط التعابير الشائعة مع بعضها كما، بحيث يكون التعبير الثاني مستخدمًا للجدول الناتج عن التعبير الأول كما في المثال التالي: hsoubguide=# WITH cte1 AS( hsoubguide-# SELECT test_1.value AS V1,test_2.value AS v2 hsoubguide-# FROM test_1,test_2 hsoubguide-# WHERE test_1.id=test_2.test_1_id) hsoubguide-# ,cte2 AS( Hsoubguide-# SELECT * FROM cte1 hsoubguide-# WHERE v1='ccc') hsoubguide-# SELECT * FROM cte2 ORDER BY v2 DESC; v1 | v2 -----+----- ccc | CCC ccc | AAA (2 rows) هناك الكثير مما يمكن الاستفادة منه باستخدام تعابير الجداول الشائعة عند استخدامها مع الربط أو دوال النوافذ التي سنتعرف عليها في الفقرة التالية. دوال النوافذ تقوم دوال النوافذ (Window Functions) بإجراء عملية حسابية على مجموعة من أسطر الجدول التي لها علاقة محددة مع السطر الحالي، وهذا يشابه إلى حد ما العمليات الحسابية التي قمنا بها باستخدام العمليات التجميعية COUNT و SUM وغيرها، إلا أن استخدام دوال النوافذ لا يسبب تجميع النتيجة في سطر واحد، بل تحافظ الأسطر على ذاتها، وخلف الكواليس تقوم دوال النوافذ بالوصول إلى الأسطر اللازمة للقيام بالعملية الحسابية. لا بد أن الشرح السابق سيتضح أكثر مع الأمثلة على أي حال. فلنلقِ نظرة على الجدول التالي: للحصول على جدول مماثل للجدول السابق، يمكنك تطبيق العرض الذي استخدمناه في الفقرة السابقة: CREATE OR REPLACE VIEW employee_view AS SELECT employees.last_name, employees.salary, departments.department FROM employees, employee_departments, departments WHERE employees.id = employee_departments.employee_id AND departments.id = employee_departments.department_id; ثم الاستعلام عنه كما تعلمنا سابقًا: hsoubguide=# SELECT * hsoubguide-# FROM employee_view; last_name | salary | department -----------+--------+------------ Jones | 45000 | Accounting Adams | 50000 | Sales Johnson | 40000 | Marketing Williams | 37000 | Accounting Smith | 55000 | Sales (5 rows) في المثال التالي، سنحاول استخراج مجموع الرواتب التي تُصرف في كل قسم وذلك باستخدام دالة التجميع SUM والتوجيه GROUP BY: SELECT department ,sum(salary) AS department_salary_sum FROM employee_view GROUP BY department; وستكون المخرجات كما يلي: department | department_salary_sum ------------+----------------------- Accounting | 82000 Sales | 105000 Marketing | 40000 (3 rows) ولكن ماذا لو أردنا استعراض الجدول الأساسي هذا : last_name | salary | department -----------+--------+------------ Jones | 45000 | Accounting Adams | 50000 | Sales Johnson | 40000 | Marketing Williams | 37000 | Accounting Smith | 55000 | Sales (5 rows) مع إضافة عمودٍ جديد، فيه مجموع الرواتب في القسم الخاص بالموظف المذكور في ذلك السطر، هل يمكننا كتابة الاستعلام التالي: SELECT last_name,salary,department,SUM(salary) FROM employee_view GROUP BY department; للأسف، لا يمكننا القيام بذلك، وسيظهر الخطأ التالي: ERROR: column "employee_view.last_name" must appear in the GROUP BY clause or be used in an aggregate function LINE 1: SELECT last_name,salary,department,SUM(salary) ينص الخطأ على أن أي عمود غير مذكور ضمن عبارة GROUP BY أو ضمن دالة التجميع فإنه لا يمكن عرضه، وهذا منطقي، لأن دالة التجميع والعبارة GROUP BY هدفها التجميع، أي اختصار الأسطر وإظهار نتائج الاختصار. وهنا تأتي أهمية دوال النوافذ، فالآن سنستخدم التابع SUM ولكن لن نقوم بكتابة GROUP BY، بل سنستبدلها بالعبارة OVER (PARTITION BY department) وتعني أن عملية الجمع ستتم على جزء من الجدول مقسّم حسب قيمة العمود department كما يلي : hsoubguide2=# SELECT hsoubguide2-# last_name, hsoubguide2-# salary, hsoubguide2-# department, hsoubguide2-# SUM(salary) hsoubguide2-# OVER (PARTITION BY department) hsoubguide2-# FROM employee_view; last_name | salary | department | sum -----------+--------+------------+-------- Jones | 45000 | Accounting | 82000 Williams | 37000 | Accounting | 82000 Johnson | 40000 | Marketing | 40000 Adams | 50000 | Sales | 105000 Smith | 55000 | Sales | 105000 (5 rows) تقوم العبارة PARTITION BY بتقسيم الجدول إلى مجموعات حسب العمود المذكور ضمن هذه العبارة، ثم يتم تنفيذ الدالة المذكورة على هذه المجموعات، وحفظ الناتج لعرضه في العمود الخاص بالدالة. عدة دوال نوافذ في استعلام واحد يمكننا استخدام العديد من توابع النوافذ، ولكن يجب إضافة عبارة OVERخاصة بكل استدعاء من الاستدعاءات، كما في المثال التالي: hsoubguide2=# SELECT last_name,salary,department, hsoubguide2=# AVG(salary) OVER (PARTITION BY department), hsoubguide2=# SUM(salary) OVER (PARTITION BY salary/10000) hsoubguide2=# FROM employee_view; last_name | salary | department | avg | sum -----------+--------+------------+--------------------+-------- Jones | 45000 | Accounting | 41000.000000000000 | 82000 Williams | 37000 | Accounting | 41000.000000000000 | 82000 Johnson | 40000 | Marketing | 40000.000000000000 | 40000 Adams | 50000 | Sales | 52500.000000000000 | 105000 Smith | 55000 | Sales | 52500.000000000000 | 105000 (5 rows) ولكن في حال كنا نرغب في استخدام نفس النافذة لتنفيذ عدة دوال عليها، فيمكننا تعريف النافذة في نهاية الاستعلام لمرة واحدة، واستخدامها عن طريق اسمٍ مستعارٍ لها، كما في المثال التالي: hsoubguide2=# SELECT last_name,salary,department, hsoubguide2=# AVG(salary) OVER my_window, hsoubguide2=# SUM(salary) OVER my_window hsoubguide2=# FROM employee_view hsoubguide2=# WINDOW my_window AS (PARTITION BY department); last_name | salary | department | avg | sum -----------+--------+------------+--------------------+-------- Jones | 45000 | Accounting | 41000.000000000000 | 82000 Williams | 37000 | Accounting | 41000.000000000000 | 82000 Johnson | 40000 | Marketing | 40000.000000000000 | 40000 Adams | 50000 | Sales | 52500.000000000000 | 105000 Smith | 55000 | Sales | 52500.000000000000 | 105000 (5 rows) ملاحظة: في حال أردنا إعطاء العمود الحاوي على نتائج دالة النافذة اسمًا مستعارًا باستخدام AS فيجب وضعها بعد عبارة OVER كما يلي: hsoubguide2=# SELECT last_name,salary,department, hsoubguide2=# AVG(salary) OVER (PARTITION BY department) AS avg_department_salary, hsoubguide2=# SUM(salary) OVER (PARTITION BY department) AS sum_department_salary hsoubguide2=# FROM employee_view; last_name | salary | department | avg_department_salary | sum_department_salary -----------+--------+------------+-----------------------+----------------------- Jones | 45000 | Accounting | 41000.000000000000 | 82000 Williams | 37000 | Accounting | 41000.000000000000 | 82000 Johnson | 40000 | Marketing | 40000.000000000000 | 40000 Adams | 50000 | Sales | 52500.000000000000 | 105000 Smith | 55000 | Sales | 52500.000000000000 | 105000 (5 rows) مثال ممتع باستخدام دوال النافذة سنقوم في هذا المثال بإضافة عمود إلى الجدول، فيه مجموع رواتب الموظفين مجزّأة حسب الحرف الأول من أسمائهم الأخيرة. فإذا كان هناك موظفان اسماهما الأخيرة بالحرف J سيكونان في نفس النافذة، ويتم حساب مجموع رواتبهما وإخراجه إلى جانب اسميهما. وللقيام بذلك، سنستخدم التابع LEFT الذي يأخذ وسيطين، الأول هو اسم العمود والثاني هو عدد الحروف بدءًا من اليسار التي نريد اقتطاعها. وستكون عبارة تجزئة النوافذ كما يلي: OVER (PARTITION BY LEFT(last_name,1)) ولكننا نريد أيضًا أن تكون المخرجات ليست فقط مجموع رواتب الأشخاص المتشابهين في الحرف الأول من اسمهم الأخير، بل نريد إضافة هذا الحرف قبل مجموع الرواتب، كي تكون المخرجات واضحة، فبدلًا من أن يظهر لنا العدد 50000 مثلًا، نريد أن يخرج A:50000. وللقيام بذلك، سنستخدم عملية وصل النصوص || وبدلًا من أن نطلب في الاستعلام إخراج ناتج العملية SUM(salary) سنطلب في الاستعلام العبارة التالية: LEFT(last_name,1) || ':' || SUM(salary) وبذلك يكون لدينا الاستعلام التالي: hsoubguide2=# SELECT last_name,salary,department, hsoubguide2=# LEFT(last_name,1) || ':' || SUM(salary) hsoubguide2=# OVER (PARTITION BY LEFT(last_name,1)) hsoubguide2=# AS Salary_sum_by_letter hsoubguide2=# FROM employee_view; last_name | salary | department | salary_sum_by_letter -----------+--------+------------+---------------------- Adams | 50000 | Sales | A:50000 Jones | 45000 | Accounting | J:85000 Johnson | 40000 | Marketing | J:85000 Smith | 55000 | Sales | S:55000 Williams | 37000 | Accounting | W:37000 (5 rows) دالة النافذة rank يمكننا استخدام دالة النافذة rank لإنتاج رقم ترتيبي للأسطر ضمن التقسيم الذي قمنا به، فلو أردنا مثلًا إنتاج رقم لترتيب الموظفين حسب رواتبهم تصاعديًّا، نكتب الاستعلام التالي: hsoubguide2=# SELECT last_name,salary,department, hsoubguide2=# rank() OVER (ORDER BY salary) hsoubguide2=# FROM employee_view; last_name | salary | department | rank -----------+--------+------------+------ Williams | 37000 | Accounting | 1 Johnson | 40000 | Marketing | 2 Jones | 45000 | Accounting | 3 Adams | 50000 | Sales | 4 Smith | 55000 | Sales | 5 (5 rows) ويمكننا إنتاج رقم ترتيبي للموظفين حسب رواتبهم، ولكن لكل قسم على حدة، وذلك باستخدام العبارة PARTITION BY كما يلي: hsoubguide2=# SELECT last_name,salary,department, hsoubguide2=# rank() OVER (PARTITION BY department ORDER BY salary) hsoubguide2=# FROM employee_view; last_name | salary | department | rank -----------+--------+------------+------ Williams | 37000 | Accounting | 1 Jones | 45000 | Accounting | 2 Johnson | 40000 | Marketing | 1 Adams | 50000 | Sales | 1 Smith | 55000 | Sales | 2 (5 rows) نريد الآن إيجاد الموظفين الأعلى راتبًا في كل قسم، ولذلك سنقوم بإنشاء عرض جديد، للحفاظ على الجدول الناتج عن الاستعلام السابق ولكن سنخزّنه بترتيب معكوس، بحيث يكون الموظف الأعلى راتبًا له قيمة rank تساوي 1 كما يلي: hsoubguide2=# CREATE OR REPLACE VIEW employee_rank AS hsoubguide2-# SELECT last_name,salary,department, hsoubguide2-# rank() OVER (PARTITION BY department ORDER BY salary DESC) hsoubguide2-# FROM employee_view; CREATE VIEW أصبح بإمكاننا الآن تنفيذ استعلامات على الجدول الافتراضي الجديد employee_rank كما يلي: hsoubguide2=# SELECT * FROM employee_rank WHERE rank=1; last_name | salary | department | rank -----------+--------+------------+------ Jones | 45000 | Accounting | 1 Johnson | 40000 | Marketing | 1 Smith | 55000 | Sales | 1 (3 rows) يمكن استعراض وثائق Postgres للمزيد من التعمق في دوال النوافذ. الفهارس Indexes الفهرس Index هو بنية محددة تنظّم مرجعًا للبيانات مما يجعلها أسهل في البحث، وبكون الفهرس في Postgres نسخةً من العنصر المُراد فهرسته مع مرجع إلى موقع البيانات الفعلي. تقوم Postgres عند استعلام البيانات باستخدام الفهارس إن كانت متاحة وإلا فهي تقوم بمسحٍ متتالٍ (sequential scan)، حيث يتم فيه البحث عبر جميع البيانات قبل إرجاع نتيجة. يحقق استخدام الفهارس سرعة كبيرة في البحث والاستعلام، فإضافة عمود فهرس إلى الجدول ستسرّع غالبًا الوصول إلى البيانات، إلا أنّه بالمقابل سيؤدي إلى تبطيئ إدخال البيانات، وذلك لأن عملية الإدخال ستتضمن كتابة بيانات في مكانين مختلفين مع المحافظة على ترتيب الفهرس ضمن البيانات السابقة. يجب الانتباه إلى نوع البيانات المستخدمة كفهرس، فالفهرسة على أساس الأرقام أو الطابع الزمني فعالة، بينما يُعدّ استخدام النصوص كفهارس غير فعّال. لنأخذ مثالا عمليًّا بوضع فهرس للجدول employees (الذي عرضناه في بداية المقال): CREATE INDEX idx_salary ON employees(salary); يُمكن استخدام عدة أعمدة كفهارس في الوقت نفسه، فإن كنت تقوم بتصفية نتائج الجداول باستخدام أعمدة محددة، فيمكنك إنشاء فهارس منها كما يلي: CREATE INDEX idx_salary ON employees(last_name, salary); نصائح عند استخدام الفهارس إنشاء الفهارس آنيًّا عندما تقوم Postgres بإنشاء الفهارس (كما في أنظمة قواعد البيانات الأخرى) فإنها تقوم بقفل الجدول أثناء بناء الفهرس، وهذا يمنع إضافة المزيد من البيانات إلى الجدول. فإن كانت كمية البيانات صغيرة فإن العملية لن تستغرق الكثير من الوقت، ولكنّها ستسبب إعاقةً كبيرة للعمل على هذا الجدول إن كانت كمية البيانات كبيرة جدًا ومن ثمّ فستكون مدة قفل الجدول أطول. وهذا يعني أنه للحصول على تحسين في كفاءة قاعدة البيانات لا بدّ من إيقاف العمل على قاعدة البيانات لمدة محددة، على الأقل للجدول الذي يتم إنشاء الفهرس له. إلا أن Postgres تتيح إمكانية إنشاء الفهرس دون قفل الجدول، وذلك باستخدام CREATE INDEX CONCURRENTLY كما في المثال التالي: CREATE INDEX CONCURRENTLY idx_salary ON employees(last_name, salary); عندما يكون الفهرس أذكى منك إن استخدام الفهرس ليس هو الحل الأسرع دومًا، فمثلًا إن كانت نتيجة الاستعلام ستعيد نسبة كبيرة من بيانات الجدول، فإن المرور على جميع بيانات الجدول أسرع في هذه الحال من استخدام الفهرس، لذا عليك الوثوق بأن Postgres ستقوم بالأمر الصحيح فيما يتعلق بسرعة الاستعلام سواء كان استخدام الفهرس أو غيره. فهرسة النوع JSONB إن طريقة الفهرسة الأكثر مرونة وقوة هي استخدام فهرس GIN، حيث يقوم GIN بفهرسة كل عمود ومفتاح ضمن مستند JSONB، ويمكن إضافته كما في المثال التالي: CREATE INDEX idx_products_attributes ON products USING GIN (attributes); المفاتيح الخارجية والفهارس في بعض أنظمة الربط العلائقي للكائنات Object relational mapping، يؤدي إنشاء مفتاح خارجي (Foreign Key) إلى إنشاء فهرس (index) أيضًا، وننوه هنا إلى أن Postgres لا تقوم تلقائيا بإنشاء فهرس عند إنشاء مفتاح خارجي، فهي خطوة منفصلة عليك الانتباه إليها عندما لا تستخدم ORM. الخلاصة نجحنا في هذا الفصل بجعل استعلاماتنا أقصر وأسهل قراءةً بفضل العرض View وتعابير الجداول الشائعة CTE، كما تعرفنا على دوال النوافذ Window Functions التي تسمح بالقيام بعمليات تجميعية تحتاج إلى لغة برمجة في العادة، كما تطرقنا إلى الفهارس وكيفية إنشائها. اقرأ أيضًا المقال التالي: أنواع بيانات خاصة في قواعد بيانات Postgres المقال السابق: استخدام أساسيات SQL في Postgres النسخة الكاملة لكتاب الدليل العملي إلى قواعد بيانات PostgreSQL كيفيّة حماية PostgreSQL من الهجمات المُؤتمتة (Automated Attacks)
-
يتناول هذا القسم الطرق الأساسية للاستعلام عن البيانات وعرض الجداول، وربط البيانات بين عدة جداول، كما نتعرف فيه على كيفية تعديل البيانات ضمن الجدول أو حذفها، والعديد من التعليمات والتوابع الأكثر أهمية في Postgres. إنشاء الجداول سنشرح في هذه الفقرة كيفية إنشاء الجداول المذكورة في المثال السابق يدويًّا. ملاحظة: إن كنت قمت بتنزيل قاعدة البيانات من المقال السابق، تثبيت Postgres والتعرف على أساسيات إدارتها لقواعد البيانات، فيمكنك الاطلاع على التعليمات الحقيقية التي تم من خلالها إنشاء الجداول، وذلك من خلال الأوامر التالية في صدفة bash: pg_dump -st products hsoubguide pg_dump -st purchases hsoubguide pg_dump -st purchase_items hsoubguide pg_dump -st users hsoubguide تقوم التعليمة السابقة بإظهار جميع التعليمات اللازمة لإنشاء الجدول المذكور بالحالة التي هو عليها الآن، إلا أننا سنركّز في هذه الفقرة على التعليمات الأساسية فقط. في حال قمت بتنزيل النسخة الكاملة من قاعدة البيانات كما ورد سابقًا، وترغب في إنشاء الجداول من الصفر أيضًا، فسنقوم لذلك بإنشاء قاعدة بيانات جديدة كي لا تتعارض مع الجداول السابقة. CREATE DATABASE hsoubguide2; يمكننا إنشاء جدول جديد في قاعدة البيانات عن طريق التعليمة CREATE TABLE حيث نقوم بتحديد اسم الجدول، وتفصيل أسماء الأعمدة ونوع البيانات المحتواة في كل منها، إلى جانب الصفات الخاصة بكل عمود، كما في الأمثلة التالية التي تشرح نفسها بنفسها: CREATE TABLE products ( id integer NOT NULL, title character varying(255), price numeric, created_at timestamp with time zone, deleted_at timestamp with time zone, tags character varying(255)[] ); في التعليمة السابقة، أُنشئ جدول اسمه products وحُدّد فيه الأعمدة التالية: العمود id، يحتوي بيانات نوعها integer أي أعداد صحيحة، وله المواصفة NOT NULL، أي أنه يُمنع إنشاء سطر دون تحديد قيمة لهذا العمود. العمود title، يحتوي بيانات محرفية، طولها متغير، يمكن أن يصل إلى 255 حرفًا، ولذلك نستخدم النوع character varying مع وضع الطول الأكبر بين قوسين. العمود price، يحوي بيانات عددية من النوع numeric وهو نوع عددي مميز، يمكن تخزين أعداد يصل عدد منازلها إلى 131072 منزلة قبل الفاصلة العشرية، و 16383 منزلة بعد الفاصلة! إلا أن العمليات الحسابية عليه تكون أبطأ من النوع integer والسعة التخزينية التي يحتاجها أكبر أيضًا. العمود created_at و العمود deleted_at يحتويان على بيانات التاريخ والوقت مع حفظ المنطقة الزمنية، فهما من النوع timestamp with time zone العمود tags يحتوي بيانات محرفية من النوع character varying إلا أن وجود الرمز [] في تعريف هذا العمود، يعني أنه سيحتفظ بمصفوفة من العبارات المحرفية (أي أنه لن يحتوي عبارة واحدة، بل قائمة من العبارات) يمكنك الاطلاع على المزيد من التفاصيل عن الأنواع العددية من توثيق Postgres الرسمي. يمكننا رؤية تفاصيل الجدول الذي أُنشئ كما يلي: hsoubguide2=# \d products Table "public.products" Column | Type | Collation | Nullable | Default ------------+--------------------------+-----------+----------+--------- id | integer | | not null | title | character varying(255) | | | price | numeric | | | created_at | timestamp with time zone | | | deleted_at | timestamp with time zone | | | tags | character varying(255)[] | | | بعد أن قُمنا بإنشاء الجدول وتحديد أنواع الأعمدة، سنقوم بتحديد مواصفة هامّة للعمود id هي PRIMARY KEY وتعني أن القيمة المخزنة ضمن هذا العمود ستكون فريدة UNIQUE لا تتكرر في أي سطر من الأسطر، ولا يمكن أن تكون NULL، وستكون القيمة المخزنة في هذا العمود هي التي تميّز الأسطر عن بعضها. سنتعرف من خلال هذه الخطوة على التعليمة التي تقوم بتعديل تعريف الجداول ALTER TABLE، والتعديل الذي سنقوم به سيكون ADD CONSTRAINT كما يلي: ALTER TABLE ONLY public.products ADD CONSTRAINT products_pkey PRIMARY KEY (id); حيث قمنا بإضافة CONSTRAINT أسميناه products_pkey ينصّ على جعل العمود id بالمواصفة PRIMARY KEY، وذلك في الجدول products. يمكننا استخدام التعليمة ALTER لتغيير أي شيء من خصائص الجدول، كتغيير اسمه، أو تغيير اسم أحد الأعمدة، أو إضافة أعمدة جديدة أو حذف أعمدة، ويمكنك الاطلاع على المزيد عن هذه التعلمية من توثيق Postgres الرسمي. أصبح لديك الآن المعرفة الكافية لفهم كيفية إنشاء الجداول، ويمكنك قراءة التعليمات التالية وتنفيذها بوضوح لإنشاء الجداول التي نحتاجها في أمثلتنا. CREATE TABLE purchases ( id integer NOT NULL, created_at timestamp with time zone, name character varying(255), address character varying(255), state character varying(2), zipcode integer, user_id integer ); CREATE TABLE purchase_items ( id integer NOT NULL, purchase_id integer, product_id integer, price numeric, quantity integer, state character varying(255) ); CREATE TABLE users ( id integer NOT NULL PRIMARY KEY, email character varying(255), password character varying(255), details public.hstore, created_at timestamp with time zone, deleted_at timestamp with time zone ); ملاحظة: في الجدول الأخير، يوجد العمود details يحوي بيانات من النوع hstore، وسنتحدث عن هذا النوع الخاص في الفصل التالي، أنواع بيانات خاصة في قواعد بيانات Postgres. والآن، إذا أردت حذف جدولٍ من الجداول، فاستخدم التعليمة DROP TABLE كما يلي: DROP TABLE users تعديل الجداول يمكننا التعديل على الجدول بعد إنشائه باستخدام التعليمة الأساسية ALTER، حيث يمكننا من خلالها القيام بتعديل جميع خصائص الجداول، نذكر منها أهم عمليات التعديل التالية: 1- تعديل اسم الجدول: ALTER TABLE users RENAME TO our_lovely_users; 2- تعديل اسم عمود ما: ALTER TABLE products RENAME COLUMN title TO product_name; 3- إضافة عمود إلى الجدول: ALTER TABLE users ADD COLUMN image_url character varying(1024); 4- حذف عمود من الجدول: ALTER TABLE users DROP COLUMN IF EXISTS image_url 5- تعديل نوع البيانات في عمود ما من الجدول: ALTER TABLE products ALTER COLUMN price SET DATA TYPE float; يمكنك التوسع في إمكانية التعديل على الجداول بالاطلاع على وثائق Postgres الرسمية. إدخال البيانات بعد أن قمنا بإنشاء الجداول، علينا إدخال البيانات فيها، ويتم ذلك عن طريق التعليمة INSERT INTO ، وسنقوم في المثال بإدخال سطر في الجدول products كما يلي: INSERT INTO products( id, title, price, created_at, tags ) VALUES ( 1, 'Python Book', 29.99, NOW(), ARRAY['Book','Programming','Python'] ); توضّح التعليمة السابقة كيفية إدخال سطر في جدول ما، وذلك عن طريق ذكر اسم الجدول، ثم ترتيب الأعمدة التي سنحدد قيمتها بين قوسين، وبعد ذلك نكتب الكلمة المفتاحية VALUES ثم نذكر قيمة كل عمود في السطر الجديد بالترتيب نفسه. لاحظ استخدامنا للتابع NOW الذي يعطي التاريخ والوقت في لحظة تنفيذ التعليمة. لاحظ أيضًا كيف قمنا بإدخال مصفوفة في العمود tags، وسنتحدث عن ذلك أكثر لاحقًا في هذا الدليل. للتأكد من أن السطر تم إدخاله، يمكنك استخدام التعليمة SELECT وهي تعليمة الاستعلام الأساسية، وسنستخدمها هنا دون أي شروط كما يلي: SELECT * FROM products; حيث تشير * إلى اختيار جميع الأعمدة لعرضها من الجدول products، وسنحصل على المخرجات التالية: hsoubguide2=# SELECT * FROM products; id | title | price | created_at | deleted_at | tags ----+------------------+--------+-------------------------------+------------+--------------------------- 1 | Python Book | 29.99 | 2020-06-22 12:22:02.281079+03 | | {Book,Programming,Python} (1 row) يمكننا أيضًا إدخال عدة أسطر في تعليمة INSERT واحدة، وذلك بوضع قوسين () حول البيانات الخاصة بكل سطر، وفصل الأسطر بفاصلة كما يلي: INSERT INTO products(id,title,price,created_at,tags) VALUES ( 2,'Book2',1.99,NOW(),ARRAY['a1','b','q'] ), ( 3,'Book3',2.99,NOW(),ARRAY['a2','c','w'] ), ( 4,'Book4',3.99,NOW(),ARRAY['a3','d','e'] ), ( 5,'Book5',4.99,NOW(),ARRAY['a4','e','r'] ); ملاحظة: سنتحدث عن كيفية التعديل على الأسطر المخزّنة أو حذف أسطر من الجدول في فقرات لاحقة. كانت هذه بداية سريعة في كيفية إنشاء جدول وإدخال البيانات إليه، ثم الاستعلام عنها، وسنتوسع في الفقرات التالية في أنواع الاستعلامات وكيفية القيام بها، وذلك على اعتبار وجود الجداول والبيانات مُسبقًا، واعتبار أن إدخال البيانات يتم تلقائيًّا عن طريق ربط قاعدة البيانات بموقع ويب، أو بواجهة مستخدم ما. استعراض الجداول نستعرض في المثال التالي، كيفية عرض جميع الجداول المخزنة في قاعدة البيانات باستخدام التعليمة \dt كما يلي: hsoubguide=# \dt List of relations Schema | Name | Type | Owner --------+----------------+-------+---------- public | products | table | postgres public | purchase_items | table | postgres public | purchases | table | postgres public | users | table | postgres (4 rows) ملاحظة: العبارة hsoubguide=# مولّدة تلقائيًّا من psql وتحوي اسم قاعدة البيانات التي نحن بداخلها، لذلك لا تقم بنسخها في حال كنت ترغب بنسخ التعليمات في هذا الدليل. الاستعلام عن البيانات كلمة "استعلام" تعني طلب الحصول على أسطر محددة من قاعدة البيانات، ولكننا قد نرغب في تحديد أعمدة محددة من جداول محددة، وقد نحتاج إلى تحديد شروط على الأسطر التي نرغب بالحصول عليها، كأن نطلب الحصول على المستخدمين الذين لم يقوموا بأي تفاعل منذ سنة، أو الحصول على بيانات عن المنتجات التي يتجاوز عدد مبيعاتها قيمة محددة. ولذلك فالخطوة الأولى في بناء الاستعلامات هي معرفة مصدر البيانات التي تتعامل معها، ثم معرفة البيانات التي يمكننا الحصول عليها، وهي الأعمدة التي يحتويها الجدول. في مثالنا التالي، سنحاول معرفة بعض المعلومات عن المستخدمين المخزّنين في قاعدة البيانات. سنستخدم التعليمة \d يليها اسم الجدول لاستعراض أسماء الأعمدة ضمن الجدول كما يلي: hsoubguide=# \d users Table "public.users" Column | Type | Collation | Nullable | Default ------------+--------------------------+-----------+----------+----------------------------------- id | integer | | not null | nextval('users_id_seq'::regclass) email | character varying(255) | | | password | character varying(255) | | | details | hstore | | | created_at | timestamp with time zone | | | deleted_at | timestamp with time zone | | | Indexes: "users_pkey" PRIMARY KEY, btree (id) Referenced by: TABLE "purchases" CONSTRAINT "purchases_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ظهر لدينا الآن مجموعة متنوعة من البيانات، سنختار منها المعلومات الثلاث التالية الخاصة بالمستخدمين وهي id و email و created_at. والآن أصبحنا نعرف المعلومات الأساسية لبناء الاستعلام وهي: الجدول الذي نريد استعلام البيانات منه البيانات التي نريدها من ذاك الجدول يبين المثال التالي التركيب النحوي للقيام بالاستعلام المطلوب، وهو يحوي البيانات المُراد الحصول عليها، يليها اسم الجدول الحاوي عليها، ثم فاصلة منقوطة للدلالة على انتهاء الاستعلام: hsoubguide=# SELECT id,email,created_at hsoubguide-# FROM users; id | email | created_at ----+-------------------------------+------------------------ 1 | Earlean.Bonacci@yahoo.com | 2009-12-20 22:36:00+02 2 | Evelyn.Patnode@gmail.com | 2010-11-12 23:27:00+02 3 | Derek.Crenshaw@gmail.com | 2009-03-08 05:06:00+02 4 | Shari.Julian@yahoo.com | 2010-11-20 12:58:00+02 5 | Zita.Breeding@gmail.com | 2009-08-12 01:33:00+03 6 | Samatha.Hedgpeth@yahoo.com | 2010-07-18 13:40:00+03 7 | Quinton.Gilpatrick@yahoo.com | 2010-09-03 00:56:00+03 8 | Vivian.Westmoreland@yahoo.com | 2009-10-01 14:34:00+03 9 | Danny.Crays@gmail.com | 2009-04-22 10:30:00+03 10 | Edmund.Roles@yahoo.com | 2009-07-08 00:01:00+03 11 | Shanell.Lichtenstein@aol.com | 2009-05-22 03:18:00+03 12 | Romaine.Birdsell@aol.com | 2009-01-14 07:07:00+02 13 | Zita.Luman@yahoo.com | 2009-02-04 16:49:00+02 14 | Claud.Cousineau@gmail.com | 2009-08-17 21:48:00+03 15 | Kali.Damore@yahoo.com | 2010-07-07 13:28:00+03 16 | Graciela.Kubala@yahoo.com | 2010-08-19 08:42:00+03 17 | Theresia.Edwin@yahoo.com | 2010-08-11 11:21:00+03 18 | Ozella.Yoshimura@gmail.com | 2010-07-23 19:03:00+03 19 | Wynona.Greening@aol.com | 2009-05-24 17:25:00+03 20 | Kimi.Mcqueeney@gmail.com | 2010-06-22 18:16:00+03 21 | Cherryl.Tarnowski@gmail.com | 2009-01-26 11:56:00+02 22 | Isabel.Breeding@gmail.com | 2010-07-11 16:28:00+03 23 | Ivana.Kurth@yahoo.com | 2010-06-25 11:36:00+03 24 | Humberto.Jonson@yahoo.com | 2009-09-23 16:09:00+03 25 | Ivana.Sosnowski@aol.com | 2009-01-16 13:55:00+02 26 | Cortney.Strayer@gmail.com | 2009-07-19 09:08:00+03 27 | Williams.Upson@gmail.com | 2010-08-10 08:48:00+03 28 | Jeremiah.Buonocore@yahoo.com | 2009-03-19 09:49:00+02 29 | Ozella.Roles@gmail.com | 2009-10-09 12:44:00+03 30 | Salvatore.Arends@aol.com | 2009-09-05 04:55:00+03 31 | Layne.Sarver@aol.com | 2010-09-26 11:00:00+03 32 | Takako.Gilpatrick@aol.com | 2009-02-22 17:46:00+02 33 | Russ.Mcclain@yahoo.com | 2010-01-12 19:27:00+02 34 | Claud.Westmoreland@aol.com | 2010-06-11 20:21:00+03 35 | Derek.Knittel@gmail.com | 2010-08-17 00:09:00+03 36 | Eleanor.Patnode@yahoo.com | 2010-06-06 04:27:00+03 37 | Carmel.Bulfer@aol.com | 2009-06-06 23:13:00+03 38 | Mauro.Pung@yahoo.com | 2009-08-20 05:34:00+03 39 | Sherilyn.Hamill@gmail.com | 2010-04-02 02:39:00+03 40 | Glen.Lanphear@yahoo.com | 2010-08-06 18:14:00+03 41 | Stacia.Schrack@aol.com | 2010-06-14 22:28:00+03 42 | Tonette.Alba@gmail.com | 2009-12-28 12:21:00+02 43 | Eve.Kump@yahoo.com | 2009-08-20 12:45:00+03 44 | Shanell.Maxson@gmail.com | 2009-11-21 08:28:00+02 45 | Gudrun.Arends@gmail.com | 2010-06-30 15:30:00+03 46 | Angel.Lessley@yahoo.com | 2009-08-21 20:06:00+03 47 | Harrison.Puett@yahoo.com | 2009-07-21 18:20:00+03 48 | Granville.Hedgpeth@gmail.com | 2009-08-03 17:54:00+03 49 | Samatha.Pellegrin@yahoo.com | 2009-03-25 22:17:00+02 50 | Wan.Dilks@gmail.com | 2009-10-09 01:43:00+03 (50 rows) ملاحظة: يجب إنهاء تعليمات postgres بفاصلة منقوطة، وفي حال عدم القيام بذلك، فعند الضغط على enter لا تعتبر psql أن التعليمة قد انتهت، وتنتظر كتابة الفاصلة المنقوطة في السطر الجديد. يسمح لنا الأمر السابق بعرض جميع بيانات الجدول، إلا أن هذا لا يناسب عرض الجداول الحاوية على كمية كبيرة من البيانات، ولذلك يتوجب علينا الحد من كمية البيانات وتصفيتها. الحد من البيانات المعروضة باستخدام LIMIT يمكننا إضافة التوجيه LIMIT يليه عدد الأسطر الأكبر الذي نسمح بعرضه عند الاستعلام، وذلك للحد من الأسطر المعروضة، كما يلي: hsoubguide=# SELECT id,email,created_at hsoubguide-# FROM users hsoubguide-# LIMIT 5; id | email | created_at ----+---------------------------+------------------------ 1 | Earlean.Bonacci@yahoo.com | 2009-12-20 22:36:00+02 2 | Evelyn.Patnode@gmail.com | 2010-11-12 23:27:00+02 3 | Derek.Crenshaw@gmail.com | 2009-03-08 05:06:00+02 4 | Shari.Julian@yahoo.com | 2010-11-20 12:58:00+02 5 | Zita.Breeding@gmail.com | 2009-08-12 01:33:00+03 (5 rows) ترتيب البيانات باستخدام ORDER BY بعد أن تعرفنا على كيفية الحد من البيانات، قد نرغب بإظهار عدد محدود من البيانات ولكن وفق ترتيب معيّن، فمثلًا نريد الاستعلام عن 5 مستخدمين ولكن بعد ترتيبهم حسب البريد الإلكتروني الخاص بهم، وللقيام بذلك سنستخدم التوجيه ORDER BY كما يلي: hsoubguide=# SELECT id,email,created_at hsoubguide-# FROM users hsoubguide-# ORDER BY email hsoubguide-# LIMIT 5; id | email | created_at ----+-----------------------------+------------------------ 46 | Angel.Lessley@yahoo.com | 2009-08-21 20:06:00+03 37 | Carmel.Bulfer@aol.com | 2009-06-06 23:13:00+03 21 | Cherryl.Tarnowski@gmail.com | 2009-01-26 11:56:00+02 14 | Claud.Cousineau@gmail.com | 2009-08-17 21:48:00+03 34 | Claud.Westmoreland@aol.com | 2010-06-11 20:21:00+03 (5 rows) ماذا لو أردنا القيام بترتيب تنازلي؟ يمكننا استخدام DESC اختصارًا لكلمة "descending" كما يلي: hsoubguide=# SELECT id,email,created_at hsoubguide-# FROM users hsoubguide-# ORDER BY email DESC hsoubguide-# LIMIT 5; id | email | created_at ----+--------------------------+------------------------ 13 | Zita.Luman@yahoo.com | 2009-02-04 16:49:00+02 5 | Zita.Breeding@gmail.com | 2009-08-12 01:33:00+03 19 | Wynona.Greening@aol.com | 2009-05-24 17:25:00+03 27 | Williams.Upson@gmail.com | 2010-08-10 08:48:00+03 50 | Wan.Dilks@gmail.com | 2009-10-09 01:43:00+03 (5 rows) فلنلقِ نظرةً على جدول المنتجات كي نحظى بفرصة الحصول على مثالٍ جديد: hsoubguide=# SELECT title,price hsoubguide-# FROM products hsoubguide-# LIMIT 8; title | price ------------------+-------- Dictionary | 9.99 Python Book | 29.99 Ruby Book | 27.99 Baby Book | 7.99 Coloring Book | 5.99 Desktop Computer | 499.99 Laptop Computer | 899.99 MP3 Player | 108.00 (8 rows) سنقوم في المثال التالي بترتيب الجدول السابق حسب السعر، ثمّ سنقوم بترتيب المنتجات المتشابهة بالسعر بعكس الترتيب الأبجدي لأسماء المنتجات، وذلك كما يلي: hsoubguide=# SELECT title,price hsoubguide-# FROM products hsoubguide-# ORDER BY price ASC, title DESC hsoubguide-# LIMIT 10; title | price ---------------+------- Coloring Book | 5.99 Baby Book | 7.99 Pop CD | 9.99 Holiday CD | 9.99 Electronic CD | 9.99 Dictionary | 9.99 Country CD | 9.99 Classical CD | 9.99 Romantic | 14.99 Drama | 14.99 (10 rows) عمليات التجميع Aggregation Functions سنتعرف في هذه الفقرة على توابع يمكنها القيام بتجميع قيم الأعمدة في قيمة واحدة، فمثلًا يمكننا الحصول على متوسط أسعار المنتجات في جدول المنتجات، أو عدد المنتجات الموجودة، أو السعر الأكبر أو اﻷصغر، وذلك كما يلي: hsoubguide=# SELECT MAX(price),MIN(price),AVG(price),SUM(price),COUNT(price) hsoubguide-# FROM products; max | min | avg | sum | count --------+------+----------------------+---------+------- 899.99 | 5.99 | 132.0390476190476190 | 2772.82 | 21 (1 row) لاحظ كيف اختلفت المخرجات فلم يعد هناك أسماء الأعمدة المعتادة، بل استُبدلت بأسماء التوابع المستخدمة. تقوم عملية التجميع COUNT بإرجاع عدد الأسطر التي تحتوي قيمة غير خالية، وسيوضّح المثال التالي ما نرمي إليه: hsoubguide=# SELECT COUNT(*), hsoubguide-# COUNT(email), hsoubguide-# COUNT(details) hsoubguide-# FROM users; count | count | count -------+-------+------- 50 | 50 | 34 (1 row) لاحظ كيف أرجع استدعاء العملية COUNT على العمود details القيمة 34، رغم أنها أرجعت القيمة 50 للعمود email، وقد استخدمنا كذلك COUNT(*) لإظهار عدد جميع الأسطر في الجدول بغض النظر عن قيم الأعمدة. ولكن هل لاحظت أسماء الأعمدة في المخرجات؟ إنها جميعًا count، ولا يبدو ذلك مفيدًا لمن سيطّلع على هذه المخرجات، ولذلك سنستخدم التوجيه AS. للمزيد عن عمليات التجميع، يمكنك الرجوع إلى توثيق Postgres الرسمي. التوجيه AS يسمح لنا هذا التوجيه بإعادة تسمية أعمدة جدول الخرج، وذلك ليتناسب مع الهدف الذي قُمنا بالاستعلام لأجله. رأينا في المثال السابق كيف قمنا بثلاث عمليات COUNT مختلفة، ولكن الجدول في المخرجات كانت جميع أعمدته بالاسم count دون تمييز، ولذلك سنستخدم التوجيه AS لتغيير أسماء هذه الأعمدة كما يلي: hsoubguide=# SELECT COUNT(*) AS TOTAL_COUNT, hsoubguide-# COUNT(email) AS EMAIL_COUNT, hsoubguide-# COUNT(details) AS DETAILS_COUNT hsoubguide-# FROM users; total_count | email_count | details_count -------------+-------------+--------------- 50 | 50 | 34 (1 row) الآن أصبحت المخرجات مقروءة ومفهومة أكثر، ولكن تجدر بنا الإشارة إلى أن ذكر التوجيه AS هو أمر اختياريّ، فيمكننا تسمية جداول الخرج بدون الحاجة إلى كتابة التوجيه AS كما يلي: hsoubguide=# SELECT COUNT(*) TOTAL_COUNT, hsoubguide-# COUNT(email) EMAIL_COUNT, hsoubguide-# COUNT(details) DETAILS_COUNT hsoubguide-# FROM users; total_count | email_count | details_count -------------+-------------+--------------- 50 | 50 | 34 (1 row) حافظ دومًا على مقروئية الجدول في المخرجات، فقد يكون لديك في العمود عمليات حسابية أو عدة توابع في عبارة واحدة، وهنا سيكون لزامًا استخدام الأسماء المستعارة للأعمدة وإلا ستبدو الجداول في المخرجات غير مفهومة. ترشيح البيانات (Filtering) قًمنا حتى الآن باستعلام جميع البيانات في الجدول، وأكثر ما استطعنا تحديده هو تقليل عدد الأسطر في المخرجات، وترتيبها بشرط محدد، ولكننا ماذا لو أردنا استعراض جميع الأسطر التي تحقق شرطًا ما؟ كيف يمكننا استعراض المنتجات التي يتجاوز سعرها قيمة محددة؟ أو المستخدمين الذين أنشئت حساباتهم بعد تاريخ محدد؟ يمكننا ترشيح البيانات باستخدام الشرط WHERE، ويبين المثال التالي عملية ترشيح للحصول على المستخدمين الذين أُنشئ حسابهم في تاريخ 30/6/2010 فصاعدًا: SELECT email, created_at FROM users WHERE created_at >= '2010-06-30'; كما يمكننا دمج شروط أخرى باستخدام AND أو OR، ويبين المثال التالي استخدام AND للاستعلام عن جميع المستخدمين الذين قاموا بإنشاء حسابات في شهر تموز عام 2010: hsoubguide=# SELECT email, created_at hsoubguide-# FROM users hsoubguide-# WHERE created_at >= '2010-07-01' hsoubguide-# AND created_at < '2010-08-01'; email | created_at ----------------------------+------------------------ Samatha.Hedgpeth@yahoo.com | 2010-07-18 13:40:00+03 Kali.Damore@yahoo.com | 2010-07-07 13:28:00+03 Ozella.Yoshimura@gmail.com | 2010-07-23 19:03:00+03 Isabel.Breeding@gmail.com | 2010-07-11 16:28:00+03 (4 rows) ملاحظة: لاحظ كيف يتغيّر محثّ الأوامر من hsoubguide=# إلى hsoubguide-# عندما تتوزع التعليمة على عدة أسطر، وتكون الفاصلة المنقوطة هي نهاية التعليمة. والآن ما رأيك باستخدام بعض أوامر التجميع مثل SUM و AVG وغيرها للحصول على بعض المعلومات المفيدة! سنبدأ بالاطلاع على جدول المنتجات المشتراة، لنرى ماذا بإمكاننا أن نستخلص منه من المعلومات: hsoubguide=# SELECT * hsoubguide-# FROM purchase_items hsoubguide-# LIMIT 20; id | purchase_id | product_id | price | quantity | state ----+-------------+------------+--------+----------+----------- 2 | 1 | 3 | 27.99 | 1 | Delivered 3 | 1 | 8 | 108.00 | 1 | Delivered 4 | 2 | 1 | 9.99 | 2 | Delivered 5 | 3 | 12 | 9.99 | 1 | Delivered 6 | 3 | 17 | 14.99 | 4 | Delivered 7 | 3 | 11 | 9.99 | 1 | Delivered 8 | 4 | 4 | 7.99 | 3 | Delivered 9 | 5 | 18 | 14.99 | 1 | Delivered 10 | 5 | 2 | 29.99 | 4 | Delivered 11 | 6 | 5 | 5.99 | 1 | Delivered 12 | 7 | 6 | 499.99 | 3 | Returned 13 | 8 | 10 | 529.00 | 1 | Delivered 14 | 8 | 7 | 899.99 | 1 | Delivered 15 | 9 | 15 | 9.99 | 2 | Delivered 16 | 10 | 2 | 29.99 | 1 | Delivered 17 | 11 | 9 | 499.00 | 2 | Delivered 18 | 12 | 14 | 9.99 | 5 | Delivered 19 | 12 | 10 | 529.00 | 1 | Delivered 20 | 13 | 8 | 108.00 | 1 | Delivered 21 | 14 | 20 | 14.99 | 1 | Delivered (20 rows) هذا الجدول فيه الكثير من المعلومات الهامة، وسنعود إليه في الفقرة التالية، ولكن الآن يمكننا رؤية أن بعض عمليات الشراء يتم إيصالها "Delivered" وبعضها تُرجع للمتجر "Returned"، وسنقوم في المثال التالي بعدّها، وإيجاد مجموع المشتريات التي تم إيصالها بالفعل. hsoubguide=# SELECT COUNT(*) hsoubguide-# FROM purchase_items hsoubguide-# WHERE state='Delivered'; count ------- 3888 (1 row) hsoubguide=# SELECT COUNT(*) hsoubguide-# FROM purchase_items hsoubguide-# WHERE state='Returned'; count ------- 246 (1 row) hsoubguide=# SELECT COUNT(*) hsoubguide-# FROM purchase_items; count ------- 4371 (1 row) سنوجد مجموع المشتريات التي سُلِّمَت بالفعل، وتلك التي أُعيدَت: hsoubguide=# SELECT SUM(price) hsoubguide-# FROM purchase_items hsoubguide-# WHERE state='Delivered'; sum ----------- 517787.85 (1 row) hsoubguide=# SELECT SUM(price) hsoubguide-# FROM purchase_items hsoubguide-# WHERE state='Returned'; sum ---------- 36456.87 (1 row) استخدام عبارة CASE الشرطية تسمح لنا عبارة CASE بعمل بنية مشابهة للبنية البرمجية switch الموجودة في معظم لغات البرمجة، أو يمكن تشبيهها أكثر بعبارات if متتابعة، حيث يمكننا إنتاج قِيَم محددة بناءً على اختبارات على قيم أُخرى، ولا بد أنه المثال سيوضّح المقال. ننشئ جدولًا بسيطًا فيه عمود واحد يحوي أعدادًا صحيحة. hsoubguide=# CREATE TABLE case_example( hsoubguide(# number INT hsoubguide(# ); CREATE TABLE وسنقوم بإدخال 5 أعداد في هذا الجدول كما يلي: hsoubguide=# INSERT INTO case_example VALUES(1),(2),(3),(4),(5),(6); INSERT 0 6 hsoubguide=# SELECT * FROM case_example ; number -------- 1 2 3 4 5 6 (6 rows) والآن سنقوم بالاستعلام عن عناصر هذا العمود، ولكن بدلًا من طباعة العنصر كما هو، سنطبع عبارات حسوبية بناءً على قيمة هذا العمود. hsoubguide=# SELECT (CASE WHEN number=1 THEN 'Hsoub' WHEN number=2 OR number=3 THEN 'Khamsat' WHEN number<5 THEN 'Mostaql' WHEN number>=5 AND number<=6 THEN 'ANA' WHEN number=7 THEN 'IO' ELSE '.com' END) FROM case_example ; case --------- Hsoub Khamsat Khamsat Mostaql ANA ANA (6 rows) التعديل UPDATE والحذف DELETE بعد أن تعرّفنا على كيفية ترشيح البيانات في الاستعلامات، سنستخدم تعليمة الترشيح نفسها WHERE لتحديد الأسطر التي نرغب بالتعديل على محتوياتها أو حذفها. انتبه: في هذه الفقرة نقوم بالتعديل على محتويات الجدول من البيانات، أو حذف سطر ما من أسطر الجدول، أما لو أردت التعديل على الجدول نفسه بإضافة عمود أو حذف عمود، أو التعديل على اسم عمود ما أو نوع بياناته، فعليك استخدام التعليمة ALTER المذكورة في فقرة تعديل الجداول. سننشئ أولًا جدولًا جديدًا، كيف لا تؤثر على جداولنا السابقة التي تحوي معلومات مهمة: hsoubguide=# CREATE TABLE test_table( hsoubguide(# id integer PRIMARY KEY, hsoubguide(# number integer, hsoubguide(# name character varying(5) hsoubguide(# ); CREATE TABLE hsoubguide=# INSERT INTO test_table VALUES hsoubguide-# (1,10,'hello'), hsoubguide-# (2,13,'mosta'), hsoubguide-# (3,-5,'test2'), hsoubguide-# (4,22,'hel..'), hsoubguide-# (5,-9,'test1'); INSERT 0 5 وهذه هي محتويات الجدول الجديد: hsoubguide=# SELECT * from test_table; id | number | name ----+--------+------- 1 | 10 | hello 2 | 13 | mosta 3 | -5 | test2 4 | 22 | hel.. 5 | -9 | test1 (5 rows) التعديل على الأسطر يمكننا باستخدام الأمر UPDATE مع إضافة التوجيه SET تعديل الأسطر التي تحقق الشرط المذكور في تعليمة الترشيح WHERE كما في المثال التالي: hsoubguide=# UPDATE test_table SET name = 'O_O' WHERE id =1; UPDATE 1 hsoubguide=# SELECT * FROM test_table ; id | number | name ----+--------+------- 2 | 13 | mosta 4 | 22 | hel.. 3 | -50 | test2 5 | -90 | test1 1 | 10 | O_O (5 rows) كما يمكن تعديل عدة أعمدة معًا حسب شرط محدد، كما يلي: hsoubguide=# UPDATE test_table hsoubguide=# SET name = ('WOW' || id ), hsoubguide=# number = id*10 hsoubguide=# WHERE id > 3; UPDATE 2 hsoubguide=# SELECT * from test_table ; id | number | name ----+--------+------- 2 | 13 | mosta 3 | -50 | test2 1 | 10 | O_O 4 | 40 | WOW4 5 | 50 | WOW5 (5 rows) عدَّلنَا في المثال السابق القيمة في العمود name لتحتوي كلمة WOW موصولة بالرقم المحتوى في العمود id للسطر نفسه. وعدَّلنا القيمة في العمود number لتكون ناتج ضرب الرقم الموجود في العمود id مضروبًا بـ10. وهذه التعديلات تتم على الأسطر التي يتحقق فيها الشرط id>3. في حال لم نضع أي شرط على القيام بالتعديل، فإن التعديل ينفّذ على جميع الأسطر، كما يلي: hsoubguide=# UPDATE test_table hsoubguide=# SET number = LENGTH(name); UPDATE 5 hsoubguide=# SELECT * from test_table ; id | number | name ----+--------+------- 2 | 5 | mosta 3 | 5 | test2 1 | 3 | O_O 4 | 4 | WOW4 5 | 4 | WOW5 (5 rows) استخدمنا التابع LENGTH الذي يُرجع عدد الحروف للوسيط المُمرَّر له، ثم أسندناها للعمود number، وذلك دون استخدام أي شرط، لذلك تم تنفيذ التعديلات على جميع الأسطر. إجراء عمليات حسابية على الأعمدة يمكننا استخدام تعليمة التعديل UPDATE لتعديل قيم عمود ما عن طريق عملية حسابية أو أي عملية أخرى، كما في المثال التالي: hsoubguide=# UPDATE test_table hsoubguide-# SET number = number*10 hsoubguide-# WHERE number >3 hsoubguide-# ; UPDATE 4 hsoubguide=# SELECT * from test_table hsoubguide-# ; id | number | name ----+--------+------- 1 | 3 | O_O 2 | 50 | mosta 3 | 50 | test2 4 | 40 | WOW4 5 | 40 | WOW5 (5 rows) تعديل أسطر الجدول مع إظهار التعديلات يمكن استخدام الكلمة المفتاحية RETURNING بشكل مشابه للاستعلام SELECT في نهاية عبارة التعديل، بحيث يتم التعديل ثم الاستعلام عن بعض الأعمدة في الجدول المعدّل وعرضها، وذلك كما في المثال التالي: hsoubguide=# UPDATE test_table hsoubguide=# SET name=number hsoubguide=# WHERE id<3 hsoubguide=# RETURNING id,number,name; id | number | name ----+--------+------ 1 | 10 | 10 2 | 13 | 13 (2 rows) UPDATE 2 حذف الأسطر يمكننا حذف سطر ما عن طريق تحديد شرط ما يشير إلى ذلك السطر بالذات دون سواه، ويُعد المعرّف الخاص بالسطر أفضل ما يمكن تحديده لحذف سطرٍ ما مع ضمان عدم حذف سواه، وذلك لأن له المواصفة PRIMARY KEY التي تمنع إنشاء سطرين بنفس القيمة في العمود id. أوًلًا سنسترجع الجدول الخاص بنا قبل إجراء عمليات التعديل في الفقرة السابقة، وذلك عن طريق حذف جميع الأسطر، ثم إدخالها مرةً أخرى. يمكنك حذف جميع محتويات الجدول كما يلي: hsoubguide=# DELETE FROM test_table; DELETE 5 hsoubguide=# SELECT * FROM test_table; id | number | name ----+--------+------ (0 rows) ثم سنعيد إدخال الأسطر كما سبق: hsoubguide=# INSERT INTO test_table VALUES hsoubguide-# (1,10,'hello'), hsoubguide-# (2,13,'mosta'), hsoubguide-# (3,-5,'test2'), hsoubguide-# (4,22,'hel..'), hsoubguide-# (5,-9,'test1'); INSERT 0 5 سنحذف الآن السطر ذا المُعرّف id بقيمة 5: hsoubguide=# DELETE FROM test_table hsoubguide-# WHERE id=5; DELETE 1 hsoubguide=# SELECT * FROM test_table; id | number | name ----+--------+------- 1 | 10 | hello 2 | 13 | mosta 3 | -5 | test2 4 | 22 | hel.. (4 rows) تحذير: تُعد تعليمة DELETE من التعليمات الخطيرة، فالسطر المحذوف لا يمكن استعادته إلا في حال وجود نسخة احتياطية عن قاعدة البيانات، فاستخدم هذه التعليمة بحذر. يمكننا طبعًا استخدام تعليمة الترشيح WHERE لتحديد أكثر من سطر، ومن ثمّ سيتم حذف جميع الأسطر التي تحقق الشرط المحدد، فمثلًا يمكننا حذف جميع الأسطر التي تكون قيمة العمود number فيها موجبة كما يلي: hsoubguide=# DELETE FROM test_table hsoubguide=# WHERE number>0; DELETE 3 hsoubguide=# SELECT * from test_table; id | number | name ----+--------+------- 3 | -5 | test2 (1 row) الربط Join سنتعرف في هذه الفقرة على أسلوب أساسي في استخراج البيانات المرتبطة من عدة جداول، وطريقتها الرئيسية ببساطة، هي طلب البيانات من عدة جداول ثم تصفيتها، وسنبدأ بأبسط طرق الربط على الإطلاق، ونتدرج فيها إلى الوصول إلى أنواع الربط المتعددة. الاستعلام من عدة جداول معًا سننشئ جدولين للاختبار وإدخال بيانات إليهما، كي نتمكن من استعراض المثال الخاص بنا: الجدول الأول اسمه names فيه عمودان، الأول رقم معرّف للشخص، والثاني فيه اسمه. hsoubguide=# CREATE TABLE names( hsoubguide(# id integer PRIMARY KEY, hsoubguide(# name character varying(255) hsoubguide(# ); CREATE TABLE hsoubguide=# INSERT INTO names VALUES hsoubguide-# (1,'mostafa'), hsoubguide-# (2,'ali'), hsoubguide-# (3,'fares'); INSERT 0 3 hsoubguide=# SELECT * FROM names; id | name ----+--------- 1 | mostafa 2 | ali 3 | fares (3 rows) أما الجدول الثاني، ففيه أرقام هواتف نفس الأشخاص، ولكن لن نضع فيه أسماءهم، ففيه الرقم المعرّف بالشخص، ورقم هاتفه: hsoubguide=# CREATE TABLE phones( hsoubguide(# id integer PRIMARY KEY, hsoubguide(# phone character varying(15) hsoubguide(# ); CREATE TABLE hsoubguide=# INSERT INTO phones VALUES hsoubguide-# (1,'+966123456789'), hsoubguide-# (2,'+961111111111'), hsoubguide-# (3,'+962333333333'); INSERT 0 3 hsoubguide=# SELECT * FROM phones; id | phone ----+--------------- 1 | +966123456789 2 | +961111111111 3 | +962333333333 (3 rows) فلنبدأ بالاستعلام عن البيانات من الجدولين معًا: hsoubguide=# SELECT * FROM names, phones; id | name | id | phone ----+---------+----+--------------- 1 | mostafa | 1 | +966123456789 2 | ali | 1 | +966123456789 3 | fares | 1 | +966123456789 1 | mostafa | 2 | +961111111111 2 | ali | 2 | +961111111111 3 | fares | 2 | +961111111111 1 | mostafa | 3 | +962333333333 2 | ali | 3 | +962333333333 3 | fares | 3 | +962333333333 (9 rows) لاحظ كيف تم إظهار جميع أسطر الجدول الأول مرتبطة بسطر واحد من الجدول الثاني، ثم مرةً أخرى، جميع أسطر الجدول الأول مرتبطة بالسطر الثاني من الجدول الثاني وهكذا. ماذا لو أردنا عرض اسم الشخص مع رقم هاتفه؟ يكفي أن نختار الأسطر التي يتوافق فيها العمود id من الجدول names مع العمود id من الجدول phones، ولكن علينا كتابة اسم الجدول تليه نقطة . ثم اسم العمود id كي نتمكن من تمييز العمود الموجود في الجدول الأول عن الآخر في الجدول الثاني، كما يلي: hsoubguide=# SELECT * FROM phones, names hsoubguide-# WHERE phones.id = names.id; id | phone | id | name ----+---------------+----+--------- 1 | +966123456789 | 1 | mostafa 2 | +961111111111 | 2 | ali 3 | +962333333333 | 3 | fares (3 rows) قمنا لتوّنا بالربط بين جدولين، وسنقوم بتحسين المخرجات الآن قليلًا، بعرض العمود id مرة واحدة: hsoubguide=# SELECT phones.id,phone,name hsoubguide-# FROM phones,names hsoubguide-# WHERE phones.id = names.id; id | phone | name ----+---------------+--------- 1 | +966123456789 | mostafa 2 | +961111111111 | ali 3 | +962333333333 | fares (3 rows) والآن يمكننا أن نتّجه إلى تطبيق الربط على مثالنا الخاص بالمنتجات المشتراة، هل تتذكر جدول المنتجات المُشتراة: hsoubguide=# SELECT * hsoubguide-# FROM purchase_items hsoubguide-# LIMIT 6; id | purchase_id | product_id | price | quantity | state ----+-------------+------------+--------+----------+----------- 2 | 1 | 3 | 27.99 | 1 | Delivered 3 | 1 | 8 | 108.00 | 1 | Delivered 4 | 2 | 1 | 9.99 | 2 | Delivered 5 | 3 | 12 | 9.99 | 1 | Delivered 6 | 3 | 17 | 14.99 | 4 | Delivered (6 rows) لاحظ الأعمدة الأولى، purchase_id يدل على المعرّف الخاص بعملية الشراء كاملة (التي يقوم بها زبون ما في وقت محدد)، والعمود product_id الذي يحدد المعرف الخاص بالمنتج الذي تم شراؤه. هذه الأعمدة لا تحتوي بيانات عن المنتجات أو عن عملية الشراء، بل هي تشير إلى المعرّف الخاص بها في الجداول الخاصة بها، فهل يمكننا استخدام هذا المعرّف لجلب تلك البيانات من جداولها ثم دمج تلك البيانات مع بيانات الجدول الحالي؟ ما نحاول الوصول إليه في هذه الفقرة، هو ربط عدة جداول مع بعضها، للحصول على معلومات متكاملة في جدول الخرج، فبدلًا من عرض المعرّف الخاص بالمنتج، نريد عرض اسمه مثلًا، وبدلًا من عرض المعرّف الخاص بعملية الشراء نريد عرض المستخدم الذي قام بالشراء. فلنلقِ نظرة على مخطط قاعدة البيانات الخاصة بنا: hsoubguide=# \dt List of relations Schema | Name | Type | Owner --------+----------------+-------+---------- public | products | table | postgres public | purchase_items | table | postgres public | purchases | table | postgres public | users | table | postgres (4 rows) وسنقوم في مثالنا التالي بالاستعلام عن المنتجات التي تم شراؤها مؤخرًا، ونحتاج للقيام بذلك إلى بيانات من جدولي المنتجات والمشتريات معًا، وسنبدأ بأخذ نظرة إلى الجدولين لمعرفة الأعمدة التي سنتحتاج إليها: يحوي الجدول products على تفاصيل المنتجات، كاسم المنتج وسعره: hsoubguide=# SELECT * from products; id | title | price | created_at | deleted_at | tags ----+------------------+--------+------------------------+------------+--------------------------- 1 | Dictionary | 9.99 | 2011-01-01 22:00:00+02 | | {Book} 2 | Python Book | 29.99 | 2011-01-01 22:00:00+02 | | {Book,Programming,Python} 3 | Ruby Book | 27.99 | 2011-01-01 22:00:00+02 | | {Book,Programming,Ruby} 4 | Baby Book | 7.99 | 2011-01-01 22:00:00+02 | | {Book,Children,Baby} 5 | Coloring Book | 5.99 | 2011-01-01 22:00:00+02 | | {Book,Children} 6 | Desktop Computer | 499.99 | 2011-01-01 22:00:00+02 | | {Technology} 7 | Laptop Computer | 899.99 | 2011-01-01 22:00:00+02 | | {Technology} 8 | MP3 Player | 108.00 | 2011-01-01 22:00:00+02 | | {Technology,Music} 9 | 42" LCD TV | 499.00 | 2011-01-01 22:00:00+02 | | {Technology,TV} 10 | 42" Plasma TV | 529.00 | 2011-01-01 22:00:00+02 | | {Technology,TV} 11 | Classical CD | 9.99 | 2011-01-01 22:00:00+02 | | {Music} 12 | Holiday CD | 9.99 | 2011-01-01 22:00:00+02 | | {Music} 13 | Country CD | 9.99 | 2011-01-01 22:00:00+02 | | {Music} 14 | Pop CD | 9.99 | 2011-01-01 22:00:00+02 | | {Music} 15 | Electronic CD | 9.99 | 2011-01-01 22:00:00+02 | | {Music} 16 | Comedy Movie | 14.99 | 2011-01-01 22:00:00+02 | | {Movie,Comedy} 17 | Documentary | 14.99 | 2011-01-01 22:00:00+02 | | {Movie} 18 | Romantic | 14.99 | 2011-01-01 22:00:00+02 | | {Movie} 19 | Drama | 14.99 | 2011-01-01 22:00:00+02 | | {Movie} 20 | Action | 14.99 | 2011-01-01 22:00:00+02 | | {Movie} (20 rows) بينما يحوي الجدول purchase_items على تفاصيل عمليات الشراء، ففي كل عملية شراء، يمكن أن يكون هناك عدة منتجات مُشتراة، حيث يحوي السطر الواحد في هذا الجدول على عملية شراء لمنتج واحد، ويخزن العمود purchase_id رقمًا تسلسليًّا لعملية الشراء الكاملة، ويشير العمود product_id إلى الرقم المعرّف للمنتج، وهو نفسه الموجود في الجدول السابق products وبفضل وجوده يمكننا الربط بين الجدولين. hsoubguide=# SELECT * from purchase_items LIMIT 12; id | purchase_id | product_id | price | quantity | state ----+-------------+------------+--------+----------+----------- 2 | 1 | 3 | 27.99 | 1 | Delivered 3 | 1 | 8 | 108.00 | 1 | Delivered 4 | 2 | 1 | 9.99 | 2 | Delivered 5 | 3 | 12 | 9.99 | 1 | Delivered 6 | 3 | 17 | 14.99 | 4 | Delivered 7 | 3 | 11 | 9.99 | 1 | Delivered 8 | 4 | 4 | 7.99 | 3 | Delivered 9 | 5 | 18 | 14.99 | 1 | Delivered 10 | 5 | 2 | 29.99 | 4 | Delivered 11 | 6 | 5 | 5.99 | 1 | Delivered 12 | 7 | 6 | 499.99 | 3 | Returned 13 | 8 | 10 | 529.00 | 1 | Delivered (12 rows) الطريقة الأساسية لربط الجداول هي المفاتيح (keys) وسنشرح عنها لاحقًا باستفاضة أكثر، أما ما يهمنا الآن هو معرفة أن العمود product_id في جدول المشتريات يشير إلى العمود id في جدول المنتجات. والآن يمكننا إنشاء الاستعلام كما يلي: SELECT products.title, purchase_items.quantity FROM products, purchase_items WHERE products.id = purchase_items.product_id LIMIT 12; لاحظ كيف يتم طلب عمودين من جدولين مختلفين عن طريق كتابة اسم الجدول تتبعه نقطة . ثم اسم العمود المطلوب. وستكون المخرجات بالشكل التالي: title | quantity ------------------+---------- Ruby Book | 1 MP3 Player | 1 Dictionary | 2 Holiday CD | 1 Documentary | 4 Classical CD | 1 Baby Book | 3 Romantic | 1 Python Book | 4 Coloring Book | 1 Desktop Computer | 3 42" Plasma TV | 1 (12 rows) نلاحظ في المثال السابق، أنه ليس لدينا تفاصيل عن عملية الشراء، سوى عن الكمية المشتراة من كل منتج، وسنحاول في المثال التالي استعراض المنتج والكمية المشتراة، مع ذكر اسم المشتري وتاريخ الشراء، كي تكون المعلومات المعروضة مفيدة أكثر. نستعرض أولًا الجدول purchases: hsoubguide=# SELECT * FROM purchases LIMIT 7; id | created_at | name | address | state | zipcode | user_id ----+------------------------+--------------------+----------------+-------+---------+--------- 1 | 2011-03-16 17:03:00+02 | Harrison Jonson | 6425 43rd St. | FL | 50382 | 7 2 | 2011-09-14 08:00:00+03 | Cortney Fontanilla | 321 MLK Ave. | WA | 43895 | 30 3 | 2011-09-11 08:54:00+03 | Ruthie Vashon | 2307 45th St. | GA | 98937 | 18 4 | 2011-02-27 22:53:00+02 | Isabel Wynn | 7046 10th Ave. | NY | 57243 | 11 5 | 2011-12-20 14:45:00+02 | Shari Dutra | 4046 8th Ave. | FL | 61539 | 34 6 | 2011-12-10 15:29:00+02 | Kristofer Galvez | 2545 8th Ave. | WA | 83868 | 39 7 | 2011-06-19 06:42:00+03 | Maudie Medlen | 2049 44th Ave. | FL | 52107 | 8 (7 rows) وجدنا الأعمدة التي كنا نبحث عنها في ما أردنا الوصول إليه قبل قليل، إنها الأعمدة name و created_at، ويمكننا الآن ربط هذين العمودين بالاستعلام السابق للحصول على مخرجات أكثر فائدة: SELECT products.title, purchase_items.quantity, purchases.name, purchases.created_at FROM products, purchase_items, purchases WHERE products.id = purchase_items.product_id AND purchases.id = purchase_items.purchase_id LIMIT 12; لاحظ في المثال السابق عبارة WHERE المرشِّحة، حيث وضعنا فيها شرطين. وسنحصل على المخرجات التالية: title | quantity | name | created_at ------------------+----------+--------------------+------------------------ Ruby Book | 1 | Harrison Jonson | 2011-03-16 17:03:00+02 MP3 Player | 1 | Harrison Jonson | 2011-03-16 17:03:00+02 Dictionary | 2 | Cortney Fontanilla | 2011-09-14 08:00:00+03 Holiday CD | 1 | Ruthie Vashon | 2011-09-11 08:54:00+03 Documentary | 4 | Ruthie Vashon | 2011-09-11 08:54:00+03 Classical CD | 1 | Ruthie Vashon | 2011-09-11 08:54:00+03 Baby Book | 3 | Isabel Wynn | 2011-02-27 22:53:00+02 Romantic | 1 | Shari Dutra | 2011-12-20 14:45:00+02 Python Book | 4 | Shari Dutra | 2011-12-20 14:45:00+02 Coloring Book | 1 | Kristofer Galvez | 2011-12-10 15:29:00+02 Desktop Computer | 3 | Maudie Medlen | 2011-06-19 06:42:00+03 42" Plasma TV | 1 | Isabel Crissman | 2011-05-28 04:19:00+03 (12 rows) في المثال السابق، ربطنا ثلاثة جداول للحصول على معلومات مترابطة وإظهارها في جدول مخرجات واحد، ولكن ربما لاحظت كيف أصبح الاستعلام طويلًا، ومعقّدًا بعض الشيء، ولذلك وُجد العرض View كطريقة لحفظ الاستعلام كجدول منطقي جديد، لا يُخزّن في الذاكرة، ولكن يمكننا التعامل معه كأنه كذلك، وهو ما سنتحدث عنه بعد استعراض أنواع الربط المختلفة. أنواع الربط سنستعرض أنواعًا أخرى من الربط الذي قمنا به في الفقرات السابقة، وسنستخدم لذلك مثالًا جديدًا، ننشئ فيه جدولين، الأول لتخزين اسم الطالب وكنيته، والثاني نخزّن فيه اسم الطالب وعلامته في مادة الرياضيات. ملاحظة: للاستفادة الكاملة من هذا الدليل، نتمنّى أن تطبِّق عزيزي القارئ الأمثلة بنفسك، كي يحصل على الخبرة العملية إلى جانب الفهم النظري للمحتوى. hsoubguide=# CREATE TABLE student( hsoubguide(# id integer PRIMARY KEY, hsoubguide(# name character varying(255), hsoubguide(# last_name character varying (255) hsoubguide(# ); CREATE TABLE hsoubguide=# CREATE TABLE marks( hsoubguide(# id integer PRIMARY KEY, hsoubguide(# name character varying(255), hsoubguide(# marks integer hsoubguide(# ); CREATE TABLE hsoubguide=# INSERT INTO student VALUES hsoubguide-# (1,'mostafa','ayesh'), hsoubguide-# (2,'ali','badawi'), hsoubguide-# (3,'samer','khateeb'), hsoubguide-# (4,'amer','masre'); INSERT 0 4 hsoubguide=# INSERT INTO marks VALUES hsoubguide-# (1,'ali',14), hsoubguide-# (2,'ramez',20), hsoubguide-# (3,'amer',16), hsoubguide-# (4,'fadi',18); INSERT 0 4 لاحظ أن جدول العلامات لا يحتوي جميع الطلاب المذكورة أسمائهم في جدول الطلاب، وكذلك يحتوى طلابًا غير مذكورين في جدول الطلاب، ولاحظ كذلك أن المعرّف id لطالب ما، ليس من الضروري أن يطابق المعرّف الخاص به في جدول العلامات ! ولذلك فلن نتمكن من ربط الجدولين عن طريق المعرّف الخاص بالسطر. الربط الداخلي INNER JOIN نستعرض فيه التقاطع بين جدولين دون أي معلومات إضافية: hsoubguide=# SELECT student.name,marks.mark hsoubguide-# FROM student hsoubguide-# INNER JOIN marks hsoubguide-# ON student.name = marks.name; name | mark ------+------ ali | 14 amer | 16 (2 rows) يبدو ذلك مشابهًا لما رأيناه في الفقرة السابقة: hsoubguide=# SELECT student.name,marks.mark hsoubguide-# FROM student,marks hsoubguide-# WHERE student.name = marks.name; name | mark ------+------ ali | 14 amer | 16 (2 rows) الربط اليساري LEFT JOIN في هذا الربط، يتم عرض جميع أسطر الجدول اليساري (أي الجدول المرتبط بالتوجيه FROM) ويتم عرض الأسطر التي تقابلها أو NULL في حال عدم وجود ما يقابلها: hsoubguide=# SELECT student.name,marks.mark hsoubguide-# FROM student hsoubguide-# LEFT JOIN marks hsoubguide-# ON student.name = marks.name; name | mark ---------+------ ali | 14 amer | 16 samer | mostafa | (4 rows) الربط اليساري الخرجي LEFT OUTER JOIN نستخدمه للحصول على الأسطر التي ليس لها ما يقابلها في الجدول اليميني كما يلي: hsoubguide=# SELECT student.name,marks.mark hsoubguide-# FROM student hsoubguide-# LEFT JOIN marks hsoubguide-# ON student.name = marks.name hsoubguide-# WHERE mark IS NULL; name | mark ---------+------ samer | mostafa | (2 rows) الربط اليميني RIGHT JOIN كما في الربط اليساري، إلا أن الربط اليميني يظهر جميع أسطر الجدول المرتبط بالتعليمة JOIN (في مثالنا هو الجدول marks)، بينما يظهر الأسطر التي لها مقابل فقط من الجدول الأول (اليساري) أو NULL في حال عدم وجود تقابل، كما يلي: hsoubguide=# SELECT student.name,marks.mark hsoubguide-# FROM student hsoubguide-# RIGHT JOIN marks hsoubguide-# ON student.name = marks.name; name | mark ------+------ ali | 14 | 20 amer | 16 | 18 (4 rows) من الواضح في المثال السابق أن العبارة SELECT student.name, marks.mark غير مناسبة لهذا النوع من الربط، وذلك لأن العلامات التي ليس لها مقابل لأسماء أصحابها في الجدول names ستظهر بلا أسماء، وهذا أمر غير منطقي في العرض، لذلك سنضيف إضافةً بسيطة لحل هذه المشكلة: hsoubguide=# SELECT COALESCE(student.name,marks.name) AS student_name, hsoubguide=# marks.mark hsoubguide=# FROM student hsoubguide=# RIGHT JOIN marks hsoubguide=# ON student.name = marks.name; student_name | mark --------------+------ ali | 14 ramez | 20 amer | 16 fadi | 18 (4 rows) استخدمنا التابع COALESCE الذي يؤدي إلى عرض قيمة السطر من الجدول الأول له إن لم يكن قيمته NULL، وإلا فإنه يعرض قيمة السطر من الجدول الثاني، كما استخدمنا التوجيه AS كي لا يظهر اسم التابع في جدول المخرجات، ويظهر بدلًا منه العبارة student_name. الربط اليميني الخارجي RIGHT OUTER JOIN هنا يتم استثناء الأسطر التي لها مقابل في الجدول الأول، ونستعرض الأسطر التي ليس لها مقابل من الجدول الأول فقط. hsoubguide=# SELECT COALESCE(student.name,marks.name) AS Student_name, hsoubguide-# marks.mark hsoubguide-# FROM student hsoubguide-# RIGHT JOIN marks hsoubguide-# ON student.name = marks.name hsoubguide-# WHERE student.name IS NULL; student_name | mark --------------+------ ramez | 20 fadi | 18 (2 rows) الربط الخارجي الكامل FULL OUTER JOIN يتم فيه إظهار جميع الأسطر من جميع الجداول، سواء كان لها مقابل (عندها تُعرض قيمته) أو لم يكن لها مقابل (يُعرض NULL). hsoubguide=# SELECT COALESCE(student.name,marks.name) AS Student_name, hsoubguide-# marks.mark hsoubguide-# FROM student hsoubguide-# FULL OUTER JOIN marks hsoubguide-# ON student.name = marks.name; student_name | mark --------------+------ ali | 14 ramez | 20 amer | 16 fadi | 18 samer | mostafa | (6 rows) ما رأيكم لو نضع علامة الصفر لكل من لم يقدم الامتحان؟ نترك لكم تنفيذ الاستعلام التالي: SELECT COALESCE(student.name,marks.name) AS Student_name, COALESCE(marks.mark,0) AS mark FROM student FULL OUTER JOIN marks ON student.name = marks.name; الربط الخارجي الكامل حصًرا FULL OUTER JOIN-only نستخدم هذا الربط لاستعراض جميع الأسطر من الجدولين باستثناء الأسطر المرتبطة ببعضها من الجدولين معًا حسب شرط الربط، فهو بذلك يعطي نتائج الربط الخارجي الكامل، مستثنى منها نتائج الربط الداخلي: hsoubguide=# SELECT COALESCE(student.name,marks.name) AS Student_name, hsoubguide-# COALESCE(marks.mark,0) hsoubguide-# FROM student hsoubguide-# FULL OUTER JOIN marks hsoubguide-# ON student.name = marks.name hsoubguide-# WHERE student.name IS NULL OR marks.name IS NULL; student_name | coalesce --------------+---------- ramez | 20 fadi | 18 samer | 0 mostafa | 0 (4 rows) خلاصة تعرفنا في هذا الفصل على كيفية إنشاء وتعديل وحذف الجداول، وكذلك تعرفنا على كيفية إدخال البيانات فيها ثم الاستعلام عنها مع ترشيحها والاستفادة من العديد من الأوامر للحصول على المخرجات المطلوبة، كما تعرفنا على عمليات التجميع وعلى طرق ربط الجداول للحصول على النتائج المستخلصة من عدة مصادر. ومع ازدياد خبرتك في التعامل مع الاستعلامات، وخصوصًا بعد تعرفك على طرق ربط الجداول في هذا الفصل، ربما تكون قد لاحظت أن بعض الاستعلامات قد أصبحت طويلة، وصعبة القراءة، وبعضها يتكرر كثيرًا مع بعض التعديلات، ولذا فقد آن الأوان أن نتعرف إلى العرض View وبعض المزايا المتقدمة في Postgres في الفصل التالي، ليسهل علينا استخدام الاستعلامات الطويلة. اقرأ أيضًا المقال التالي: مزايا متقدمة في Postgres المقال السابق: أساسيات استخدام صدفة psql النسخة الكاملة من كتاب الدليل العملي إلى قواعد بيانات PostgreSQL المرجع المتقدم إلى لغة SQL
-
سنستخدم صدفة psql طوال الوقت في هذا الدليل، وسنستخدم بعض الرايات فيها أيضًا، لذا سنخصص هذا المقال للتعرف على بعض أهم الأوامر فيها. صدفة psql هي برنامج الواجهة التفاعلية للاتصال بـ Postgres ولها العديد من الرايات للتحكم بالاتصال منها: الراية -h لتحديد المضيف المراد الاتصال به (سواء عن طريق عنوان IP أو عن طريق اسم المضيف إن كان يمكن لخادوم DNS التعرف إليه) الراية -U لتحديد اسم المستخدم المراد الاتصال من خلاله الراية -p المنفَذ port المراد الاتصال عبره (المنفذ الافتراضي هو 5423) psql -h localhost -U username hsoubguide كما يمكن استخدام سلسلة نصية كاملة كوسيط واحد، تحتوي محددات الدخول إلى قاعدة البيانات: psql "dbname=hsoubguide host=10.11.108.107 user=postgres password=pass123456 port=5432 sslmode=require" بعد نجاح الاتصال يمكن البدء بتطبيق الاستعلامات، كما يمكن استخدام أوامر معينة، ويمكن تنفيذ الأمر \? للحصول على قائمة بجميع الأوامر المتاحة، والتي سنشرح بعضًا من أهمها في الفقرات التالية. أوامر استعراض قاعدة البيانات والجداول استعراض جميع قواعد البيانات الموجودة استخدم الأمر '\l' لاستعراض قائمة بجميع قواعد البيانات المخزّنة: hsoubguide=# \l List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges ------------+----------+----------+-------------+-------------+----------------------- hsoubguide | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | postgres | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | template0 | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | =c/postgres + | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | =c/postgres + | | | | | postgres=CTc/postgres (4 rows) استعراض جميع قواعد البيانات مع معلومات إضافية بإضافة الرمز + إلى الأمر \l يمكن عرض قائمة لجميع قواعد البيانات المخزنة مع معلومات إضافية عن كل منها. hsoubguide=# \l+ List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges | Size | Tablespace | Description ------------+----------+----------+-------------+-------------+-----------------------+---------+------------+---------------------------------------- ---- hsoubguide | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | | 8273 kB | pg_default | postgres | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | | 8345 kB | pg_default | default administrative connection datab ase template0 | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | =c/postgres +| 8049 kB | pg_default | unmodifiable empty database | | | | | postgres=CTc/postgres | | | template1 | postgres | UTF8 | en_GB.UTF-8 | en_GB.UTF-8 | =c/postgres +| 8049 kB | pg_default | default template for new databases | | | | | postgres=CTc/postgres | | | (4 rows) استعراض الجداول في قاعدة البيانات لاستعراض قائمة بالجداول الموجودة في قاعدة البيانات، نستخدم الأمر \dt كما يلي: hsoubguide=# \dt List of relations Schema | Name | Type | Owner --------+----------------------+-------+---------- public | basket_a | table | postgres public | basket_b | table | postgres public | departments | table | postgres public | employee_departments | table | postgres public | employees | table | postgres public | marks | table | postgres public | names | table | postgres public | phones | table | postgres public | products | table | postgres public | purchase_items | table | postgres public | purchases | table | postgres public | size_calc | table | postgres public | student | table | postgres public | table1 | table | postgres public | table2 | table | postgres public | test_explain | table | postgres public | test_table | table | postgres public | users | table | postgres public | users2 | table | postgres (19 rows) وصف جدول لوصف جدول، نستخدم الأمر \d مع اسم الجدول كما يلي: hsoubguide=# \d employees Table "public.employees" Column | Type | Collation | Nullable | Default -----------+-----------------------+-----------+----------+--------- id | integer | | not null | last_name | character varying(55) | | | salary | integer | | | Indexes: "employees_pkey" PRIMARY KEY, btree (id) استعراض الجداول مع بعض المعلومات الإضافية بإضافة الرمز + إلى أي أمر من أوامر الاستعراض، فإنها تضيف إلى المخرجات المزيد من المعلومات، فعند إضافة الرمز + إلى الأمر \dt يمكننا استعراض المزيد من المعلومات عن الجداول الموجودة في قاعدة البيانات: hsoubguide=# \dt+ List of relations Schema | Name | Type | Owner | Size | Description --------+----------------------+-------+----------+------------+------------- public | basket_a | table | postgres | 8192 bytes | public | basket_b | table | postgres | 8192 bytes | public | departments | table | postgres | 8192 bytes | public | employee_departments | table | postgres | 8192 bytes | public | employees | table | postgres | 8192 bytes | public | marks | table | postgres | 8192 bytes | public | names | table | postgres | 8192 bytes | public | phones | table | postgres | 8192 bytes | public | products | table | postgres | 16 kB | public | purchase_items | table | postgres | 304 kB | public | purchases | table | postgres | 96 kB | public | size_calc | table | postgres | 0 bytes | public | student | table | postgres | 16 kB | public | table1 | table | postgres | 8192 bytes | public | table2 | table | postgres | 8192 bytes | public | test_explain | table | postgres | 8192 bytes | public | test_table | table | postgres | 8192 bytes | public | users | table | postgres | 16 kB | public | users2 | table | postgres | 8192 bytes | (19 rows) وصف جدول مع معلومات إضافية للحصول على المزيد من المعلومات عن جدول ما، يمكننا استخدام الرمز + مع الأمر '\d' وبعدها اسم الجدول: hsoubguide=# \d+ users Table "public.users" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description ------------+--------------------------+-----------+----------+-----------------------------------+----------+--------------+------------- id | integer | | not null | nextval('users_id_seq'::regclass) | plain | | email | character varying(255) | | | | extended | | password | character varying(255) | | | | extended | | details | hstore | | | | extended | | created_at | timestamp with time zone | | | | plain | | deleted_at | timestamp with time zone | | | | plain | | Indexes: "users_pkey" PRIMARY KEY, btree (id) Referenced by: TABLE "purchases" CONSTRAINT "purchases_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) Access method: heap استعراض المستخدمين وخصائصهم بتنفيذ الأمر \dg يمكننا استعراض قائمة الأدوار، وهي قائمة المستخدمين وخصائص كل منهم، كما يلي: hsoubguide=# \dg List of roles Role name | Attributes | Member of -----------+------------------------------------------------------------+----------- mostafa | | {} postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {} أوامر أخرى أساسية تحرير الاستعلامات في محرر النصوص يمكن فتح مخزن الاستعلامات ضمن محرر النصوص الافتراضي (مثل vi أو nano) داخل صدَفَة psql باستخدام الأمر \e وهو مفيد لكتابة الاستعلامات الطويلة وتحريرها قبل تنفيذها، وعند فتحه سنجده يحوي آخر استعلام تمت كتابته، مما يسمح لنا بتعديله لإعادة تنفيذه. كما يمكن استعراض آخر محتويات هذا المخزن باستخدام الأمر \p. hsoubguide=# SELECT * hsoubguide-# FROM products hsoubguide-# LIMIT hsoubguide-# 1 hsoubguide-# ; id | title | price | created_at | deleted_at | tags ----+------------+-------+------------------------+------------+-------- 1 | Dictionary | 9.99 | 2011-01-01 22:00:00+02 | | {Book} (1 row) hsoubguide=# \p SELECT * FROM products LIMIT 1 ; تشغيل توقيت الاستعلام في الحالة الافتراضية لا يكون توقيت تنفيذ الاستعلام مُتاحًا للعرض، ولكن يمكننا تفعيله من خلال الأمر التالي: hsoubguide=# SELECT * FROM products LIMIT 1; id | title | price | created_at | deleted_at | tags ----+------------+-------+------------------------+------------+-------- 1 | Dictionary | 9.99 | 2011-01-01 22:00:00+02 | | {Book} (1 row) Time: 0.723 ms حيث سيتيح ذلك الأمر إظهار توقيت الاستعلام بالميلي ثانية. الحصول على مساعدة بخصوص تعليمات SQL يمكن استخدام الأمر '\h' يليه اسم التعليمة في SQL لعرض التوثيق الخاص بهذه التعليمة: hsoubguide=# \h VACUUM Command: VACUUM Description: garbage-collect and optionally analyze a database Syntax: VACUUM [ ( option [, ...] ) ] [ table_and_columns [, ...] ] VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ table_and_columns [, ...] ] where option can be one of: FULL [ boolean ] FREEZE [ boolean ] VERBOSE [ boolean ] ANALYZE [ boolean ] DISABLE_PAGE_SKIPPING [ boolean ] SKIP_LOCKED [ boolean ] INDEX_CLEANUP [ boolean ] TRUNCATE [ boolean ] and table_and_columns is: table_name [ ( column_name [, ...] ) ] URL: https://www.postgresql.org/docs/12/sql-vacuum.html الخروج من صدفة postgres قد تقضي وقتًا طويلًا داخل صدفة psql، ليس حبًّا بها، ولكن لعدم معرفة كيفية الخروج منها، لذلك لا تنسَ أن الأمر \q هو الذي يُخرجك من صدفة psql. hsoubguide=# \q bash-4.2$ الخلاصة تعرفنا في هذا الفصل على الأوامر الأكثر استخدامًا في صدفة psql، ولكن لا يزال هناك العديد من الأوامر الأخرى التي يمكنك استعراضها من خلال الأمر \?. اقرأ أيضًا المقال التالي: استخدام أساسيات SQL في Postgres المقال السابق: تثبيت Postgres والتعرف على أساسيات إدارتها لقواعد البيانات النسخة الكاملة لكتاب الدليل العملي إلى قواعد بيانات PostgreSQL
-
سنتعرف في هذا الفصل على طريقة تثبيت Postgres على مختلف أنظمة التشغيل، كما سنهيئ قاعدة البيانات للعمل، ونتعلم كيفية إعطاء الصلاحيات للمستخدمين، ثم نبدأ باستخدام قاعدة البيانات الخاصة بنا. تثبيت Postgres سنذكر في هذه الفقرة الخطوات الأساسية لتثبيت Postgres على أنظمة التشغيل المختلفة. التثبيت على لينكس تختلف طريقة التثبيت حسب نوع التوزيعة، ولذلك سنذكر طريق التثبيت في التوزيعات الرئيسية. التثبيت باستخدام أداة yum (فيدورا، ريد هات، سنتوس، لينكس العلمي …) يشرح المثال التالي كيفية تثبيت PostgreSQL 12 على نظام CentOS 7.6 x64: توجه إلى PostgresSQL Yum Repository واختر إصدار PostgreSQL الذي تريد تثبيته ومن ثم حدد نظام التشغيل الخاص بك وإصداره والمعمارية، في هذا الدليل قمنا باستخدام نظام centos/redhat الإصدار 7، والمعمارية x86_64. نزّل حزمة RPM للمنصة الخاصة بك من الموقع أو نفّذ الأمر التالي من الطرفية Terminal: curl -O https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm ثبّت الحزمة كما يلي: rpm -ivh pgdg-redhat-repo-latest.noarch.rpm ملاحظة: قد تحتاج إلى استخدام sudo لتنفيذ الأمر السابق. ابحث سريعًا لعرض الحزم المتاحة لـ postgres باستخدام الأمر التالي: yum list postgres* ملاحظة: من الممكن أن يُظهر لك الأمر السابق إصدارات قديمة، لذا تأكد من اختيار النسخة المناسبة التي تريد تثبيتها ومن توافق جميع الحزم في رقم الإصدار للخادوم والعميل وأداة contrib (قد لا يكون ذلك ضروريًّا دومًا ولكن لأخذ الاحتياط من المشاكل التي قد تظهر). ثبّت الحزمة التي تختارها بشكل مشابه للأمر التالي: sudo yum install postgresql12 postgresql12-devel postgresql12-libs postgresql12-server postgresql12-contrib ثبَّتنا في الأمر السابق عدة حزم مرتبطة بالإصدار 12، ففيها كلا برنامجي الخادم والعميل، وكذلك المكاتب والملفات الرأسية headers، وكذلك المكاتب والإضافات للمساهمين في هذا المشروع مفتوح المصدر، إذ فضّلنا تثبيتها جميعًا كي تتمكن لاحقًا من استخدام أي منها. نصيحة: في حال ظهور خطأ ما أثناء التثبيت فلا تيأس، ابحث عن الخطأ الذي قد يظهر لك ولا شك أنك ستجد إجابة لسؤالك. الأنظمة التي تستخدم apt (أوبنتو، ديبيان، مينت…) يمكنك تطبيق الأمر التالي باستخدام apt-get: sudo apt-get install postgresql في نظام Arch Linux نستخدم الأمر pacman كما يلي: sudo pacman -S postgresql يمكنك أيضًا الرجوع إلى هذا الفيديو، تثبيت وإعداد قاعدة بيانات PostgreSQL لمزيد من التفاصيل حول عملية التثبيت على أنظمة لينكس. التثبيت على نظام ويندوز يمكنك استخدام أداة التثبيت لنظام ويندوز (ستحتاج إلى VPN إذا كنت في سوريا أو السودان، كالعادة)، وبعد أن تحمّل أداة التثبيت اتبع خطوات التثبيت المعروفة. التثبيت على نظام ماك يمكنك تحميل أداة التثبيت لنظام ماك من هذا الرابط. واتباع خطوات التثبيت المعروفة كذلك. أساسيات إدارة قواعد بيانات Postgres بعد تثبيت Postgres، سنبدأ العمل عليها مباشرةً وإنشاء أول قاعدة بيانات لنا بوساطتها، فهل أنت مستعد؟ لننطلق! إنشاء عنقود جديد لقواعد بيانات Postgres بعد أن ثبَّت خادم قواعد بيانات Postgres على جهازك، يمكننا الآن البدء باستخدامه، ولكن يجب إنشاء ما يُسمّى بعنقود قواعد البيانات (Database Cluster) أولًا. عنقود قواعد البيانات هو مجموعة من قواعد البيانات المُدارة عبر خادوم واحد، ولإنشائه علينا إنشاء المسارات التي ستُخزّن فيها بيانات قاعدة البيانات، كما علينا توليد جداول الإعدادات المشتركة وإنشاء قاعدتي البيانات template1 و postgres، يتم ذلك بشكل تلقائي كما سنرى بعد قليل، فلا تقلق. قاعدة البيانات template1 تُمثّل قالبًا يتم استخدامه عند إنشاء أي قاعدة بيانات جديدة، أما قاعدة البيانات postgres` فهي قاعدة بيانات افتراضية مصممة ليتم استخدامها من المستخدمين والأدوات والتطبيقات الأخرى. أولًا نقوم بإنشاء عنقود قاعدة بيانات باستخدام التوجيه initdb ضمن صدفة bash كما يلي: sudo /usr/pgsql-12/bin/postgresql-12-setup initdb ملحوظة: قمنا بكتابة المسار الكامل لبرمجية الإعداد والتهيئة (postgres-setup ) الخاصة بقواعد بيانات Postgres، ففي حال لم يتم العثور عليها في جهازك، فتأكد من أنك قمت بتثبيتها بطريقة صحيحة، ثم تأكد من أن مسار التثبيت الخاص بها. يُظهر تنفيذ الأمر السابق المخرجات التالية: Initializing database ... OK ثم يمكنك بدء وتفعيل PostgreSQL باستخدام الأمرين التاليين: sudo systemctl start postgresql-12 sudo systemctl enable postgresql-12 سنحصل من الأمر السابق على المخرجات التالية: Created symlink from /etc/systemd/system/multi-user.target.wants/postgresql-12.service to /usr/lib/systemd/system/postgresql-12.service. والآن أصبح خادم قاعدة بيانات PostgreSQL مفعّلًا ويمكننا استخدامه. إدارة المستخدمين وإنشاء قاعدة بيانات بدائية بعد أن قُمنا بتثبيت قواعد بيانات Postgres وإنشاء عنقود قواعد البيانات، ستكون الخطوة القادمة هي إضافة وإدارة صلاحيات مستخدمي قواعد البيانات المخزنة، حيث يمكن إنشاء مستخدمين والسماح لهم باستخدام قاعدة بيانات محددة دون الأخرى، أو السماح لهم بتنفيذ أوامر معيّنة، ومنعهم من تنفيذ غيرها. للدخول إلى الصَّدَفَة (shell) الرئيسية الخاصة بPostgres نشغّل برنامج الصدفة بالأمر psql، ولكن علينا أولا الانتقال من حساب المستخدم العادي إلى الحساب postgres وذلك عن طريق الأمر التالي: sudo -i -u postgres لتتأكد من المستخدم الذي تعمل عليه، استخدم الأمر whoami وقد تظهر لك مخرجات مشابهة لما يلي: [mostafa@hsoub ~]$ whoami mostafa وعندما تُبدّل المستخدم نعود وننفّذ الأمر whoami مرة أخرى كما يلي: [mayesh@hsoub ~]$ sudo -i -u postgres [sudo] password for mostafa: -bash-4.2$ whoami postgres يمكنك الآن الدخول إلى قاعدة البيانات بتنفيذ الأمر psql للبدء بالعمل، ثم سنقوم بإنشاء حساب مستخدم لك، باستخدام الأمر التالي: CREATE USER mostafa WITH PASSWORD 'password'; أُنشئ الآن حساب جديد باسم mostafa وبكلمة مرور password، والخطوة التالية هي إنشاء قاعدة بيانات ومنح المستخدم mostafa صلاحية الوصول لها. CREATE DATABASE hsoubguide; أُنشئت الآن قاعدة بيانات اسمها hsoubguide، وسنقوم الآن بمنح الوصول إليها للمستخدم mostafa بالأمر التالي: GRANT ALL PRIVILEGES ON DATABASE hsoubguide TO mostafa; مُنح الآن المستخدم mostafa كل الصلاحيات في قاعدة البيانات، حيث أن هنالك عدة أنواع مختلفة من الصلاحيات: SELECT, INSERT, UPDATE, DELETE, RULE, REFERENCES, TRIGGER, CREATE, TEMPORARY, EXECUTE, USAGE. أما إذا أردنا منح واحدة منها فقط، فيمكننا تنفيذ الأمر التالي: GRANT SELECT ON DATABASE hsoubguide TO mostafa; تسمح GRANT SELECT في هذا الأمر للمستخدم mostafa باستخدام استعلامات SELECT فقط في قاعدة البيانات hsoubguide. مثال عن قاعدة بيانات قد يكون اهتمام بعض قرّاء هذا الدليل في Postgres منصبًّا على الاستعلامات أكثر من اهتمامهم بتصميم الجداول الخاصة بهم، ولذلك فقد قدّمنا مثالًا عن قاعدة بيانات جاهزة يمكنك تحميلها، لتتمكن من المتابعة مع هذا الدليل دون الحاجة إلى إنشاء الجداول الخاصة بك وإدراج البيانات الواردة ضمنها. التثبيت المحلي سيتعيّن عليك أوّلًا تنزيل البيانات، ثم تحميلها في قاعدة البيانات. ملاحظة: تُنفّذ التعليمات التالية في صَدَفة bash المُعتادة، وليس ضمن psql. curl -L -O http://cl.ly/173L141n3402/download/example.dump createdb hsoubguide pg_restore --no-owner --dbname hsoubguide example.dump سنتحدث عن الأمر pg_restore في فقرة لاحقة، وهو مسؤول عن استرجاع قاعدة بيانات من ملف، أما الأمر createdb فهو يُنشئ قاعدة بيانات في Postgres اسمها hsoubguide. الاتصال بقاعدة البيانات بعد أن أنشأت قاعدة البيانات الخاصة بك hsoubguide فعليك الآن الدخول إلى psql والاتصال بقاعدة البيانات هذه، ويمكنك القيام بذلك بطريقتين: تحديد قاعدة البيانات عن عن طريق التوجيه dbname كما يلي: psql --dbname hsoubguide تحديد قاعدة البيانات من داخل صَدفة psql باستخدام الأمر \c كما يلي: postgres=# \c hsoubguide You are now connected to database "hsoubguide" as user "postgres". الخلاصة ثبَّتنا في هذا الفصل Postgres، وتعرفنا على كيفية الدخول إلى صدفة psql ثم أنشأنا قاعدة بيانات بدائية، وتعرفنا على كيفية تحديد صلاحية المستخدمين، وسنتعرف في الفصل التالي على بعض الأوامر التي يمكننا تنفيذها داخل صدفةpsql. اقرأ أيضًا المقال التالي: أساسيات استخدام صدفة psql المقال السابق: نظرة عامة على Postgres وتاريخها النسخة الكاملة لكتاب الدليل العملي إلى قواعد بيانات PostgreSQL فيديو - تثبيت وإعداد قاعدة بيانات PostgreSQL كيف تثبت PostgreSQL وتستخدمه على Ubuntu 14.04
-
يجدر بنا معرفة بعض المزايا الأساسية لقواعد بيانات Postgres في مقدمة هذا الدليل، وذلك كي يكون واضحًا للقارئ الفوائد التي سيجنيها من التعرف إليها، وتطوير قدراته في التعامل معها. إن أردت فهم أي موضوع فهمًا جيدًا، فيجب أن تملك معرفة تاريخية جيدة عنه خصوصًا بداية نشأته وسببها وآثار ذلك وحتى الحاضر؛ وبناءً على ذلك، سنطلع أولًا على تاريخ Postgres منذ بداية ظهورها وحتى يومنا هذا. لمحة تاريخية الولادة الأولى لقواعد بيانات Postgres في عام 1986 نشر Michael Stonebraker ورقة بحثية في جامعة بيركلي، كاليفورنيا بعنوان the design of Postgres ليعلن ولادة قواعد بيانات Postgres الأولى، ذكر في ورقته أن قاعدة البيانات هذه هي النسخة المحسنة المطورة من قواعد بيانات سابقة لها اسمها INGRES (أُنشئت عام 1975)، ومن هنا جاءت التسمية POST inGRES أي أنها لاحقة لقواعد INGRES، كما ذكر أن أهم أهداف إنشائها هو دعم تخزين أنواع معقدة، والسماح للمستخدمين بإنشاء امتدادات للغة، وغيره من الأهداف المتعلقة بالتخزين والمعالجة، وكانت في ولادتها هذه من أوائل أنظمة قواعد البيانات التي تتيح استخدام أنواع البيانات المتعددة، مع إمكانية شرح العلاقات بين الجداول بشكل كامل، إلا أنها كانت في ذلك الوقت لا تستخدم لغة الاستعلامات المُهيكلة SQL بل لغة مشابهة خاصة بها. نُشرت بعد ذلك في عام 1989 النسخة الأولى من اللغة لعدد قليل من المستخدمين، تبعتها النسخة 2 عام 1990 مع بعض التحسينات، والنسخة 3 في عام 1991 مع تحسينات في إدارة التخزين وتحسينات على محرك الاستعلام الأساسي، ولكن في عام 1993 بلغت كمية طلبات الدعم والتحسينات حدا تجاوز إمكانيات فريق التطوير في ذلك الوقت،فتم إيقاف المشروع في 30 حزيران 1994. الولادة الثانية قامت جامعة بيركلي بفتح مصدر Postgres مما سمح لجميع المستخدمين باستخدام الشيفرة البرمجية والتعديل عليها، فقام Andrew Yu و Jolly Chen المتخرجَين من جامعة بيركلي في عام 1994 بجعل Postgres تستخدم لغة الاستعلامات المُهيكلة SQL وتم إنشاء صدفة psql ونُشرت النسخة الجديدة Postgres95 عام 1995 بعد فترة تجريبية قصيرة، وبرخصة مفتوحة المصدر أيضًا. ظهور PostgreSQL و postgresql.org تم تغيير اسم نظام قواعد البيانات Postgres95 إلى PostgreSQL للدلالة على أنها تستخدم لغة SQL عام 1996 وظهر أخيرًا الموقع postgresql.org في ذلك العام لتظهر النسخة 6 من النظام عام 1997 ثم تبدأ مسيرة التطوير مفتوحة المصدر من خلال المطورين المتطوعين حول العالم تحت مسمّى (مجموعة تطوير Postgres العالمية). أهم خصائص قواعد بيانات Postgres تتصف قواعد بيانات Postgres بالخصائص التالية: أكثر قواعد البيانات تقدّمًا إن هذا هو شعار قواعد بيانات Postgres (وليس تقييمًا حقيقيًّا) ولكنها ظلّت ملتزمة به منذ نشأتها عام 1986، وذلك عن طريق التحسين المستمر وإضافة المزايا الجديدة وتلافي الأخطاء القديمة. الأسرع نموًّا حسب إحصائيات موقع db-engines حيث يعطي علامة score لكل نظام قاعدة بيانات حسب عدة عوامل، منها وتيرة الأسئلة التقنية على المواقع التقنية المشهورة مثل Stack Overflow وكذلك عدد عروض العمل المطلوبة على مواقع شهيرة مثل Indeed، وغيرها من العوامل التي تشير إلى ازدياد الاهتمام وعدد المستخدمين والمحترفين لهذه الأنظمة. ربما يمكنك أن ترى النمو المتسارع لقواعد بيانات PostgreSQL من الشكل التالي بوضوح: إن ميزة النمو في الأنظمة تعني أن تعلّمك اليوم لقواعد البيانات PostgreSQL هو استثمار صحيح للمستقبل سواء للعمل في وظيفة تطلب منك خبرة Postgres أو لكي تستخدمها في موقعك الخاص. نموذج تخزين البيانات فيها من النوع SQL نقصد في هذه الفقرة تثبيت فكرة كون Postgres من النوع SQL وليس NoSQL، وذلك يعني باختصار أن قواعد بيانات Postgres تخزّن البيانات ضمن جداول لها قوالب معدّة مسبقًا، ويمكنك الاطلاع على مقال شرح الفروقات بين قواعد بيانات SQL ونظيراتها NoSQL للمزيد من المعلومات عن هذا الموضوع. تسمح بأنواع بيانات غير مُهيكلة قام المبرمجون المشاركون في تطوير قواعد بيانات Postgres بإضافة النوع JSONB الذي يسمح بتخزين كائنات JSON ضمن الجداول، وبذلك تكون قد استحوذت أيضًا على بعض مزايا قواعد بيانات NoSQL رغم كونها قواعد بيانات مهيكلة. مفتوحة المصدر يمكن تحميل الشيفرة المصدرية لقواعد بيانات Postgres من المستودع الرسمي على موقع github وهي مكتوبة بلغة C، ولكونها مفتوحة المصدر فيمكن للمبرمج فهم آلية العمل الدقيقة لأي تفصيل يبحث عنه، كما يمكنه تحسينه وتطويره ونشره إن أراد ليكون جزءًا من نسخة مستقبلية من قواعد البيانات Postgres، أو ليكون رقعة Patch لنسخة حالية موجودة. قابلة للتوسيع ذكرنا قبل قليل أنه يمكن تعديل الشيفرة المصدرية لقواعد بيانات Postgres، ولكننا الآن نتحدث عن إمكانية كتابة امتدادات لها، دون المساس بالشيفرة المصدرية أو الحاجة إلى الاطلاع عليها، وهذا يعني أنه بإمكانك كتابة توابع جديدة خاصة بك وربطها بقاعدة البيانات لاستخدامها لاحقا. ذات توثيق تفصيلي يمكنك الاطلاع على توثيق قواعد بيانات Postgres الذي يشرح كافة التفاصيل مع تقديم أمثلة لكل منها ودليل تدريبي للمبتدئين كذلك، وهو يشمل كافة المواضيع المتعلقة بها بدءًا من أبسط عبارات SQL وانتهاءً بكيفية تمديد اللغة وكتابة شيفرات برمجية لتحسينها وتطويرها. متى تختار Postgres؟ للإجابة على هذا السؤال، من المهم التفريق بين المبتدئ والمتوسط، فبالنسبة للمبتدئ، فإن أهم ما يحتاجه للبدء هو سهولة التثبيت وسهولة التعلم ووجود المصادر العربية، وهذا من أهم ميزات Postgres بالنسبة للمبرمج العربي المبتدئ فأكاديمية حسوب تعتني بإغناء المحتوى العربي الخاص بها نظرًا لسرعة نموها وانتشارها المستمر. أما لمن تجاوز الأساسيات وبدأ يهتم بالخطوة التالية، فإنك الآن قادر على كتابة استعلامات SQL بمهارة، وترغب الآن بتحسين كفاءة الاستعلامات، أو زيادة موثوقية قاعدة البيانات لديك لمنع حدوث ضياع للبيانات أو لتسريع عمل قاعدة البيانات أو تخفيض حجم قاعدة البيانات في الذاكرة، وفي هذه الحالة فإن Postgres تتيح لك إدارة سهلة وفعالة للأداء، للذاكرة ولكتابة استعلامات أوضح وأكثر سرعة في التنفيذ، كما أن الأدوات المرفقة مع قاعدة بيانات Postgres سهلة التثبيت والاستخدام، ستكون بداية قوية لك في إدارة قواعد البيانات. في حال كنت متقدمًا في استخدام قواعد البيانات، فقد تضطر لإنشاء توابع خاصة بك، أو لتعديل أمور جوهرية في محرك قواعد البيانات نفسه لعمل تعديلات مخصصة لتطبيقك أو لمنتج مميز له مزايا خاصة، فهناك العديد من الشركات اعتمدت على Postgres لتطوير قواعد بياناتها الخاصة مثل Sun وRed Hat وAmazon وYahoo والقائمة تطول. نظرة عامة على نموذج الخادم/عميل في Postgres من المفيد قبل البدء التعرف على بنية نظام قواعد بيانات PostgreSQL وفهم كيفية ارتباط أجزاء النظام ببعضها. تستخدم PostgreSQL نموذج خادم/عميل (client/server) بحيث تتكون الجلسة من الجزئين التاليين: عملية الخادم، اسمها postgres تدير ملفات قاعدة البيانات، وتستقبل اتصالات التطبيقات من طرف العميل، وتنفّذ العمليات التي يطلبها العميل على قاعدة البيانات. تطبيق العميل الخاص بالمستخدم، (الواجهة الأمامية)، هو التطبيق الذي يريد القيام بعمليات على قاعدة البيانات، ويمكن أن يكون بأشكال عديدة: واجهة سطر أوامر psql، تطبيق رسومي، خادوم ويب أو أداة صيانة قواعد بيانات ما، بعض هذه الأدوات تأتي مرفقة مع نظام قواعد بيانات PostgreSQL كما يمكن أن تكون من تطوير المستخدمين. يتواصل البرنامج العميل مع الخادم عن طريق الشبكة بواسطة بروتوكول TCP/IP كما هو حال تطبيقات الخادم/عميل المعتادة، وهذا يعني أن العميل لا يمكنه الوصول إلى الملفات الموجودة على الجهاز الخاص بالخادم إذا كان كل منهما على جهاز مختلف. يمكن للخادم استقبال عدة اتصالات بآن واحد من العملاء، حيث تقوم العملية الأساسية للخادم postgres بعمل fork بحيث تتفرع لعدة عمليات كل منها يعالج أحد هذه الاتصالات لتبقى العملية الأساسية متحررة طوال الوقت من الطلبات وتنتظر استقبال الطلبات الجديدة، وعند انتهاء تنفيذ الطلب يتم تحرير العملية المرتبطة بها وإزالتها (طبعا يبقى كل ذلك غير مرئي للعميل). الخلاصة أخذت في هذا المقال لمحة تاريخية عن Postgres وتعرفت على خصائصها والمزايا التي تقدمها كما أجبنا عن سؤال مهم يُسأَل دومًا قبل استخدام أي شيء وهو متى نستخدم Postgres ثم ألقينا نظرة على نموذج الاتصال الذي تتبعه Postgres. حان الآن وقت بدء العمل مع Postgres. اقرأ أيضًا المقال التالي: تثبيت Postgres والتعرف على أساسيات إدارتها لقواعد البيانات النسخة الكاملة من كتاب: الدليل العملي إلى قواعد بيانات PostgreSQL
- 1 تعليق
-
- 1
-
يعد دوكر Docker أداة فعالة لأتمتة تثبيت تطبيقات لينكس داخل حاويات للبرامج، للاستفادة الكاملة من إمكاناته، يجب تشغيل كل مكون من مكونات التطبيق في حاويته الخاصة. وبالنسبة للتطبيقات المعقدة التي تحوي الكثير من المكونات، يمكن أن يصبح تنسيق جميع الحاويات لبدء التشغيل والاتصال وإيقاف التشغيل معًا أمرًا غير عملي. توصل مجتمع دوكر إلى حل شائع يسمى Fig، والذي يسمح باستخدام ملف YAML واحد لتنظيم جميع حاويات دوكر وتكويناتها. حيث أصبح هذا شائعًا جدًا لدرجة أن مجتمع دوكر قرر جعل Docker Compose يعتمد على مصدر Fig، والذي تم إلغاؤه الآن. يتيح Docker Compose للمستخدمين تنظيم عمليات حاويات دوكر، بما في ذلك بدء وإيقاف التشغيل وإعداد الربط بين الحاويات ووحدات التخزين. في هذا الدليل، ستقوم بتثبيت أحدث إصدار من Docker Compose لمساعدتك في إدارة التطبيقات متعددة الحاويات على خادم دبيان 10. المتطلبات الأساسية لكي تتمكن من المتابعة مع هذه المقالة، ستحتاج إلى: خادم دبيان 10 ومستخدم عادي (non-root) بامتيازات sudo. حيث يشرح هذا الإعداد الأولي للخادم مع دليل دبيان 10 التعليمي كيفية إعداد ذلك. دوكر مثبّت بإرشادات من الخطوة الأولى والخطوة الثانية لكيفية تثبيت واستخدام دوكر على دبيان 10 ملاحظة: على الرغم من أن المتطلبات المسبقة تعطي تعليمات لتثبيت دوكر على دبيان 10، فإن أوامر docker في هذه المقالة يمكن أن تعمل على أنظمة تشغيل أخرى طالما أن دوكر مثبت. الخطوة الأولى - تثبيت Docker Compose على الرغم من أنه يمكن تثبيت Docker Compose من مستودعات Debian الرسمية، إلا أن هناك عدة إصدارات ثانوية ظهرت بعد الإصدار الأخير، لذلك في هذا الدليل ستقوم بتثبيته من مستودع GitHub الخاص بــدوكر. يختلف الأمر التالي قليلاً عن الأمر الذي ستجده في صفحة الإصدارات. فباستخدام الراية -o لتحديد ملف الإخراج أولاً بدلاً من إعادة توجيه الإخراج، هذه الصيغة تتجنب حدوث خطأ "رفض الإذن" الذي يحدث عند استخدام sudo. تحقق من الإصدار الحالي، وإذا لزم الأمر قم بتحديثه في الأمر التالي: $ sudo curl -L https://github.com/docker/compose/releases/download/1.25.3/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose بعد ذلك سنقوم بتعيين الأذونات: $ sudo chmod +x /usr/local/bin/docker-compose ثم سنتحقق من نجاح التثبيت عن طريق التحقق من الإصدار: $ docker-compose --version سيؤدي ذلك إلى طباعة النسخة التي قمنا بتثبيتها: $ docker-compose version 1.25.3, build d4d1b42b الخطوة الثانية - تشغيل حاوية مع Docker Compose يتضمن سجل دوكر العام Docker Hub، صورة Hello World للتوضيح والاختبار. حيث توضح الحد الأدنى من التكوين المطلوب لتشغيل حاوية باستخدام Docker Compose: ملف YAML الذي يستدعي صورة واحدة. سنقوم بإنشاء أصغر ملف إعدادت ممكن لتشغيل حاوية hello-world الخاصة بنا. أولاً ، قم بإنشاء دليل لملف YAML وقم بالتحويل إليه: $ mkdir hello-world cd hello-world ثم قم بإنشاء ملف YAML: $ nano docker-compose.yml ضع المحتويات التالية في الملف docker-compose.yml واحفظ الملف واخرج من محرر النصوص: $ my-test: image: hello-world يتم استخدام السطر الأول في ملف YAML كجزء من اسم الحاوية. ويحدد السطر الثاني الصورة التي سيتم استخدامها لإنشاء الحاوية. فعندما نقوم بتنفيذ الأمر docker-compose up، سيبحث عن صورة محلية بالاسم الذي حددناه، ألا وهو hello-world. بعد ذلك سنقوم بحفظ الملف والخروج منه. يمكنك النظر يدويًا إلى الصور الموجودة على نظامنا باستخدام الأمر docker images: $docker images عندما لا تكون هناك صور محلية على الإطلاق، يتم عرض عناوين الأعمدة فقط: REPOSITORY TAG IMAGE ID CREATED SIZE الآن ، أثناء وجودك في مجلد ~/hello-world، نفّذ الأمر التالي: $docker-compose up في المرة الأولى التي تقوم فيها بتنفيذ هذا الأمر، إذا لم تكن هناك صورة محلية باسم hello-world، فسوف يسحبها Docker Compose من مستودع Docker Hub العام: Pulling my-test (hello-world:)... latest: Pulling from library/hello-world 9db2ca6ccae0: Pull complete Digest: sha256:4b8ff392a12ed9ea17784bd3c9a8b1fa3299cac44aca35a85c90c5e3c7afacdc Status: Downloaded newer image for hello-world:latest . . . بعد سحب الصورة، يقوم docker-compose بإنشاء حاوية تقوم بإرفاق وتشغيل برنامج hello، والذي بدوره يؤكد أن التثبيت يعمل: . . . Creating helloworld_my-test_1... Attaching to helloworld_my-test_1 my-test_1 | my-test_1 | Hello from Docker. my-test_1 | This message shows that your installation appears to be working correctly. my-test_1 | . . . ثم يطبع شرحًا لما فعله: To generate this message, Docker took the following steps: my-test_1 | 1. The Docker client contacted the Docker daemon. my-test_1 | 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. my-test_1 | (amd64) my-test_1 | 3. The Docker daemon created a new container from that image which runs the my-test_1 | executable that produces the output you are currently reading. my-test_1 | 4. The Docker daemon streamed that output to the Docker client, which sent it my-test_1 | to your terminal. تعمل حاويات دوكر فقط عندما يكون الأمر نشطًا، لذلك بمجرد الانتهاء من تشغيل [hello]، تتوقف الحاوية. وبالتالي عندما ننظر إلى العمليات النشطة ستظهر عناوين الأعمدة، لكن لن يتم إدراج حاوية hello-world كونها لا تعمل: $docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES يمكنك الاطلاع على معلومات الحاوية، وهذا ما ستحتاجه في الخطوة التالية باستخدام الراية -a. يعرض هذا الأمر جميع الحاويات وليس فقط الحاويات النشطة: $docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 06069fd5ca23 hello-world "/hello" 35 minutes ago Exited (0) 35 minutes ago hello-world_my-test_1 و يعرض هذا المعلومات التي ستحتاج إليها لإزالة الحاوية عند الانتهاء منها. الخطوة الثالثة - إزالة الصورة (اختياري) لتجنب استخدام مساحة قرص غير ضرورية، سنقوم بإزالة الصورة المحلية. وللقيام بذلك، سنحتاج إلى حذف جميع الحاويات التي تشير إلى الصورة باستخدام الأمر docker rm، متبوعًا إما بمعرف الحاوية CONTAINER ID أو الاسم NAME. في المثال التالي، نستخدم معرف الحاوية CONTAINER ID من الأمر docker ps -a الذي قمنا بتشغيله للتو. وتأكد من استبدال معرف الحاوية: $docker rm 06069fd5ca23 بمجرد إزالة جميع الحاويات التي تشير إلى الصورة، يمكننا إزالة الصورة: $docker rm 06069fd5ca23 الخلاصة لقد قمت بتثبيت Docker Compose على دبيان 10، واختبرت التثبيت عن طريق تشغيل مثال Hello World، وأزلت صورة الاختبار والحاوية. بينما أكد مثال Hello World التثبيت، فإن هذا التكوين الأساسي لا يُظهر إحدى الفوائد الرئيسية لـ Docker Compose – وهي القدرة على إحضار مجموعة من حاويات دوكر للأعلى وللأسفل في نفس الوقت. ترجمة -وبتصرف- للمقال How To Install Docker Compose on Debian 10 لصاحبيه Brian Hogan و Kathleen Juell
-
- دبيان
- docker compose
-
(و 1 أكثر)
موسوم في:
-
تعد Let’s Encrypt خدمة تقدم شهادات SSL مجانية من خلال واجهة برمجة تطبيقات تلقائية (automated API). أما عميل Let’s Encrypt الأكثر شيوعًا فهو عميل Certbot الخاص بـ EFF. يقدم Certbot مجموعة متنوعة من الطرق للتحقق من صحة المجال الخاص بك، وجلب الشهادات، وتكوين Apache و Nginx تلقائيًا. في هذا المقال، سنناقش الوضع المستقل لـ Certbot وكيفية استخدامه لتأمين أنواع أخرى من الخدمات، مثل خادم البريد أو وسيط الرسائل مثل RabbitMQ. لن نناقش تفاصيل تكوين طبقة المنافذ الآمنة (SSL)، لكن عند الانتهاء ستحصل على شهادة صالحة يتم تجديدها تلقائيًا. وإنك ستكون قادرًا على أتمتة إعادة تحميل الخدمة للحصول على الشهادة المُجددة. المتطلبات قبل مباشرتك بقراءة هذا الدليل, ستحتاج إلى: خادم دبيان 10، مستخدم غير جذري بإمتيازات sudo، وجدار حماية أساسي. اسم نطاق يشير إلى الخادم الخاص بك. يجب عدم استخدام المنفذ 80 أو 443 على الخادم الخاص بك. وإذا كانت الخدمة التي تحاول تأمينها موجودة على جهاز مزود بخادم ويب يشْغل كلا هذين المنفذين، فسيلزمك استخدام وضع مختلف مثل وضع webroot_ الخاص بـ Certbot أو وضع التحدي المعتمد على DNS. الخطوة الأولى - تثبيت Certbot يحوي دبيان 10 على عميل Certbot في المستودع الافتراضي الخاص به، حيث يجب أن يكون محدثًا بما يكفي للاستخدام الأساسي. وإذا كنت بحاجة إلى القيام بتحديات مرتبطة بـ DNS أو استخدام ميزات Certbot الأحدث الأخرى، عندها يتوجب عليك التثبيت من buster-backports repo كما هو موضح في توثيق Certbot الرسمي. قم بتحديث قائمة الحزمة: $ sudo apt update استخدم apt لتثبيت حزمة certbot: $ sudo apt install certbot يمكنك اختبار التثبيت عن طريق مطالبة certbot بإخراج رقم إصداره: $ certbot --version certbot 0.31.0 بعد قيامك بتثبيت Certbot، قم بتشغيله للحصول على شهادتك. الخطوة الثانية - تشغيل Certbot يحتاج Certbot للرد على اختبار تشفير صادر عن واجهة برمجة التطبيقات (API) الخاص بـ Let’s Encrypt لإثبات أننا نتحكم في نطاقنا. حيث يستخدم المنافذ 80 (HTTP) أو 443 (HTTPS) لتحقيق ذلك. قم بفتح المنفذ المناسب في جدار الحماية: $sudo ufw allow 80 استبدل 443 أعلاه إذا كان هذا هو المنفذ الذي تستخدمه. حيث سيخرج ufw تأكيدًا بأنه قد تم إضافة ادارتك: Rule added Rule added (v6) يمكننا الآن تشغيل Certbot للحصول على شهادتنا. حيث سنستخدم --standalone لإخبار Certbot بالتعامل مع التحدي باستخدام خادم الويب المدمج الخاص به. ويرشد خيار --preferred-challenges إلى Certbot لاستخدام المنفذ 80 أو المنفذ 443. فإذا كنت تستخدم المنفذ 80، فستستخدم خيار --preferred-challenges http. وأما بالنسبة إلى المنفذ 443، استخدم --preferred-challenges tls-sni. والآن سنستخدم الراية -d لتحديد النطاق الذي نطلب شهادة له. حيث يمكنك إضافة رايات -d متعددة لتغطية نطاقات متعددة في شهادة واحدة. للتوضيح سنستخدم --preferred-challenges http، لكن يتحتم عليك استخدام الخيار المناسب حسب حالة الاستخدام الخاصة بك. والآن قم بتشغيل الأمر التالي مع الخيارات المفضلة لديك للحصول على شهادتك: $sudo certbot certonly --standalone --preferred-challenges http -d your_domain عند تشغيل الأمر سيُطلب منك إدخال عنوان بريد إلكتروني والموافقة على شروط الخدمة. وبعد قيامك بذلك من المفترض أن ترى رسالة تخبرك عن مكان تخزين الشهادات موضحاً فيها بأن العملية كانت ناجحة: IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/your_domain/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/your_domain/privkey.pem Your cert will expire on 2019-08-28. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - Your account credentials have been saved in your Certbot configuration directory at /etc/letsencrypt. You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained by Certbot so making regular backups of this folder is ideal. - If you like Certbot, please consider supporting our work by: Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le ها قد حصلنا على شهاداتنا. دعنا الآن نلقي نظرة على ما قمنا بتنزيله وكيفية استخدام الملفات مع برنامجنا. الخطوة الثالثة - إعدادات التطبيق لن نتطرق في هذه المقالة إلى إعداد SSL لتطبيقك، حيث أن لكل تطبيق متطلبات وخيارات تهيئة مختلفة، لكن دعنا نلقي نظرة على ما تم تنزيله من قِبل Certbot. استخدم ls لعرض المجلد الذي يحوي مفاتيحك وشهاداتك: $sudo ls /etc/letsencrypt/live/your_domain سترى الناتج التالي: cert.pem chain.pem fullchain.pem privkey.pem README يحتوي ملف README في هذا المجلد على مزيد من المعلومات حول كل ملف من هذه الملفات. غالبًا ما ستحتاج فقط إلى ملفين من هذه الملفات: privkey.pem: هذا هو المفتاح الخاص بالشهادة. ويجب الإبقاء على هذا المفتاح بشكل آمن وسري، فهذا هو السبب في أن معظم ما في مجلد /etc/letsencrypt لديه أذونات مقيدة للغاية حيث يمكن الوصول إليه فقط من قبل المستخدم الجذر root. ستشير معظم إعدادات البرامج إلى هذا الملف على أنه ملف ssl-certificate-key أو ssl-certificate-key-file. fullchain.pem: هذه هي شهادتنا مرفقة بجميع الشهادات الوسيطة. ستستخدم معظم البرامج هذا الملف للحصول على الشهادة الفعلية، وستشير إليه في إعداداتها باسم مثل ssl-certificate. لمزيد من المعلومات حول الملفات الأخرى الموجودة، راجع قسم Where are my certificate? من توثيق Certbot. ستحتاج بعض البرامج إلى شهاداتها في تنسيقات أو مواقع أخرى، أو بأذونات مستخدم أخرى. من الأفضل ترك كل شيء في مجلد letsencrypt وعدم تغيير أي أذونات هناك (سيتم استبدال الأذونات عند التجديد على أي حال)، ولكن قد لا يكون هذا الخيار مناسبًا، ففي هذه الحالة ستحتاج إلى كتابة نص برمجي لنقل الملفات وتغيير الأذونات حسب الحاجة. حيث يجب تشغيل هذا النص البرمجي عند تجديد Certbot للشهادات والتي سنتحدث عنها بعد ذلك. الخطوة الرابعة - التعامل مع التحديثات التلقائية لـ Certbot تعد شهادات Let's Encrypt صالحة لمدة تسعين يومًا فقط. وهذا لدفع المستخدمين إلى أتمتة عملية تجديد الشهادة. حيث تقوم بذلك حزمة certbot التي قمنا بتثبيتها عن طريق إضافة سطر برمجي إلى /etc/cron.d. ويعمل هذا السطر البرمجي مرتين يوميًا ليقوم بتحديث أي شهادة ستنتهي صلاحيتها خلال ثلاثين يومًا. مع تجديد شهاداتنا تلقائيًا ما زلنا بحاجة إلى طريقة لتشغيل مهام أخرى بعد التجديد. حيث سنحتاج على الأقل إلى إعادة تشغيل خادمنا أو إعادة تحميله لاستلام الشهادات الجديدة، وكما ذكرنا في الخطوة الثالثة قد نحتاج إلى التلاعب في ملفات الشهادات بطريقة ما لجعلها تعمل مع البرنامج الذي نستخدمه. وهذا هو الغرض من خيار renew_hook. لإضافة renew_hook، نحتاج إلى تحديث ملف تكوين تجديد الخاص بـ Certbot. حيث يتذكر Certbot جميع التفاصيل عن كيفية جلبك الشهادة لأول مرة، وسيتم تشغيله بنفس الخيارات عند التجديد. نحتاج فقط إلى إضافة الخطاف (hook) الخاص بنا. قم بفتح ملف التكوين باستخدام أي محرر تفضله: $sudo nano /etc/letsencrypt/renewal/your_domain.conf سيتم فتح ملف نصّي /etc/letsencrypt/renewal/your_domain.conf مع بعض خيارات الإعدادات. بعدها قم بإضافة خطافك (hook) على السطر الأخير. نستخدم في هذه الحالة مثالًا يعيد تحميل خدمة rabbitmq: $renew_hook = systemctl reload rabbitmq قم بتحديث الأمر أعلاه بإضافة كل ما تحتاج تشغيله لإعادة تحميل الخادم أو قم بتشغيل سكربت مخصص للقيام بذلك. عادةً ما نستخدم في دبيان خدمة systemctl لإعادة تحميل الخدمة. احفظ الملف وأغلقه، ثم قم بتشغيل Certbot بوضعية التشغيل الجاف (dry run) للتأكد من أن الصيغة صحيحة: $sudo certbot renew --dry-run إذا لم ترَ أي أخطاء فأنت جاهز تمامًا. فقد تم تعيين Certbot للتجديد عند الضرورة وتشغيل أي أوامر مطلوبة للحصول على الخدمة باستخدام الملفات الجديدة. الملخص في هذا المقال، ثبّتنا Certbot Let’s Encrypt Client، وتنزيل شهادة SSL باستخدام الوضع المستقل، كما وقمنا بتمكين عمليات التجديد التلقائي بخطافات التجديد (renew hooks). فمن المفترض أن يمنحك هذا بداية جيدة لاستخدام شهادات Let’s Encrypt مع خدمات مغايرة خادم الويب الإعتيادي. لمزيد من المعلومات يرجى الإطلاع على توثيق Certbot ترجمة -وبتصرف- للمقال How To Use Certbot Standalone Mode to Retrieve Let's Encrypt SSL Certificates on Debian 10 لأصحابه Brian Boucheron و Kathleen Juell و Hanif Jetha
-
يعد دوكر تطبيقًا يبسّط إدارة عمليات التطبيق في الحاويات. حيث تتيح لك الحاويات تشغيل تطبيقاتك في عمليات معزولة الموارد فيما بينها. فهي من ناحية المبدأ تشبه Virtual Machines، لكن الحاويات أكثر قابلية للنقل وأكثر ملاءمة للموارد وأكثر اعتمادًا على نظام تشغيل المضيف. وللحصول على مقدمة تفصيلية للمكونات المختلفة لحاوية دوكر، راجع المقال نظام Docker Ecosystem: مقدمة إلى المكونات المشتركة.. في هذا الدليل، ستقوم بتثبيت واستخدام Docker Community Edition (CE) على دبيان 10. حيث ستقوم بتثبيت دوكر بذاته والتعامل مع الحاويات والنِسخ، ورفع نسخة إلى مستودع دوكر. المتطلبات لمتابعة هذا الدليل، ستحتاج إلى ما يلي: خادم دبيان 10 مع مستخدم عادي بصلاحيات sudo بالإضافة لجدار الحماية. حساب على Docker Hub في حال كنت ترغب في إنشاء نسخك الخاصة ورفعها إلى Docker Hub، كما هو موضح في الخطوتين 7 و 8. الخطوة 1- تثبيت دوكر قد لا تكون حزمة تثبيت دوكر المتوفرة في مستودع دبيان الرسمي حزمًة ذو أحدث إصدار. لذا لضمان حصولنا على أحدث إصدار، سنقوم بتثبيت دوكر من مستودع دوكر الرسمي وذلك بإضافة مصدر حزمة جديد مع مفتاح GPG من دوكر للتأكد من صلاحية التنزيلات، ومن ثم تثبيت الحزمة. أولاً، قم بتحديث قائمة الحزم الموجودة لديك: $ sudo apt update بعد ذلك قم بتثبيت بعض الحزم الأساسية التي تتيح لـ apt استخدام الحزم عبر HTTPS: $ sudo apt install apt-transport-https ca-certificates curl gnupg2 software-properties-common ثم أضف مفتاح GPG لمستودع دوكر الرسمي إلى نظامك: $ curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add أضف مستودع دوكر إلى مصادر APT: $ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable" بعد ذلك قم بتحديث قاعدة بيانات الحزم بحزم دوكر من المستودع المضاف حديثًا: $ sudo apt update تأكد من أنك على وشك التثبيت من مستودع دوكر بدلاً من مستودع دبيان الافتراضي: $ apt-cache policy docker-ce سترى مخرجات مثل هذا على الرغم من أن رقم إصدار دوكر قد يكون مختلفًا: $ docker-ce: Installed: (none) Candidate: 5:18.09.7~3-0~debian-buster Version table: 5:18.09.7~3-0~debian-buster 500 500 https://download.docker.com/linux/debian buster/stable amd64 Packages لاحظ أن docker-ce غير مثبت، لكن المرشح للتثبيت هو من مستودع دوكر لـ دبيان 10 (buster) أخيرًا، قم بتثبيت دوكر: $ sudo apt install docker-ce تم الآن تثبيت دوكر وبدأ البرنامج الخفي (daemon، العفريت كما يطلق عليه مستخدمو لينكس)، و تم تمكين العملية للبدء في التشغيل. تحقق من أنه يعمل بتنفيذ الأمر التالي: $ sudo systemctl status docker سيكون الناتج مشابهًا لما يلي، مما يدل على أن الخدمة نشطة وتعمل: docker.service - Docker Application Container Engine Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled) Active: active (running) since Mon 2019-07-08 15:11:19 UTC; 58s ago Docs: https://docs.docker.com Main PID: 5709 (dockerd) Tasks: 8 Memory: 31.6M CGroup: /system.slice/docker.service └─5709 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock لا يمنحك تثبيت دوكر خدمة Docker daemon وحسب، بل يمنحك أيضًا خط أوامر docker، أو عميل دوكر. سنكتشف كيفية استخدام أمر docker لاحقًا في هذا الدليل التعليمي. الخطوة 2- تنفيذ أمر دوكر دون sudo (اختياري) افتراضيًا، لا يمكن تشغيل أمر docker إلا للمستخدم الجذر أو بواسطة مستخدم في مجموعة docker، والتي يتم إنشاؤها تلقائيًا أثناء عملية تثبيت دوكر. وإذا حاولت تشغيل الأمر docker دون أن تضيفه إلى sudo أو دون أن تكون في مجموعة docker، فستحصل على ناتج مثل هذا: docker: Cannot connect to the Docker daemon. Is the docker daemon running on this host?. See 'docker run –help'. إذا كنت تريد تجنب كتابة sudo كلما قمت بتشغيل أمر docker، أضف اسم المستخدم الخاص بك إلى مجموعة docker: $ sudo usermod -aG docker ${USER} لتطبيق عضوية المجموعة الجديدة، قم بتسجيل الخروج من الخادم والعودة مرة أخرى أو اكتب ما يلي: $ su - ${USER} عندها سيُطلب منك إدخال كلمة مرور المستخدم للمتابعة. تأكد من إضافة المستخدم الآن إلى مجموعة docker عن طريق كتابة: $ id -nG sammy sudo docker وإذا كنت بحاجة إلى إضافة مستخدم آخر إلى مجموعة docker التي لم تسجل الدخول بها من قبل، فيمكنك الإعلان عن اسم المستخدم هذا باستخدام: $ sudo usermod -aG docker username تفترض بقية المقالة هذه أنك تقوم بتشغيل أمر docker كمستخدم في مجموعة docker. في حال اخترت عدم القيام بذلك، يرجى إلحاق الأوامر بـsudo. دعنا نستكشف أمر docker تاليًا. الخطوة 3- استخدام أمر docker يتكون استخدامdocker من تمرير سلسلة من الخيارات والأوامر متبوعة بالوسطاء (arguments). حيث تأخذ الصيغة هذا الشكل: $ docker [option] [command] [arguments] لعرض كافة الأوامر الفرعية المتاحة، اكتب: $ docker فبالنسبة لـ دوكر 18، تتضمن القائمة الكاملة للأوامر الفرعية المتاحة ما يلي: Attach إرفاق تدفقات الإدخال والإخراج والخطأ القياسية المحلية إلى حاوية قيد التشغيل Build إنشاء نسخة من ملف Dockerfile Commit إنشاء نسخة جديدة من تغييرات الحاوية Cb نسخ الملفات / المجلدات بين الحاوية ونظام الملفات المحلي Create إنشاء حاوية جديدة Diff فحص التغييرات بالملفات أو المجلدات على نظام ملفات الحاوية Events معرفة الأحداث مباشرًة من الخادم Exec تشغيل أمر في حاوية قيد التشغيل Export استخراج نظام ملفات الحاوية كإنشاء محفوظة (tar archive) History إظهار تاريخ النسخة Images عرض قائمة النسخ Import استيراد المحتويات من tarball لإنشاء نسخة نظام ملفات Info عرض المعلومات على مستوى النظام Inspect إظهار معلومات منخفضة المستوى عن كائنات دوكر Kill إيقاف حاوية واحدة قيد التشغيل أو أكثر Load تحميل نسخة من أرشيف tar أو STDIN Login تسجيل الدخول إلى سجل دوكر Logout تسجيل الخروج من سجل دوكر Logs عرض سجلات الحاوية Pause إيقاف جميع العمليات مؤقتًا داخل حاوية واحدة أو أكثر Port سرد تعيينات المنافذ (port mappings) أو تعيين معين للحاوية Ps قائمة الحاويات Pull سحب نسخة أو وحدة تخزين (repository) من سجل Push رفع نسخة أو وحدة تخزين إلى السجل Rename إعادة تسمية الحاوية Restart إعادة تشغيل حاوية واحدة أو أكثر Rm إزالة حاوية واحدة أو أكثر Rmi إزالة نسخة واحدة أو أكثر Run تشغيل أمر في حاوية جديدة Save حفظ نسخة أو أكثر في أرشيف tar (يتم بثها إلى STDOUT افتراضيًا) Search بحث في Docker Hub عن النسخ Start بدء تشغيل حاوية واحدة أو أكثر Stats عرض بث مباشر لإحصائيات استخدام موارد الحاوية Stop إيقاف تشغيل حاوية واحدة أو أكثر Tag إنشاء علامة TARGET_IMAGE تشير إلى SOURCE_IMAGE Top عرض العمليات الجارية للحاوية Unpause إلغاء الإيقاف المؤقت لجميع العمليات داخل حاوية واحدة أو أكثر Update تحديث التكوين لحاوية واحدة أو أكثر Version إظهار معلومات إصدار دوكر Wait القيام بالحظر حتى تتوقف حاوية واحدة أو أكثر، ثم اطبع رموز الخروج الخاصة بها لعرض الخيارات المتاحة لأمر معين، اكتب: $ docker docker-subcommand –help أما لعرض معلومات على مستوى النظام حول دوكر، يمكنك استخدام: $ docker info دعنا نستكشف بعضًا من هذه الأوامر. حيث سنبدأ بالعمل مع النسخ. الخطوة 4 – التعامل مع نسخ دوكر يتم إنشاء حاويات دوكر من نسخ دوكر عندها يقوم تلقائيًّا بسحب هذه النسخات من Docker Hub، وهو سجل تديره الشركة المسؤولة عن مشروع دوكر. حيث يمكن لأي شخص إنزال نسخ دوكر على Docker Hub، لذا فإن معظم التطبيقات وتوزيعات Linux التي ستحتاج إليها ستتم استضافتها هناك. للتحقق مما إذا كان يمكنك الوصول إلى النسخ وتنزيلها من Docker Hub، اكتب: $ docker run hello-world سيشير الإخراج إلى أن دوكر يعمل بشكل صحيح: Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world 1b930d010525: Pull complete Digest: sha256:41a65640635299bab090f783209c1e3a3f11934cf7756b09cb2f1e02147c6ed8 Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. … بدايةً لم يتمكن دوكر من العثور على نسخة hello-world محليًا، لذلك تم تنزيل النسخة من وحدة التخزين الافتراضية Docker Hub. فبمجرد تنزيل النسخة، يقوم دوكر بإنشاء حاوية من النسخة ويتم تنفيذ التطبيق ضمن الحاوية عارضاً الرسالة. يمكنك البحث عن النسخات المتاحة على Docker Hub باستخدام أمر docker مع الأمر الفرعي search. على سبيل المثال، للبحث عن نسخة Ubuntu، اكتب: $ docker search ubuntu سيقوم هذا البرنامج بالبحث ضمن Docker Hub ويعرض قائمة بجميع النسخات التي يتطابق اسمها مع سلسلة البحث. وفي هذه الحالة، سيكون الناتج مشابهًا لما يلي: NAME DESCRIPTION STARS OFFICIAL AUTOMATED ubuntu Ubuntu is a Debian-based Linux operating sys… 9704 [OK] dorowu/ubuntu-desktop-lxde-vnc Docker image to provide HTML5 VNC interface … 319 [OK] rastasheep/ubuntu-sshd Dockerized SSH service, built on top of offi… 224 [OK] consol/ubuntu-xfce-vnc Ubuntu container with "headless" VNC session… 183 [OK] ubuntu-upstart Upstart is an event-based replacement for th… 99 [OK] ansible/ubuntu14.04-ansible Ubuntu 14.04 LTS with ansible 97 [OK] neurodebian NeuroDebian provides neuroscience research s… 57 [OK] 1and1internet/ubuntu-16-nginx-php-phpmyadmin-mysql-5 ubuntu-16-nginx-php-phpmyadmin-mysql-5 50 [OK] ubuntu … في العمود OFFICIAL، تشير OK إلى نسخة تم إنشاؤها ودعمها من قبل الشركة المسؤولة عن المشروع. فبمجرد تحديد النسخة التي ترغب باستخدامها، يمكنك تنزيلها على الحاسوب باستخدام الأمر الفرعي pull. قم بتنفيذ الأمر التالي لتنزيل نسخة ubuntu الرسمية على الحاسب: $ docker pull ubuntu سترى الناتج التالي: Using default tag: latest latest: Pulling from library/ubuntu 5b7339215d1d: Pull complete 14ca88e9f672: Pull complete a31c3b1caad4: Pull complete b054a26005b7: Pull complete Digest: sha256:9b1702dcfe32c873a770a32cfd306dd7fc1c4fd134adfb783db68defc8894b3c Status: Downloaded newer image for ubuntu:latest يمكنك بعد ذلك تشغيل حاوية باستخدام النسخة التي تم تنزيلها بالأمر run. وكما رأيت في مثال hello-world، في حال لم يتم تنزيل صورة عند تنفيذ docker باستخدام الأمر الفرعي run، فسيقوم عميل دوكر بتنزيل النسخة أولاً ثم تشغيل حاوية باستخدامها. لرؤية النسخات التي تم تنزيلها على الحاسوب، اكتب: $ docker images ستبدو المخرجات مشابهة لما يلي: REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu latest 4c108a37151f 2 weeks ago 64.2MB hello-world latest fce289e99eb9 6 months ago 1.84kB كما سترى لاحقًا في هذا الدليل، يمكن تعديل النسخ التي تستخدمها لتشغيل الحاويات واستخدامها لإنشاء نسخ اخرى جديدة، والتي قد يتم رفعها بعد ذلك (يعني يتم عمل push لها حسب المصطلح التقني) إلى Docker Hub أو سجلات دوكر الأخرى. سنرى في الخطوة التالية كيفية تشغيل الحاويات بمزيد من التفاصيل. الخطوة 5 - تشغيل حاوية دوكر إن حاوية hello-world التي قمت بتشغيلها في الخطوة السابقة هي مثال على حاوية يتم تشغيلها والخروج منها بعد بث رسالة اختبار. فيمكن للحاويات ان تكون تفاعلية و أكثر فائدة من ذلك. فهي تشبه الآلات الافتراضية، لكن أكثر ملاءمة للموارد. كمثال، لنقم بتشغيل حاوية باستخدام أحدث نسخة لـ Ubuntu. حيث يمنحك الجمع بين المبدلين -i و -t إمكانية الوصول التفاعلي إلى القشرة في الحاوية (interactive shell access): $ docker run -it ubuntu يجب تغيير موجه الأوامر ليعكس حقيقة أنك تعمل الآن داخل الحاوية ويجب أن يأخذ هذا النموذج: root@d9b100f2f636:/# لاحظ معرف الحاوية في موجه الأوامر. نظرًا الى هذا المثال، يكون المعرف d9b100f2f636. فستحتاج إلى معرّف الحاوية هذا لاحقًا لتحديد الحاوية عندما تريد إزالتها. الآن يمكنك تشغيل أي أمر داخل الحاوية. فعلى سبيل المثال، فلنقم بتحديث قاعدة بيانات الحزمة داخل الحاوية. لست بحاجة إلى إضافة أي أمر باستخدام sudo، لأنك تعمل داخل الحاوية كمستخدم جذر (root): root@d9b100f2f636:/# apt update ثم قم بتثبيت أي تطبيق فيه. دعنا نثبت Node.js: root@d9b100f2f636:/# apt install nodejs يؤدي هذا إلى تثبيت Node.js في الحاوية من مستودع Ubuntu الرسمي. وعند انتهاء التثبيت، تحقق من تثبيت Node.js: root@d9b100f2f636:/# node -v سترى رقم الإصدار المعروض في جهازك الطرفي: v8.10.0 أي تغييرات تجريها داخل الحاوية تنطبق فقط على الحاوية نفسها. للخروج من الحاوية، اكتب exit في واجهة الأوامر. دعنا نلقي نظرة على إدارة الحاويات على نظامنا بعد ذلك. الخطوة 6- إدارة حاويات دوكر بعد استخدام دوكر لفترة من الوقت، سيكون لديك العديد من الحاويات النشطة (قيد التشغيل) وغير النشطة على الحاسوب الخاص بك. ولعرض الحاويات النشطة، استخدم: $ docker ps سترى ناتج مشابه لما يلي: CONTAINER ID IMAGE COMMAND CREATED في هذا الدليل التعليمي، بدأت حاويتين؛ واحدة من نسخة hello-world والأخرى من نسخة ubuntu. بحيث لم يعد كلا الحاويتين قيد التشغيل، لكنهما لا تزالان موجودتان على نظامك. لعرض جميع الحاويات - النشطة وغير النشطة، قم بتشغيل docker ps باستخدام المبدّل -a: $ docker ps -a سترى مخرجات مشابهة لهذا: CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d42d0bbfbd35 ubuntu "/bin/bash" About a minute ago Exited (0) 20 seconds ago friendly_volhard 0740844d024c hello-world "/hello" 3 minutes ago Exited (0) 3 minutes ago elegant_neumann لعرض أحدث حاوية قمت بإنشائها، قم بتشغيل docker ps باستخدام -l: $ docker ps -l CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d42d0bbfbd35 ubuntu "/bin/bash" About a minute ago Exited (0) 34 seconds ago friendly_volhard لبدء تشغيل حاوية ما، استخدم docker start، متبوعًا بمعرف الحاوية أو اسم الحاوية. لنبدأ الحاوية القائمة على Ubuntu بمعرف d9b100f2f636: $ docker start d42d0bbfbd35 عندها ستبدأ الحاوية، ويمكنك استخدام docker ps لمعرفة حالتها: CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d42d0bbfbd35 ubuntu "/bin/bash" About a minute ago Up 8 seconds friendly_volhard لإيقاف حاوية جارية، استخدم docker stop، متبوعًا بمعرف الحاوية أو الاسم. وهذه المرة سنستخدم الاسم الذي خصصه دوكر للحاوية، الا وهو friendly_volhard: $ docker stop friendly_volhard بمجرد أن تقرر أنك لم تعد بحاجة إلى الحاوية بعد الآن، قم بإزالتها باستخدام الأمر docker rm، مرة أخرى باستخدام إما معرف الحاوية أو إسمها. يمكنك استخدام الأمر docker ps -a للعثور على معرف الحاوية أو اسم الحاوية المرتبط بصورة hello-world وإزالتها. $ docker rm elegant_neumann يمكنك بدء حاوية جديدة وتسميتها باستخدام المفتاح--name. ويمكنك أيضًا استخدام المبدل --rm لإنشاء حاوية تزيل نفسها عندما تتوقف. راجع الأمر help docker run للحصول على مزيد من المعلومات حول هذه الخيارات وغيرها. يمكن تحويل الحاويات إلى صور يمكنك استخدامها لبناء حاويات جديدة. دعونا نلقي نظرة على كيفية عمل ذلك. الخطوة 7- إحداث التغييرات في حاوية لنسخة دوكر عند بدء تشغيل نسخة من دوكر، يمكنك إنشاء الملفات وتعديلها وحذفها تمامًا كما يمكنك باستخدام جهاز افتراضي (virtual machine). حيث ستنطبق التغييرات التي تجريها على تلك الحاوية فقط.. يمكنك تشغيلها وإيقافها، لكن بمجرد ازالتها باستخدام الأمر docker rm، ستفقد التغييرات للأبد. يوضح لك هذا القسم كيفية حفظ حالة الحاوية كنسخة دوكر جديدة. بعد تثبيت Node.js داخل حاوية Ubuntu، لديك الآن حاوية تعمل على نسخة معينة، لكن هذه الحاوية مختلفة عن النسخة (image) التي استخدمتها لإنشائها. عندها قد ترغب في إعادة استخدام حاوية Node.js هذه كأساس للنسخ الجديدة لاحقًا. ثم قم بتنفيذ التغييرات على نسخة دوكر جديدة باستخدام الأمر التالي. $ docker commit -m "What you did to the image" -a "Author Name" container_id repository/new_image_name إن المفتاح -m مخصص لرسالة التنفيذ (commit message) التي تساعدك والآخرين على معرفة التغييرات التي أجريتها، بينما يُستخدم -a لتحديد المؤلف. معرف الحاوية container_id هو الحاوية التي لاحظتها سابقًا في الدليل عندما بدأت جلسة دوكر التفاعلية. إلا إذا قمت بإنشاء مستودعات إضافية على Docker Hub، فعادة ما يكون المستودع هو اسم مستخدم Docker Hub. على سبيل المثال، بالنسبة للمستخدم sammy بمعرّف الحاوية d9b100f2f636، سيكون الأمر كما يلي: $ docker commit -m "added Node.js" -a "sammy" d42d0bbfbd35 sammy/ubuntu-nodejs وعند تنفيذك نسخة ما، يتم حفظ النسخة الجديدة محليًا على جهاز الحاسب الخاص بك. لاحقًا في هذا الدليل، ستتعلم كيفية رفع نسخة إلى سجل دوكر مثل Docker Hub حتى يتمكن الآخرون من الوصول إليها. سيؤدي إدراج نسخ دوكر مرة أخرى إلى إظهار النسخة الجديدة، بالإضافة إلى النسخة القديمة التي تم اشتقاقها منها: $ docker images سترى مخرجات مثل هذا: REPOSITORY TAG IMAGE ID CREATED SIZE sammy/ubuntu-nodejs latest d441c62350b4 10 seconds ago 152MB ubuntu latest 4c108a37151f 2 weeks ago 64.2MB hello-world latest fce289e99eb9 6 months ago 1.84kB في هذا المثال، ubuntu-nodejs هي النسخة الجديدة التي تم اشتقاقها من نسخة ubuntu الموجودة من Docker Hub. حيث يعكس اختلاف الحجم التغييرات التي تم إجراؤها. وفي هذا المثال، كان التغيير هو تثبيت NodeJS. لذا في المرة القادمة التي تحتاج فيها إلى تشغيل حاوية باستخدام Ubuntu مع NodeJS مثبتة مسبقًا، يمكنك فقط استخدام النسخة الجديدة. يمكنك أيضًا إنشاء نسخ من ملف Dockerfile، والذي يتيح لك أتمتة تثبيت البرنامج في نسخة جديدة. لكن يعد هذا خارج نطاق المقال. الآن دعنا نشارك النسخة الجديدة مع الآخرين حتى يتمكنوا من إنشاء حاويات منها. الخطوة 8- رفع نسخ دوكر إلى مستودع دوكر الخطوة المنطقية التالية بعد إنشاء نسخة جديدة من نسخة موجودة هي مشاركتها مع عدد قليل من أصدقائك أو العالم كله على Docker Hub أو أي سجل دوكر آخر يمكنك الوصول إليه. و لرفع نسخة ما إلى Docker Hub أو أي سجل دوكر آخر، يجب أن يكون لديك حساب هناك. يوضح لك هذا القسم كيفية رفع نسخة دوكر إلى Docker Hub. ولمعرفة كيفية إنشاء سجل دوكر الخاص بك، تفقد كيفية إعداد سجل دوكر خاص على Ubuntu 14.04 لرفع نسختك، قم أولاً بتسجيل الدخول إلى Docker Hub. $ docker login -u docker-registry-username عندها ستتم مطالبتك بالمصادقة باستخدام كلمة مرور Docker Hub. و إذا حددت كلمة المرور الصحيحة، فيجب أن تنجح المصادقة. ملاحظة: إذا كان اسم مستخدم سجل دوكر الخاص بك مختلفًا عن اسم المستخدم المحلي الذي استخدمته لإنشاء النسخة، فسيتعين عليك وضع علامة على نسختك باسم مستخدم التسجيل خاصتك. بالنسبة للمثال الوارد في الخطوة الأخيرة، اكتب: $ docker tag sammy/ubuntu-nodejs docker-registry-username/ubuntu-nodejs ثم يمكنك رفع نسختك باستخدام: $ docker push docker-registry-username/docker-image-name لرفع نسخة ubuntu-nodejs إلى مستودع sammy، سيكون الأمر على الشكل التالي: $ docker push sammy/ubuntu-nodejs قد تستغرق العملية بعض الوقت حتى تكتمل أثناء تحميل النسخ، ولكن عند اكتمالها سيبدو الإخراج كما يلي: The push refers to a repository [docker.io/sammy/ubuntu-nodejs] e3fbbfb44187: Pushed 5f70bf18a086: Pushed a3b5c80a4eba: Pushed 7f18b442972b: Pushed 3ce512daaf78: Pushed 7aae4540b42d: Pushed … بعد رفع النسخة إلى السجل، يجب أن تكون مدرجة في لوحة تحكم حسابك مثل تلك التي تظهر في الصورة أدناه. إذا أظهرت محاولة الرفع خطأ من هذا النوع، فمن المحتمل أنك لم تقم بتسجيل الدخول: The push refers to a repository [docker.io/sammy/ubuntu-nodejs] e3fbbfb44187: Preparing 5f70bf18a086: Preparing a3b5c80a4eba: Preparing 7f18b442972b: Preparing 3ce512daaf78: Preparing 7aae4540b42d: Waiting unauthorized: authentication required قم بتسجيل الدخول باستخدام docker login وتكرار محاولة الرفع. ثم أثبت وجوده في صفحة مستودع Docker Hub الخاص بك. يمكنك الآن استخدام docker pull sammy/ubuntu-nodejs لسحب النسخة إلى جهاز جديد واستخدامها لتشغيل حاوية جديدة. الخلاصة لقد قمت بتثبيت دوكر، وعملت بالنسخ والحاويات، ورفعت نسخة معدلة إلى Docker Hub. الآن بعد أن تعرفت على الأساسيات، استكشف بقية دروس أكاديمية حسوب قسم DevOps. ترجمة -وبتصرف- للمقال How To Install and Use Docker on Debian 10 لصاحبه Brian Hogan
-
تقتصر بعض تطبيقات Angular على كونها أدوات مساعدة في صفحة ويب تقليدية، إلا أنّ الأغلبية العُظمى لتطبيقاتها هي التطبيقات وحيدة الصفحة (single-page applications)، التي تستبدل تصفح الويب المعتمد على المتصفّح بواجهاتٍ وانتقالاتٍ مشابهة في صفحةٍ واحدة، مقدمة للمستخدم تجربة تفاعليّة رائعة. إلّا أنّ كتابة تطبيقات وحيدة الصفحة بطريقةٍ بسيطة يجعلنا نخسر شيئًا قيّمًا في التطبيقات المعتمدة على المتصفّح وهو الرّابط URL. ففي تطبيقٍ بسيطٍ وحيد الصفحة، سيكون هناك رابط URL واحد فقط، ولن تكون هناك طريقة لمشاركة روابطَ مخصصة لكلّ مورد (resource) على حدة، كتعليق محدّد على تدوينة ما على سبيل المثال. وعلى العكس، فسيقوم تطبيق تقليدي في طرف المخدّم يتبع نمط REST (تبادل الحالة ضمن رابط HTML) بإدارة الواجهات كبنية هرميّة من الروابط المنظّمة سهلة القراءة والتي تظهر حالة التّطبيق الحاليّة بوضوح. تُقدّم هذه الروابط طريقةً للمستخدم ليعود إلى حالةٍ من حالات التطبيق في وقتٍ لاحق، أو ليشارك هذه الحالة مع الآخرين. يُعرَّف التوجيه في تطبيق ويب تقليديّ بأنّه صلة الوصل بين عمليّات HTTP مثل (GET وPOST وما إلى ذلك) وأنماط الروابط وبين المتحكّمات. إن لم تكن معتادًا على التوجيه من طرف المخدّم فسيقدّم لك دليل التوجيه السريع مقدّمةً سهلةً للتوجيه في JavaScript. على أي حال، فالتوجيه من طرف المستخدم يقلب الحالة رأسًا على عقب. فبدلًا من أن يتمّ التفاعل مع الأفعال التي ينقلها لنا المتصفّح، فسنحتاج إلى إبقاء المتصفّح متابعًا للتحديثات بينما يتفاعل التطبيق مباشرةً مع مدخلات المستخدم. كمثالٍ على ذلك، كيف يمكننا الإبقاء على شريط التّنقل محدّثًا بروابط تمثّل الحالة بدقّة، وفي نفس الوقت نستجيب لروابط تُدخل فيه أو تُحمّل من العلامات المرجعية؟ لحُسن الحظ، فقد تمّ إيجاد الوحدة ngRoute لتساعدنا في هذا العمل. لنتمكّن من استخدام هذه الوحدة، سنحتاج إلى تحميل ملف angular-route.js إضافةً إلى ملف مكتبة Angular الرئيسي. كما سنقوم بتحميل بيئة Bootstrap لاستخدام أنماط CSS الخاصة بها. <base href="/"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular-route.js"></script>لاحظ تعريف العنصر base في السطر الأول في المثال السابق. يُعرِّف هذا العنصر رابط URL القاعديّ لتطبيق Angular الحالي، وهذا العنصر يطلبه دعم Angular لـتوابع HTML5 الخاصة بالتاريخ (سنتحدث عنها أدناه). إن كنت تتساءل لمَ تم إسناد الخاصية href إلى مسار الجذر، بدلًا من أن تكون مسندةً إلى مسار الصفحة، فهذا لأنّ أمثلة هذه السلسلة تعمل داخل أُطُر iframe خاصة لكل واحد منها. <body ng-app="app"> <!-- كل الأمثلة توضع هنا --> </body>بعد ذلك، سيكون علينا جلب ngRoute عند تهيئة الوحدة الجذر، وقد قمنا بتغطية هذه الفكرة بعمق في فصل الوحدات. angular.module('app', ['ngRoute']);وهكذا نكون قد أتممنا تحميل الوحدة ngRoute في تطبيقنا. routeProvider$لنتمكن من استخدام ngRoute، سنحتاج إلى ربط مسار URL نسبي واحدٍ أو أكثر مع معالجاتٍ (handlers). يتيح لنا التابع module.config إعداد وحداتٍ مثل ngRoute بعد أن تقوم Angular بتحميلها. كل ما علينا هنا هو معرفة اسم الخدمة (service) أو المزوّد (provider) الذي سيقوم بإعداد الوحدة. في حالة ngRoute يُسمّى المزوِّد بـrouteProvider$. سنتمكن عند وضع هذا الاسم في تابع الاستدعاء الخلفي config من عمل سلسلةٍ من عمليّات ربط المسارات باستخدام التابع when. الوسيط الأول للتابع when هو المسار النسبي الخاص بوجهة التوجيه، أما الوسيط الثاني فهو كائن الإعدادات الذي يحدد الكيفية التي سيتم بها معالجة محتوى وجهة التوجيه. angular.module('app') .config(function($routeProvider) { $routeProvider .when('/', { template: "<a href='#/about'>About</a>" }) .when('/about', { template: "<a href='#/'>Home</a>" }); });يشبه الخيار template الذي يظهر في المثال السابق ذلك الذي استخدمناه في إعداد التوجيهات سابقًا. لا يحوي المثال السابق أي محتوىً متغيّر، فقط نص HTML ثابت. يُقدّم كلّ قالبٍ رابطًا إلى المسار النسبي للآخرين، لذا سنتمكن في المثال السابق من التبديل بين الاثنين. قد تتساءل أين سيتم وضع المحتوى المعالَج، حسنًا، تُقدّم الوحدة ngRoute توجيهًا مُميّزًا، هو ng-view، ويجب أن يكون هذا التوجيه موجودًا في وحدتنا لتعمل. تتكوّن القوالب الرئيسي في هذا الفصل كلّها من عنصرdiv خالٍ يوجد فيه التوجيه ng-view، وكلّ ما سوى ذلك سيقوم المعالج (handler) بمعالجته للمسار الحالي. <div ng-view></div>جرب النقر على الرابط الناتج لتتم معالجة الرابط للمورد الآخر. لم يعمل؟ حسنًا، لقد تجاهلنا أمرًا هامًا، إن نظرت إلى شريط التنقل الخاص بمتصفحك أثناء نقر الرابط فلن تراه قد تغير ليلائم المسار النسبي الصحيح، في الواقع، ليس هناك أي أمرٍ خاطئ في النص البرمجي الخاص بالمثال، وفي التطبيقات الحقيقية ستلاحظ التغيير، فكلّ ما في الأمر أن البيئة التفاعلية التي تمّ استضافة الأمثلة في داخلها ضمن هذه السلسلة لا تسمح لنا برؤية التغيير في الصفحة الرئيسية للفصل. الخدمة location$جميع أمثلة هذه السلسلة موضوعة داخل صناديق sandbox داخل أُطر iframe الخاصة بكل واحد منها. (يُمكنك إلقاء نظرة على الرابط التالي Codecademy/stuff.js إن كان لديك فضول لمعرفة طريقة القيام بذلك.) وهذا ممتازٌ لعزل البيئة البرمجية، إلا أنّه يمنعك من رؤية رابط الـURL الخاص بالأمثلة في شريط الانتقال. ومن حسن الحظ أنّه توجد طريقةٌ للتحايل على هذا الأمر، مع فائدةٍ إضافيّة في استكشاف خدمةٍ مفيدةٍ في Angular تسمح بمعاينة رابط URL الحالي، وهي الخدمة location$. angular.module('app') .controller('LocationController', function($scope, $location) { $scope.location = $location.absUrl(); });يتم في المثال السابق التصريح عن متحكم بسيطٍ يستقبل الخدمة location$ عن طريق حقن التبعية، ويستخدمها للدخول إلى رابط URL المُطلق الحالي للتطبيق، حيث تكون القيمة هي دمجًا للرابط الثابت للجذر الخاص بالصفحة المغلّفة للمثال مع رابط الامتداد الذي تُديره الوحدة ngRoute. المتحكميُمكننا أن نقوم بتحميل المتحكّم LocationController في المثال السابق بالطريقة المعيارية باستخدام التّوجيه ng-controller، إلّا أنّه بإمكاننا أيضًا أن نقوم بإعداد متحكّمٍ كخيار (option)، كما سنضيف خاصّيّة المجال location إلى قالبنا. angular.module('app') .config(function($routeProvider) { $routeProvider .when('/', { controller: 'LocationController', template: '<div class="well well-sm" ng-bind="location"></div>\ <a href="#/about">About</a>' }) .when('/about', { controller: 'LocationController', template: '<div class="well well-sm" ng-bind="location"></div>\ <a href="#/">Home</a>' }); });ها نحن ذا، صار لدينا الآن محاكٍ لشريط الانتقال لأمثلتنا، ولكن للأسف لن تتمكّن من تعديلها لرؤية الكيفية التي ستدير بها الوحدة ngRoute التغييرات بمعالجتها وإظهار محتوىً جديد، وسيكون عليك اختبار ذلك في تطبيقاتك الخاصة. رمز Hashbangتتوقّع وحدة ngRoute في الحالة الافتراضية أن تبدأ مسارات URL النسبيّة برمز hash (#)، ويُمكنك بسهولةٍ تغييره إلى البادئة hashbang متمثّلة بالرمز (!#) بإضافة إعداداتٍ إلى الخدمة locationProvider$ كما يبيّن المثال التالي. لاحظ أنّه يجب علينا أيضًا إضافة رمز ! إلى الروابط في قوالبنا. angular.module('app') .config(function($locationProvider) { $locationProvider.hashPrefix('!'); }) .config(function($routeProvider) { $routeProvider .when('/', { controller: 'LocationController', template: '<div class="well well-sm" ng-bind="location"></div>\ <a href="#!/about">About</a>' }) .when('/about', { controller: 'LocationController', template: '<div class="well well-sm" ng-bind="location"></div>\ <a href="#!/">Home</a>' }); });لا يزال رمز البادئة hashbang مصادقًا عليه من Google في دليل Webmasters الخاص بها، وتحديدًا في المقطع Making AJAX Applications Crawlable، الذي ينص على أنّ "أقسام الـhash يجب أن تبدأ بعلامة تعجّب"، على أيّ حالٍ، طالما أنّك تقوم بالخطوات اللازمة للتّأكّد من ملاءمة موقعك لقواعد SEO فقد تجد أنّه من الأفضل أن يكون تطبيقك مخدومًا بأسلوبٍ خالٍ من البادئة، وهذا الأسلوب مفعّلٌ في HTML5 الآن. الكائن History من HTML5يُقدّم الكائن window.history تحكّمًا بالتّنقّل في المتصفّحات الحديثة، مما يقدّم تجربة مستخدمٍ سلسة. angular.module('app') .config(function($locationProvider) { $locationProvider.html5Mode(true); });يُمكننا كتابة قيم href الخاصة بنا كمسارٍ بسيط، بدون البادئة # ولا البادئة !#. angular.module('app') .config(function($routeProvider) { $routeProvider .when('/', { controller: 'LocationController', template: '<div class="well well-sm" ng-bind="location"></div>\ <a href="/about">About</a>' }) .when('/about', { controller: 'LocationController', template: '<div class="well well-sm" ng-bind="location"></div>\ <a href="/">Home</a>' }); });استخدام الكائن history من HTML5 وسيلةٌ ممتازةٌ إن كنت غير مضطرٍّ لدعم متصفحاتٍ قديمة في تطبيقك. العنصر templateUrlلنقم الآن بتحليل تطبيقنا البسيط إلى مكوناته الأولية. كما قمنا سابقًا مع التوجيهات بجعل شيفراتنا البرمجية أكثر قابلية للإدارة عن طريق استخراج قوالبنا (حتى الصغيرة منها) إلى ملفاتٍ مستقلة، فسيكون علينا للقيام بذلك فقط استبدال العناصر template في إعداداتنا بالعناصر templateUrl. angular.module('app') .config(function($routeProvider) { $routeProvider .when('/', { controller: 'LocationController', templateUrl: '/views/index.html' }) .when('/about', { controller: 'LocationController', templateUrl: '/views/about.html' }); });دون تغييرٍ على صفحة About التي سنضعها في مجلد views. <div class="well well-sm" ng-bind="location"></div> <a href="/">Home</a> <h4>About</h4>سنتكلّم في الفقرات التالية عن فكرة التعامل مع روابط URL غير الصالحة، لذا لنقم بإضافة واحدٍ منها الآن. <div class="well well-sm" ng-bind="location"></div> <a href="/about">About</a> | <a href="/bad-path">Bad path</a> <h4>Home</h4>جرّب النقر على Bad path في المثال السابق، ستنتهي التّسلية للأسف، وستضطر لإعادة إنعاش الصفحة لاستعادتها. التابع otherwiseما الذي يمكننا فعله لتجنب النهايات الحزينة كما حدث قبل قليل؟ قد يكون أحد الإجابات هو تجنب تقديم روابط غير صالحة، ولكن في التطبيقات الحقيقيّة لا يمكننا إيقاف المستخدم عن الكتابة في شريط التنقل. يُمكننا أن نُعِدّ المُخدّم ليقوم بإعادة توجيهٍ للروابط غير الصالحة إلى رابط الجذر لتطبيق Angular، ولكن هذا يعني إعادة تحميل الصفحة من جديد، مما يعني إعادة تشغيل التطبيق من البداية. كيف يُمكننا بناء تطبيقٍ من طرف المستخدم ليقوم بإعادة توجيهٍ للروابط غير الصالحة؟ لو افترضنا بأننا نريد أن نُعلم المستخدم بأن الرابط غير صالح قبل أن نقوم بإعادة التوجيه، فالأمر الأول الذي نحتاجه هو الواجهة التي ستظهر لأي رابط غير صالح. سنسُمّي هذا القالب 404 ليتناسب مع معناه، وطبعًا يمكنك تسميته بأي اسمٍ آخر. <div class="well well-sm" ng-bind="location"></div> <a href="/">Home</a> <h4 class="text-danger">404 - Not Found</h4>كلّ ما نحتاجه الآن هو عبارة when أخرى لإضافة المسار 404/. ثم لربط أي رابط غير متوافق مع الروابط الموجودة بالمسار 404/، وسنقوم بذلك بإضافة استدعاءٍ للتابع otherwise الذي يقوم بإضافة المسار إلى العنصر redirectTo فيه. في المثال التالي، سيتم تشغيل الاستدعاء للتابع config إضافةً إلى كل الإعدادات السابقة. (يُمكنك تسجيل عددٍ غير محدودٍ من الاستدعاءات الخلفية (callbacks) للتابع config في المزوِّد (provider)) angular.module('app') .config(function($routeProvider) { $routeProvider .when('/404', { controller: 'LocationController', templateUrl: '/views/404.html' }) .otherwise({ redirectTo: '/404' }); });جّرب النقر الآن على Bad path. صحيحٌ أنّ بيئة الأمثلة لا تسمح لك بإدخال مسارٍ عشوائيّ في شريط التصفح، إلّا أنّه يمكنك تعديل "href="/bad-path في القالب السابق إلى أيّ قيمة تريدها لترى كيف سيتم التعامل معها. ربما يجب عليك دومًا إضافة معالجٍ (handler) ليتعامل مع كل الاحتمالات الباقية في إعدادات التوجيه الخاصة بك. الأحداث (Events)تُقدّم Angular تطبيقًا للرسائل من نمط (publish-subscribe) لأحداث التطبيق، وهي تعتمد على ثلاث توابع: emit$ وbroadcast$ وon$، حيث يقوم التابع emit$ و broadcast$ بتفعيل نشر أحداثٍ مخصصة، أما التابع on$ فيقوم بتسجيل معالجات (handlers) للأحداث التي تقوم بنشرها الوحدة ngRoute. فمثلًا، عندما يتمّ تغيير المسار بنجاح سينتج عن ذلك نشر الأحداث التالية: routeChangeStart$locationChangeStart$locationChangeSuccess$routeChangeSuccess$هذه الأحداث مرتبةٌ في القائمة بنفس ترتيب ظهورها أثناء دورة حياة عملية تغيير المسار، لنقُم بإثبات ذلك. تسجيل معالج للحدث (event handler)لنستخدم التابع on$ لتسجيل معالجٍ بسيط لأحداث المسار، حيث سيقوم هذا المعالج بجمع أسماء الأحداث في مصفوفة، ثم سنقوم لاحقًا بطباعة أسماء الأحداث في الصفحة بنفس الترتيب الذي حُفظت به. angular.module('app') .value('eventsLog', []) .factory('logEvent', function(eventsLog) { return function(event) { eventsLog.push(event.name); }; });في هذا المثال، سنقوم بتسجيل الأحداث أثناء انتقالنا من مسار الجذر (والمسؤول عنه هو المتحكم HomeController) إلى المسار events/ (والمسؤول عنه المتحكم EventsController). سنقوم أيضًا بجعل المتحكّم يضيف اسمه إلى القائمة أيضًا لنعرف وقت استدعاء هذا المتحكم. angular.module('app') .controller('HomeController', function($scope, $location, $rootScope, eventsLog, logEvent) { $scope.location = $location.absUrl(); $scope.link = {path: '/events', title: 'Events'}; eventsLog.push("HomeController: " + $location.path()); $rootScope.$on('$routeChangeStart', logEvent); $rootScope.$on('$locationChangeStart', logEvent); $rootScope.$on('$locationChangeSuccess', logEvent); $rootScope.$on('$routeChangeSuccess', logEvent); $scope.eventsLog = eventsLog; });يقوم المتحكم EventsController بإضافة اسمه إلى المصفوفة eventsLog ثم يقوم بكشف (expose) السجل للمجال بحيث يمكننا رؤيته في الصفحة. angular.module('app') .controller('EventsController', function($scope, eventsLog, $location) { $scope.location = $location.absUrl(); $scope.link = {path: '/', title: 'Home'}; eventsLog.push("EventsController: " + $location.path()); $scope.eventsLog = eventsLog; });نفس الواجهة ستعمل في كلا المتحكّمين. وسيتم عرض شريط انتقالٍ متغيّر إضافةً إلى محتويات السجل باستخدام التوجيه ng-repeat. <div class="well well-sm" ng-bind="location"></div> <a ng-href="{{link.path}}">{{link.title}}</a> <ol> <li ng-repeat="event in eventsLog track by $index"> {{event}} </li> </ol>كملاحظةٍ جانبية، يُعدّ المرشح json المبني في Angular مفيدًا في التنقيح وإنشاء السجلات، فإن أردنا رؤية ما هو أكثر من اسم الحدث، فيُمكننا استخدامه لعرض كامل المحتويات لكائن الحدث. سيكون علينا لإتمام المثال أن نكتب إعداداتٍ مباشرةً للمسارين. angular.module('app') .config(function($routeProvider) { $routeProvider .when('/', { controller: 'HomeController', templateUrl: '/views/events/index.html' }) .when('/events', { controller: 'EventsController', templateUrl: '/views/events/index.html' }); });انقر على الرابط في المثال السابق لرؤية الأحداث المسجلة في الصفحة. الموارد التي تتبع أسلوب RESTيُقدّم أسلوب REST في تطوير تطبيقات الويب مجموعةً من المعايير لتنظيم عمليات CRUD (إنشاء، قراءة، تحديث و حذف) على مجموعات الموارد والموارد المفردة أيضًا. لن تحتاج عند استخدامك لـ Angular أن تتّبع أسلوب REST في التوجيه إلا إذا قمت بتحميل الوحدة ngResource التي لم نقم بتغطيتها في السلسلة. على أيّ حال، سنختم هذا الفصل بالقليل من التوجيه المتّبع لأسلوب REST لتحصل على خبرةٍ في بعض الأمور العمليّة في التوجيه. سنحتاج إلى مجموعة موارد لنتمكّن من البدء، وسنعرض في الفصل التالي HTTP كيف نقوم بتحميل البيانات من مخدّمٍ في النهاية الخلفية (backend). أما الآن فسنقوم فقط بحقن مصفوفةٍ من كائناتٍ من نوع item في داخل المتحكّم. angular.module('app') .factory('Item', function(filterFilter) { var items = [ {id: 1, name: 'Item 1', color: 'red'}, {id: 2, name: 'Item 2', color: 'blue'}, {id: 3, name: 'Item 3', color: 'red'}, {id: 4, name: 'Item 4', color: 'white'} ]; return { query: function(params) { return filterFilter(items, params); }, get: function(params) { return this.query(params)[0]; } }; });لكلّ عنصرٍ في المصفوفة قيمة فريدة للخاصية id فيه، مما يسمح لنا بالتعامل معه كموردٍ منفرد. تقوم الخدمة التي يُنشئها التابع factory بتغليف الوصول إلى المصفوفة items بتابعين مفيدين هما: query لمجموعة الموارد، وget للموارد المنفردة. يستخدم التابع query المرشّح ذا الاسم سيء الحظ، filter (محقونٍ مع filterFilter) لتنفيذ جلبٍ حسب المثال باستخدام الوسيط params، أمّا التابع get فيقوم باستدعاء التابع query ليقوم بإعادة موردٍ واحد. (راجع فصل المرشحات إن كنت قد تجاوزته، لمعلوماتٍ إضافيّة عن المرشّحات في Angular.) أوّل شيءٍ نحتاجه في تطبيقٍ يتبع أسلوب REST هو دليل (index) أو مسارٌ للقائمة ليتم كشف المجموعة items كاملةً. وكلّ ما على المتحكم القيام به هو كشف كامل المحتويات للمصفوفة items باستدعاء التابع query دون وُسطاء. angular.module('app') .controller('ItemsController', function($scope, $location, Item) { $scope.location = $location.absUrl(); $scope.items = Item.query(); });واجهة هذا المتحكم سهلةٌ أيضًا، وسنستخدم التوجيه ng-repeat لإظهار العناصر. <div class="well well-sm" ng-bind="location"></div> <a ng-href="/">Home</a> <ol> <li ng-repeat="item in items"> {{item.name}} - {{item.color}} </li> </ol>في هذا المثال الأساسيّ، ستربط إعدادات التوجيه بين مسار الجذر (/) وبين واجهة المجموعة view. angular.module('app') .config(function($routeProvider) { $routeProvider .when('/', { controller: 'ItemsController', templateUrl: '/views/items/index.html' }); });لا يُحقّق المثال السّابق أسلوب REST فعلًا، وذلك لأنّ المسار ليس اسم مجموعة الموارد (items/). المشكلة هي أننا نحتاج إلى ننشئ شيئًا ما في مسار الجذر ليكون المكان الافتراضي الذي يتم تحميل تطبيق Angular منه. سنرى الحل في المثال التالي، وهو إعادة التوجيه الفوري من المسار الجذر إلى المسار items/. العنصر redirectToلنتمكّن من القيام بإعادة توجيهٍ تلقائيّة من مسارٍ لآخر، سيكون علينا استخدام العنصر redirectTo بدلًا من متحكّم وقالب. angular.module('app') .config(function($routeProvider) { $routeProvider .when('/', { redirectTo: '/items' }) .when('/items', { controller: 'ItemsController', templateUrl: '/views/items/index.html' }); });كما يُمكنك أن ترى في شريط الموضع في المثال السابق، فالمجموعة items يتم عرضها في المسار items/. ومهما كان عدد المرات التي تنقر بها على الرابط Home، فلن تتمكن من الانتقال إلى المسار الجذر / وسيتم دومًا إعادة توجيهك إلى المسار items/. هذا استخدامٌ بسيط للخيار redirectTo. إن كنت تريد تطبيق بعض العمليات على إعادة التوجيه، فيُمكنك توفير تابعٍ بدلًا من مسارٍ نصّيّ. التابع searchكما ناقشنا في بداية هذا الفصل، تقدّم روابط URL النصّية تمثيلًا لحالة التطبيق (مثل عملية ترشيحٍ للموارد) يُمكن أن يتم حفظها ومشاركتها بسهولة. كيف يُمكننا معالجة نصّ طلب (query) يطلب فقط الحصول على العناصر التي تكون قيمة العنصر color فيها هو لونًا معيّنًا؟ angular.module('app') .config(function($routeProvider) { $routeProvider .when('/', { controller: 'LocationController', template: '<div class="well well-sm" ng-bind="location"></div>\ <a ng-href="/items?color=red">Red items</a>' }) .when('/items', { controller: 'ItemsController', templateUrl: '/views/items/index.html' }); });في هذا المثال، تحتوي واجهة مسار الجذر الخاص بالتطبيق على رابط التّنقّل الحاوي على نصّ الطلب color=red. يُمكننا جلب قيمة هذا الوسيط بسهولةٍ باستخدام التابع search للخدمة location$. angular.module('app') .controller('ItemsController', function($scope, $location, Item) { $scope.location = $location.absUrl(); $scope.items = Item.query({color: $location.search().color}); });عند النّقر على الرابط Red items في المثال السابق، سترى كيف استخدم التابع query (الذي قُمنا بتعريفه سابقًا في الخدمة items) المُرشّح filter للقيام بعملية جلبٍ حسب المثال. الخدمة routeParams$يعتمد أسلوب REST نموذجيًّا على إلحاق معرّف المورد الفريد في نهاية المسار بدلًا من وضعه في نصّ الطلب. كيف يُمكننا القيام بمعالجةٍ صحيحة للمسار بحيث يحوي المعرّف الفريد؟ أو بعبارةٍ أدقّ، كيف يُمكننا استخراج المعرّف الفريد 3 من المسار items/3/ على سبيل المثال؟ لنقم بتحديث واجهة المجموعة لتتضمّن هذا الأسلوب من رابط التّنقّل لكلّ مورد مفرد على حدة. <div class="well well-sm" ng-bind="location"></div> <p class="lead">Items</p> <ol> <li ng-repeat="item in items"> <a ng-href="/items/{{item.id}}"> {{item.name}} </a> </li> </ol>تُقدّم الخدمة routeParams$ وصولًا ملائمًا للعناصر في المسار، حيث يقوم بكشفها (expose) كعناصر مُسمّاة. يُمثّل القسم الأخير من المسار المعرّف الفريد لموردٍ وحيد يتبع أسلوب REST. فمثلًا، يجب أن يعيد المسار items/3/ تمثيلًا للمورد Item بالمعرّف الفريد 3. في إعدادات التوجيه الخاصة بنا، يُمكننا استخدام البادئة الخاصّة : لتعريف متغيّراتٍ ذات أسماءٍ متغيرّة قابلةٍ للاستخراج من المسار. تمّ الاتّفاق على تسمية الوسيط المعرّف للمورد بالاسم id:، لذا سيكون نصّ المسار هو items/:id/. angular.module('app') .config(function($routeProvider) { $routeProvider .when('/', { redirectTo: '/items' }) .when('/items', { controller: 'ItemsController', templateUrl: '/views/items/linked-index.html' }) .when('/items/:id', { controller: 'ItemController', templateUrl: '/views/items/show.html' }); });تقوم الوحدة بوضع الوسطاء المستخرجة من المسار في الخدمة routeParams$ التي يجب أن نقوم بحقنها في المتحكّم ItemController جنبًا إلى جنبٍ مع الخدمة Item التي أنشأناها. angular.module('app') .controller('ItemController', function($scope, $location, Item, $routeParams) { $scope.location = $location.absUrl(); $scope.item = Item.get({id: $routeParams.id}); });سنقوم باستدعاء التّابع Item.get مستخدمين القيمة التي تمّ إسنادها في routeParams.id$، وهذا التّابع يستخدم المرشّح filter لإيجاد موقع النموذج الصحيح. (يُمكن للتابع ()Array.prototype.find الموجود في المكتبة المركزيّة أن يُقدّم طريقةً أفضل للقيام بذلك عندما ينتشر هذا التابع على نطاقٍ واسع.) طريقة عرض المورد item المفرد سهلةٌ ومباشرة، سنقوم بتضمين رابطٍ للعودة إلى المسار items/ بحيث نتمكّن من العودة إلى واجهة عرض القائمة. <div class="well well-sm" ng-bind="location"></div> <a ng-href="/items">Items</a> <p class="lead">{{item.name}}</p> <p>Color: {{item.color}}</p>صحيحٌ أنّ الخدمة routeParams$ والتابع ()location.search$ كلاهما سهل الاستخدام نوعًا ما، إلّا أنّهما معًا يقدّمان حجر بناءٍ هامًّا في أيّ عمليّة توجيه متضمّنةٍ لأسلوب REST. خاتمةتعلّمنا في هذا الفصل أساسيات طريقة Angular الخاصة بها في التوجيه. ورغم أنّ ngRoute لا تُقدّم بعض الميزات المعقّدة للتوجيه، كدعم الموارد المتداخلة، أو آلة الحالة من الدرجة الأولى، أو توليد مسارات URL، إلا أنّها تعطيك مقدّمةً ممتازةً للتوجيه في Angular. ستجد أنّ العديد من التطبيقات الحقيقية تستخدم UI-Router من مشروع AngularUI بدلًا من ذلك، ولن نتطرّق إليها في هذه السلسلة، بل سيكون الفصل الأخير مقدّمةً لتحميل البيانات من المخدّم في النهاية الخلفية (backend) باستخدام الخدمة http$ في Angular. ترجمة -وبتصرّف- للفصل الثاني عشر (Routing) من كتاب: Angular Basics لصاحبه: Chris Smith.
-
يكمن سحر العمل عن بعد (remote work) في أن المكان الذي تعمل منه لا يملك أية دلالة أو تأثير مُباشرة على العمل، فيُمكنك أن تقوم ببناء فريق، ثقافة، وشركة ليس لها حدود جغرافيّة. في Help Scout، نفتخر بثقافة العمل عن بعد التي أنشأناها، صحيحٌ أنّنا ارتكبنا العديد من الأخطاء خلال مسيرتنا، إلّا أنّنا نسعى للتّعلّم من كلّ تلك الأخطاء. لقد أعطتني ثلاثُ سنواتٍ من العمل الحرّ تَلَتها ثلاثُ سنواتٍ هنا في Help Scout بعض الآراء الرّاسخة بخصوص مفهوم العمل عن بعد، وسأورد هنا خلاصة ما تعلّمته خلال هذه السّنوات: 1- الكتابة تؤثر على كل ما تقوم بههناك سببٌ وجيهٌ لتكون نصيحة "توظيف الكاتب الأفضل" ذائعة الصّيت: الكتابة الواضحة تعني التّفكير الواضح، فتكلفة إرباك الفريق أو تشويش الزّبون تكلفةٌ باهظة، ولكنّها لا تُقارن بتكلفة إرباك نفسك. وبما أنّ التّواصل عن بعد مبنيٌّ تقريبًا وبشكل حصري على الكتابة، فستحتاج إلى أن تكون قادرًا على إيصال أفكارٍ معقّدةٍ بقوّة، وشرحها لفريقك بلغةٍ سهلةٍ واضحة. 2- لابد من تحديد وقت للتوقف عن العملقد يكون من الصّعب عليك عندما تعمل من المنزل أن "توقف التّشغيل" عند نهاية اليوم. ما مقدار العمل الكافي عندما تكون أنت مُدير وقتك بشكل كامل؟ سأقتبس مقولةً لـJo Bennett من مسلسلThe Office التّلفزيونيّ يقول فيها: "إن كان بإمكانك أن توقّع باسمك على هذا اليوم، وتكون فخورًا بمقدار العمل الذي أنجزته، عندها يُمكنك أن تحتفل بكل ما تحمله الكلمة من معنى". وإن كان ذلك يعني بأنّ يوم الأربعاء سيكون قصيرًا ومريحًا بعض الشّيء مقابل عملٍ طويلٍ يوم الخميس، فلْيكن ذلك. لا يأتي العمل العظيم من عدد السّاعات التي تصرفها عليه، بل من المهام الهادفة التي أنجزتها فيه. يُمكنك القول بأنّ يوم عملك قد انتهى عندما تكون فخورًا بأن توقّع باسمك عليه. 3- إن لم يؤمن مديرك بفكرة العمل عن بعد فستفشللا يُمكنك النّجاح في العمل عن بعد إن لم تكن شركتك مبنيّة على أساس العمل عن بُعد. حتّى لو كان لديكم مكتبٌ رئيسيّ لإدارة الفريق، فلا بدّ على الفريق أن يختار بين ثقافة العمل عن بعد، أو ثقافة العمل في نفس المكتب، فليس هناك حلٌّ وسطٌ بينهما. ستعيق ثقافة العمل في نفس الحيّز الجغرافي قدرات فريقك الكامنة، فهي تعتبر العامل عن بعدٍ مواطنًا من الدّرجة الثّانية. 4- سترتفع إنتاجيتك على حساب التعاونهذه مقايضةٌ مشهورةٌ في العمل عن بعد، سيكون التّركيز والإنتاج المطلوبان لإدارة وقتك الخاص صعبًا، ولكنّ عليك تحمّل ذلك. سيتطلّب التّعاون مجهودًا إضافيّا، وعليك أن تعرف كيف تُوازن بين إنتاجيّتك الشّخصيّة مع إنتاجيّة الفريق، أو بعبارةٍ أخرى، معدّل إنجاز المهام عندما يعمل شخصان أو أكثر معًا.الوقت هو الأساس، والنّجاح يعتمد على السُّرعة في التّحرّك، والسّرعة في الاستجابة، وأن تكون ملتزمًا بنظام العمل في الشركة. 5- اسع جاهدا للتحكم في بيئتكيُساعد الارتباط الوثيق للمؤثّرات الخارجية الكثيرَ من النّاس على إنشاء العقليّة المناسبة للعمل.” في هذا المكان سأعمل، وفي هذا المكان سأعيش”. عندما يتعارض العمل مع الوجود في المنزل، ستحتاج إلى وضع حدودٍ لنفسك، فبالنّسبة لي، أقوم بإبقاء باب مكتبي المنزليّ مغلقُا دومًا، وذلك لأفصل نفسي عن بقيّة المنزل، إنّ مكتبي أرضٌ محرّمةٌ لا يتمّ فيها شيءٌ غير العمل. إن لم تقم بهذا الفصل فستجد أن "المنزل" قد أصبح مكانًا بلا هويّة، حيث لا يُمكنك فصل العمل عن حياتك الأُسريّة. 6- برامج المحادثة = إلهاء متواصللا يوجد في العمل عن بعد إمكانيّة ليتلهّى الموظّفون بحديثهم إلى بعضهم في المكتب، إلّا أنّ تطبيقات المراسلة والمحادثة هي البديل. تبقى هذه التّطبيقات مفيدةً للتّقليل من عدد رسائل البريد الإلكترونيّ المتبادلة، إلّا أنّ العديد من الأشخاص يُبقون التّنبيهات مُشغّلة، وبذلك سيبقى التّراسل معطِّلًا للعمل. حسنًا، مقاطعة العمل أمرٌ ضروريٌّ من وقتٍ لآخر، ولكن يجب اختيار هذا الوقت بحكمة. من المُفيد أن يتّبع الفريق نظامًا مُعيّنًا للتّراسل، ويبدأ ذلك بتعريف نوع الرّسائل الملائمة لكلّ قناةٍ من قنوات الاتّصال. 7- تشجع واطلب وقتا دون إزعاجفليحذر من يُحاول إرضاء النّاس: لأنّك في العمل عن بعد لن يرى أحدٌ ما تقوم به، فلا أحد سيعرف الوقت الملائم لمقاطعتك. يُفترض بالمحادثات غير المتزامنة أن لا تكون مخصّصةً للأمور العاجلة أبدًا أو للّقاءات، ولكن طيبة قلبك ستدفعك للردّ على رسائل البريد الإلكتروني والمحادثات الفوريّة دون تأخير. توقّف عن القيام بذلك، فعندما تحتاج إلى إغلاق هذه الوسائل لتتمكّن من إنجاز العمل الصّعب، كُن حازمًا (ولكن بلطف) بخصوص عدم قُدرتك على الرّد في هذا الوقت. 8- رجح دوما سوء التواصل على الخبث والمكرهذه هي النُّسخة الخاصّة بالعمل عن بعد لمبدأ Hanlon’s razor، فقد تبدو بعض الانفعالات المتبادلة باردةً بسبب ميل الإنسان إلى إساءة تفسير النّصوص الخالية من العواطف، فالمزاح والتّهكّم لا يُمكن التّعبير عنه جيّدًا، فليس هناك لغة جسد لتساعد المتلقّي على الفهم، كما أنّ المحادثات عبر برامج المحادثة يُمكن أن تنقطع في أيّ لحظة، ممّا سيؤدّي بسهولةٍ إلى افتراض وجود نبرةٍ حادّةٍ في الكلام رغم أنّ الشخص الآخر لا يقصد ذلك. حافظ على حُسن الظّنّ، فعندما تعمل مع أشخاصٍ رائعين، سيكون حسن ظنّك في مكانه غالبًا. 9- الحديث والدردشة عن أمور لا علاقة لها بالعمل ينفع أكثر مما تتخيلالصّمت ليس دومًا من ذهب، فالمحادثات القصيرة تُنشئ روابط بين النّاس لا يشعرون بقيمتها إلّا عندما تزول. حتّى المحادثات التي تخلو من الفائدة يُمكنها أن تُقدّم مُدخلاتٍ خلّاقة، وقد تُعطيك تغذيةً راجعة مبكّرة، وتُعلمك بما يجري ضمن الشركة. يُمكنك الاستعاضة عن هذه المحادثات بلقاءاتٍ محضّرة من قبل أو مرتجلة بينك أنت وشخص آخر (one-on-one meeting) ، كأن تعقد جلسةً أسبوعيّة مع قائد الفريق أو أن تقوم بعمل دردشات غير رسميّة مع شخصٍ من خارج القسم الذي تعمل فيه. 10- أنت المسؤول عن الحديث عن سياق عملكلقد مرّت بضعة أشهر دون أن أنشر أي تحديث داخلي أو أن أخبر أيّ أحد من أفراد الفريق عن أداء مُدوّنتنا(والتي أتوّلى مسؤوليتها). لقد ظننت بأنّ جميع العاملين خارج قسم التّسويق لن يهتمّوا لذلك، ولكنّني كُنت مخطئًا بالطّبع، أفضلُ الفِرَق ترغب بأن تعرف على الأقل القليل عن كلّ شيءٍ يحدث في الشركة. هذه التحديثات البسيطة تطمئن المُوظّفين وتُساعد في تحسين العمل، فعلى سبيل المثال، أتطلّع دومًا للتّحديثات في قسم الدّعم الفنّي، وذلك لأنّني أرغب بمعرفة المزيد عن زبائننا. 11- الثقة تولد الثقةلا أحد يرغب أن يظلّ المدير واقفًا وراءه طوال الوقت. في العمل عن بعد، المُدير الجيّد يكون على عكس ذلك تمامًا، لا بُدّ من أن تحصل على الثّقة لتقوم بمهامك. وبالمقابل، عليك أن تثق بقائد فريقك. إن كنت أنت المدير، ستكون اللحظة التي تنتقل فيها من المتابعة المنظّمة إلى الإزعاج البغيض هي اللحظة التي تتحول فيها إلى نوع المدراء الذي تكرهه. إن كنت تريد أن يثق بك الناس، فثق بهم. 12- الأيام القليلة الأولى صعبة ومربكةكانت البداية سهلةً بالنسبة لي، فقد كان عندي تجربةٌ سابقة في العمل عن بعد، إلّا أن العمل عن بعد يكون منعطفًا صعبًا للكثيرين، ولذا يكون توظيف أشخاصٍ جدد أمرًا غاية في الأهمّيّة. سأخبرك بأحد أفضل الممارسات لدي، وقد قُمنا بها في شركتنا وهي تحديد "الصّديق الصّدوق للموظّف الجديد"، حيث يقوم واحدٌ من أفراد الفريق بمصادقة الموظّف الجديد فَور انضمامه، ويكون ذلك ممتازًا للإجابة عن الأسئلة "الغبيّة"، ليتعلّم القواعد المتعارف عليها، وليشعرَ بالرّاحة في تكيّفه مع الشركة الجديدة، الممارسات الجديدة، الوجوه الجديدة، وكلّ ذلك وهو بعيدٌ مئات أو آلاف الأميال عن باقي الفريق. 13- أنصحك بألا تعمل مرتديا ملابس النومسأخبرك بسرّي الذي احتفظت به لنفسي طيلة هذا المقال: أنا لا أعمل بملابس النوم فعلًا، فهذا تعبيرٌ مبتذلٌ قد شاع بين الناس، وسيسرّني نقض هذه الصورة، وذلك لأنّ معظمنا يرتدي الملابس التي لا نشعر بالخجل إن ارتديناها عندما نكون معًا، وهذا يؤثّر على مزاجك، وثقتك، وعلى العمل ككُلّ. خلاصة رُبّما يكون أفضلُ جزءٍ في العمل عن بعد هو الفرص التي يُتيحها، كما أنّك ستتعرّف على أشخاصٍ يؤثّرون فيك أكثر من تأثير العمل نفسه، ولذلك عليك أن تختار من تعمل معهم بحكمة، فستحتكّ بهم لساعاتٍ كثيرةٍ وعليك أن تكون متوافقًا معهم. إن وجدت مجموعةً من الناس لها نفس هدفك، فالعمل عن بعدٍ سيعطيك الفرصة لتكون معهم، بغضِّ النّظر عن المكان الذي أنت فيه، وهذا ما يُسعدني. ترجمة -وبتصرّف- للمقال Lessons Learned from 6 Years of Working in My Pajamas لصاحبه Gregory Ciotti. حقوق الصورة البارزة: Designed by Freepik.
-
يقترنُ المتحكّم بمجالٍ جديد يتمّ إنشاؤه عند إنشاء المتحكّم، أمّا التّوجيه فلا يتمّ إعطاؤه مجالًا خاصًّا به عند إنشائه في الحالة الافتراضيّة، بل يستخدم المجال المتاح، وذلك اعتمادًا على مكانه في المستند. بالنّسبة لي فأنا أعتبر هذه الحالة الافتراضيّة سيّئة، وذلك لأنّ معظم التّوجيهات تُكتب كمكوّناتٍ سيتمّ إعادة استخدامها مع حالتها المغلَّفة. ولكن هذه الحالة الافتراضيّة تُفيد في جعل تعلُّم استخدام التّوجيهات سهلًا في البداية، وذلك كما رأينا في الفصل السّابق. والآن بعد أن اعتدنا على التّوجيهات المخصّصة، فقد حان الوقت لنتعلّم كيفيّة إدارة حالة التّوجيه (directive state) بطريقةٍ مغلّفة، مما يسمح بإعادة استخدامٍ آمنٍ للتّوجيه المخصّص. عزل المجال يُشار إلى عبارة عزل المجال (isolate scope) غالبًا كأحد المصطلحات الصّعبة في Angular. ولكن كما هو الحال غالبًا في علوم الحاسوب (وأيضًا في شبه علوم الحاسوب)، فالمفاهيم خلف الأسماء الرّنّانة غالبًا ما تكون بسيطةَ الفهم. عزل المجال يعني فقط إعطاء التّوجيه مجالًا خاصًّا به بحيث لا يكون موروثًا من المجال الحالي. قبل أن نبدأ، نحتاج إلى الاهتمام بالإعدادات المُعتادة. أوّلًا، لنقُم بإضافة Angular وBootstrap إلى الصّفحة. <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script> لابدّ من تعريف وحدةٍ للتطبيق. angular.module('app', []); والآن سيكون علينا تحميل وحدة الجذر الخاصّة بنا عن طريق تمرير اسمها إلى التّوجيه ng-app. <body ng-app="app"> <!-- الأمثلة توضع هنا --> </body> والآن بعد أن أصبحت Angular ووحدة الجذر مُعدّتين جيّدًا، لنقُم بتعريف متحكّمٍ يقوم بالكشف (expose) عن ثلاث عناصر لتخزين سلاسل نصّيّة، وكلّ عنصرٍ سيُخزّن اسم المكوِّن الذي سيستخدمه في العرض. السلسلة النصية الأولى تُعرّف عن المتحكّم نفسه، والسلسلتان النصيتان الباقيتان لنسختين منفصلتين من توجيهٍ سنقوم بتعريفه لاحقًا. angular.module('app') .controller('MessageController', function($scope) { $scope.message = "the controller"; $scope.message1 = "directive 1"; $scope.message2 = "directive 2"; }); وفيما يلي شيفرة التّوجيه، وفيها نقوم أيضًا بإنشاء عنصرٍ اسمه message في نسخة عنصر المجال الذي يتم تمريره إلى تابع الرّبط. angular.module('app') .directive('message', function() { return { template: "<p>hello, from {{message}}</p>", link: function(scope, element, attrs) { scope.message = scope.$eval(attrs.message); } }; }); لقد ناقشنا في الفصل السابق استخدام scope.$eval للوصول إلى عنصرٍ في المجال تمّ تمريره كقيمة نصّيّة لوسيط التّوجيه، وما يهمّنا تحديدًا في هذا المثال هو ما سيحدث للعنصر scope.message الموجود في المتحّكم ونسختي التّوجيه. سنضيف خانات إدخالٍ مرتبطةً بعناصر المجالات الثلاثة التي تمّ إنشاؤها في المتحكّم. قبل أن تنظر إلى استخدام المثال ومخرجاته، ألقِ نظرةً سريعةً مرّةً أخرى على المتحكّم والتّوجيه السابقين. هل ترى أيّ إمكانيّةٍ للتضارب بينهما؟ <div ng-controller="MessageController"> <h4>hello, from {{message}}</h4> <input class="form-control" ng-model="message"> <input class="form-control" ng-model="message1"> <input class="form-control" ng-model="message2"> <br> <div class="well" message="message1"></div> <div class="well" message="message2"></div> </div> خلال الوقت الذي أنهى فيه الاستخدام الثّاني للتّوجيه عمله، تمّ إسناد القيمة “2 directive” للعنصر message في مجال المتحكّم. إن قمتَ بكتابة شيءٍ داخل خانة الإدخال العُلويّة، والتي ترتبط بالعنصر message، فسترى أنّ التغيير أدّى إلى تحديث مخرجات نسختي التّوجيه معًا. في هذا المثال، تمّت مشاركة مجال المتحكّم ضمن النّسخ الثّلاثة، مما أدّى إلى حدوث هذه المشكلة، فنحن لا نريد أن تقوم أيّ نسخةٍ من نُسخ التّوجيه بتعديل المجال الذي يُشاركه المتحكّم MessageController مع نُسخ التّوجيه الأخرى. إذًا، كيف يُمكننا إعطاء كلّ نسخةٍ للتّوجيه مجالها الخاصّ المعزول؟ هل يُمكنك اكتشاف الحلّ بنفسك؟ الحلُّ هو التّصريح عن عنصر scope في كائن الإعدادات الخاص بالتّوجيه. الخيار الأكثر سهولةً هو إسناد القيمة true إلى هذه الخاصّية، مما يعطي نُسخ التّوجيه مجالاتٍ خاصّة بها. هذه المجالات الجديدة ترث من المجال الحاليّ، تمامًا كما لو كانت نُسخ التّوجيه نُسخًا لمتحكّمٍ ما. angular.module('app') .directive('message', function() { return { template: "<p>hello, from {{message}}</p>", link: function(scope, element, attrs) { scope.message = scope.$eval(attrs.message); }, scope: true }; }); لقد استطعنا حلّ مشكلة الحالة الابتدائيّة لكلّ العناصر. ولكن هل انتهينا؟ كلّا. جرّب الكتابة في أيّ خانةٍ من خانات الدّخل. إذا أردنا أن تؤدّي التحديثات للعنصرين message1 وmessage2 التّابعين لمجال المتحكّم إلى تحديثات في التّوجيه، فسيكون علينا القيام بأكثر مما قمنا به حتّى الآن، لأنّ تابع الرّبط لا يتمّ تشغيله إلّا مرّةً واحدة، عندما يتمّ إخراج (render) التّوجيه للمرّة الأولى، وعند استخدام eval$ لتغيير قيمة عنصرٍ في التّوجيه فلن يكون هذا التّغيير دائمًا ولن يعمل الرّبط ثنائيّ الاتّجاه بين مُخرجات التّوجيه والنّموذج الأصليّ. إن لم تكن قد جرّبت تغيير القيَم في خانات الدّخل بعد، فأنصحك بشدّةٍ بتجربة ذلك لتتأكّد بنفسك من أنّ القيم لا يتمّ تحديثها. إذًا ما الذي يجب علينا إضافته لتابع الرّبط ليعمل الرّبط ثنائيّ الاتّجاه؟ لا نحتاج إلى شيء، علينا حذف تابع الرّبط. فالشيفرة الأمريّة (imperative) المطلوبة في تابع الرّبط مملّةٌ ومتوقّعة. لم لا نقوم باستبدالها بإعداداتٍ تصريحيّة ونقوم بأتمتة العمل البرمجيّ؟ هذا بالضّبط هو ما أراد مخترعوا Angular القيام به. طرق ربط المجال المعزول يتمّ التصريح عن طرق ربط المجال المعزول باستخدام الرّموز التّالية: الرّمز = للرّبط ثنائيّ الاتّجاه (two-way binding) الرّمز & للرّبط وحيد الاتّجاه (one-way binding) الرّمز @ للرّبط النّصّي (text binding) لنلقِ نظرةً على كلّ واحدةٍ من تلك الطّرق على حدة، ولنبدأ بالطريقة المفيدة، الرّبط ثنائيّ الاتّجاه. الربط ثنائي الاتجاه (=) لحلّ مشكلة تحديث مخرجات التّوجيه، فلن يكون علينا إلّا استبدال الطريقة الأمريّة (imperative) في كتابة الشّيفرة، والتي اعتدنا عليها عند كتابة تابع الرّبط، بكائن إعداداتٍ يتمّ إسناده للعنصر scope. angular.module('app') .directive('message', function() { return { template: "<p>hello, from {{message}}</p>", scope: { message: '=' } }; }); لاحظ بأنّ قيمة العنصر scope أصبحت الآن كائنًا بدلًا من القيمة true البوليانيّة الأوّليّة (primitive). ما قمنا به يؤدّي إلى قطع العلاقة الوراثيّة بين المجالات. ولنتمكّن من رؤية ذلك عمليًّا، يُمكنك استبدال scope: true في المثال الأسبق، بـ {} :scope. لا يعمل تابع الرّبط في ذلك المثال دون الوصول إلى العناصر في المجال الأب. لنتمكّن من أتمتة العمل الذي يقوم به تابع الرّبط ذاك، فيجب علينا إضافة عنصرٍ إلى كائن الإعدادات scope بحيث يكون اسمه بنفس اسم عنصر المجال المحلّي الذي نريد ربطه بعنصرٍ ما في المجال الخارجيّ، فبما أنّ توجيهنا يستخدم العنصر message في القالب الخاصّ به، فسنضيف عنصرًا اسمه message إلى كائن الإعدادات، ونُسند إليه القيمة =، مما يدلّ على أنّنا نريد أن يتمّ ربطه باستخدام الرّبط ثنائيّ الاتّجاه مع العنصر في المجال الخارجيّ الذي يتم تمرير اسمه إلى الخاصّيّة message. قد لا يكون الأمر سهلًا، ولكنّه يعمل جيّدًا ويُمكنك تجريب ذلك بكتابة شيءٍ ما في خانات الإدخال لتحديث عناصر المتحكّم في المثال السابق. وبهذا نكون قد أتممنا كتابة المتحكّم message أخيرًا. الربط وحيد الاتّجاه (&) قد تحتاج التّوجيهات أحيانًا إلى طريقةٍ لاستدعاء التّوابع في المجال الشّامل رغم أنّها تكون في مجالٍ معزول. بعبارةٍ أخرى، قد تحتاج إلى معالجة عبارة Angular ضمنالسياق الأصلي. ومن أجل ذلك تمّ إيجاد طريقة الرّبط وحيد الاتّجاه، أو خيار المعالجة. لنبدأ بمتحكّمٍ يقوم بالكشف عن تابع استدعاءٍ خلفيٍّ (callback) اسمه kaboom. angular.module('app') .controller('ClickController', function($scope) { $scope.message = "Waiting..."; $scope.kaboom = function() { $scope.message = "Kaboom!"; } }); يعرض المثال التالي كيف يُمكن استخدام المتحكّم ClickController، حيث يمكن استخدامه مع التّوجيه العام bootstrap-button الّذي يُمكن أن يتمّ إعداده ليقوم باستدعاء التّابع kaboom عندما يتم نقر الزّر. <div ng-controller="ClickController"> <h4> {{message}} </h4> <bootstrap-button the-callback="kaboom()"></bootstrap-button> </div> لنقُم الآن بكتابة شيفرة التّوجيه bootstrap-button، في داخل قالبه البسيط، سنستخدم التّوجيه ng-click المدمج في Angular لربط حدث النّقر بتابع الاستدعاء الخلفي. views/button.html <button type="button" class="btn btn-default" ng-click="theCallback()"> Submit </button> بما أنّ التّوجيه العام الذي سنكتبه لا يُمكنه معرفة اسم تابع الاستدعاء الخلفيّ الحقيقي الذي سيتم تضمينه، سنستخدم اسمًا عشوائيًّا theCallback كاسمٍ للتّابع. حسنًا، والآن كيف سنقوم بربط theCallback (في الواقع هي عنصرٌ في المجال) مع kaboom؟ ليست هذه بمشكلةٍ، فلدينا الرّبط وحيد الاتّجاه. angular.module('app') .directive('bootstrapButton', function() { return { restrict: 'E', scope: { theCallback: '&' }, templateUrl: '/views/button.html' } }); يقوم الرّمز (&) بعمليّة الرّبط المطلوبة. والآن ماذا عن خيار الإعداد الأخير، @، المُسمّى بخيار الرّبط النّصّيّ؟ سنحتاج إلى الانتقال مؤقّتًا إلى مثالٍ آخر من المصطلحات الصّعبة في Angular، وذلك كي نتمكّن من تبيان فائدة هذا الخيار بأفضل ما يمكن. الاستبدال Transclusion لابدّ من وجود اسمٍ آخر أفضل من Transclusion، فمُستخدموا Ruby on Rails سيلاحظون تشابه آلية عمل الـTransclusion مع yield، وهذا المصطلح أسهل للفهم. تعني عمليّة الاستبدال transclude لعنصرٍ ما، أن نقوم بالتّحكّم بإعادة إنتاج المكوّن الذي قام بتضمين التّوجيه، وفي حالتنا يكون هذا المكوّن هو القالب الذي يتضمّن التّوجيه. إن لم تكن هذه العبارة واضحةً لك، فيُمكنك التّفكير بأنّ الاستبدال (transclusion) يعطي الأمر التّالي: “قم بقصّ محتويات القالب الموجود داخل شيفرة التّوجيه، ثمّ ألصقها هنا“. ويعتمد مكان اللصق على التّوجيه المدمج ng-transclude الذي يُمكننا وضعها في مكانٍ ما داخل القالب الموجود في التّوجيه المخصّص. كمثالٍ بسيط، لنقم بكتابة التّوجيه box الذي نريد تضمينه حول محتوىً ما، عن طريق وضعه في عنصرٍ div كغلاف. <div box> This <strong>content</strong> will be <em>wrapped</em>. </div> ما سيقوم به التّوجيه box هو تغليف المحتوى الدّاخليّ بعنصر p مع بعض التّنسيق من Bootstrap. هناك جزآن يجب الاهتمام بهما عند استخدام الاستبدال (transclusion)، أوّلهما، يجب أن نقوم بإضافة transclude: true إلى إعدادات التّوجيه، وثانيهما، لابُدّ من استخدام التّوجيه ng-transclude في مكانٍ ما من القالب الخاصّ بالتّوجيه، وذلك لاختيار المكان الذي نريد أن نقوم فيه بإضافة المحتوى الجديد. angular.module('app') .directive('box', function() { return { template: '<p class="bg-primary text-center" ng-transclude></p>', transclude: true }; }); ألم يكن هذا سهلًا؟ بالطّبع. يُمكننا جعل المثال السابق أسهل من ذلك بعدم استخدام التّوجيه من الأساس، وإضافة تنسيق CSS للعنصر المغلِّف مباشرةً. ولذلك سنقوم باستخدام الاستبدال (transclusion) في مثالٍ أكثر واقعيّة لتغليف التّعقيدات الخاصة بتنسيق Bootstrap لـلوحةٍ مع ترويسة. سيسمح التّوجيه panel لمستخدمه بتحديد النّص الخاص بشريط العنوان في رأس اللوحة، وأيضًا بتحديد المحتوى المطلوب إدخاله في جسم اللوحة. بالنّسبة لمحتوى اللوحة، سيتمّ توظيف الاستبدال (transclusion) تمامًا كما في المثال السّابق، أمّا بالنّسبة لنصّ العنوان، فسنستخدم الخيار المتبقّي لربط المجال المعزول الذي تركناه قبل قليل. الربط النصي (@) عندما يكون كلّ ما تحتاجه هو نسخ سلسلة نصّيّة إلى مجال التّوجيه الخاصّ بك، عندها سيكون الرّبط البسيط وحيد الاتّجاه (@) هو الطّريقة التّصريحيّة للقيام بذلك. تُبيّن الشّيفرة التّالية القالب الخاصّ بالتّوجيه. هدفنا من العنصر title هو أن نسمح للنّصّ الذي تحويه بأن يصبح أحد خصائص التّوجيه. views/panel.html <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"> {{title}} <small> Static text in the directive template </small> </h3> </div> <div class="panel-body" ng-transclude></div> </div> وفي إعدادات التّوجيه الخاصّ بنا، سنقوم بالحدّ من استخدامه للعناصر، وسنصرّح عن طريقة ربط الخاصّيّة title مع العنصر title في مجاله. angular.module('app') .directive('panel', function() { return { restrict: 'E', templateUrl: '/views/panel.html', scope: { title: '@' }, transclude: true }; }); المثال التّالي يوضّح طريقة استخدام التّوجيه panel لكتابة النّصّين الثّابتين (static) في العنوان، وذلك في الوقت نفسه الذي يتمّ فيه استخدام الرّبط ثنائيّ الاتّجاه في المحتوى المُستبدل (transcluded). <div ng-controller="MessageController"> <panel title="Static text in the client template"> <h3> <em>Hello</em>, from <strong>{{message}}</strong> </h3> <input class="form-control" ng-model="message"> </panel> </div> لا تصلح طريقة الرّبط التي استخدمناها للعنوان باستخدام @ إلّا مع السّلاسل النّصّيّة التي لن يتمّ التّعديل عليها، فهذه الطّريقة لا يتمّ فيها مراقبة التّغييرات التي تطرأ على العنصر الأصليّ. خاتمة بناءً على خبرتك الحاليّة، فقد يكون هذا الفصل هو الأصعب ضمن هذه السلسلة، لذا أُهنّئُك على إنهائك له. يُفترض بك الآن أن تكون قادرًا على تعريف توجيهاتٍ مخصّصة يُمكن استخدامها دون الخوف من التّأثيرات الجانبيّة النّاتجة عن الحالة المشتركة، وهذه خطوةٌ كبيرة في طريقك نحو إكمال سيطرتك على التّوجيهات في Angular. في هذه النّقطة، سنحوّل تركيزنا عن الفروقات الدّقيقة في استخدام التّوجيهات، وسنتوجّه إلى مفهومٍ أساسيٍّ في مجال تطوير الويب من طرف المُستخدم: التسيير (routing) و الـURLs. ترجمة -وبتصرّف- للفصل الحادي عشر (Directive Scopes) من كتاب: Angular Basics لصاحبه: Chris Smith.
-
لَعِبت التّوجيهات المبنيّة داخل Angular مثل ng-bind وng-model وinput دور النّجوميّة في هذه السلسلة منذ الفصل الأوّل، المبادئ، حتّى أنّ فصل المجموعات كان يركّز كلّيًّا على استخدام توجيهٍ واحدٍ (قويٍّ إلى حدّ كبير) هو التّوجيه ng-repeat. قد تظنّ بعد ذلك أن قائمة التّوجيهات المبنيّة في داخل Angular تحوي حلولًا لجميع الحالات، ولكنّ هذا غير صحيحٍ بالطّبع. لقد تصوّر مصمّموا Angular بأنّها ستكون طريقةً للسّماح لغير المبرمجين ببناء صفحاتٍ تفاعليّة، إلّا أنّها تطوّرت لتصبح منصّةً تعطي مطوّري الويب القوّة لإنشاء امتداداتٍ وتخصيص HTML العاديّة، وهذا يعني إنشاء التّوجيهات الخاصّة بك، وهذا تمامًا ما سنقوم به في هذا الفصل. يكون إنشاء توجيهٍ مخصّص بشكله الأبسط مشابهًا كثيرًا لإنشاء مرشّح مخصّص، وهذا هو سبب الحديث عن المرشّحات قبل التّوجيهات في فصلٍ سابق. التّابع الخارجي الذي سنقوم بتمريره إلى التّابع directive هو غلافٌ لحقن التّبعيّة ويُدعى بتابع الصّناعة (factory function)، ويُعرف بالوصفة (recipe) ضمن توثيق Angular. يتمّ استدعاء هذا التّابع مرّةً على الأكثر، أو لا يتمّ استدعاؤه أبدًا إن لم يتمّ استخدام التّوجيه، كما تعلّمنا سابقًا في فصل الخدمات. في داخل تابع الصناعة يوجد تابعٌ آخر تقوم Angular فعليًّا باستدعائه وهو المكان الذي نقوم فيهذر عن بالعمل الحقيقيّ للتّوجيه، ويُسمّى بتابع الربط link. التابع directiveنستخدم التّابع directive لتسجيل توجيهٍ مخصّص، وهذا التّابع ذو الاسم المناسب لوظيفته جزءٌ من الواجهة البرمجيّة للوحدات، لذا سنحتاج إلى إنشاء وحدةٍ جذرٍ لتطبيقنا. angular.module('app', []);ثمّ نقوم بتحميل الوحدة الجذر عن طريق تمرير اسم التّطبيق إلى التّوجيه ng-app. <body ng-app="app"> <!-- الأمثلة توضع هنا --> </body>وبذلك أصبحنا جاهزين لإنشاء توجيه مخصّص، ولنبدأ بتوجيهٍ بسيطٍ جدًّا اسمه hello. angular.module('app') .directive('hello', function() { return function(scope, element) { element.text("hello"); }; });للقيام بأيّ شيءٍ مفيدٍ سنحتاج إلى استخدام الوسيط الثّاني لتابع الرّبط، والّذي يكون غلافًا (wrapper) لعنصرٍ من المستند نجلبه من مكتبة jqLite الخاصّة بـAngular أو من مكتبة jQuery حسب الوسيط الذي قمت بتضمينه. في هذا المثال، قمنا باستبدال المحتوى السّابق للعنصر بالعبارة hello. The message is <span hello></span>.الناتج: The message is hello.أليس هذا رائعًا؟ لقد أنشأنا لتوّنا خاصّيّة HTML جديدة خاصّةً بنا. الوسيط scopeفي المثال السابق قمنا يدويًا (hardcoded) بكتابة النصّ التي نريد كتابته وهذا لا يفيدنا كثيرًا، لذا لنرى إن كان بإمكاننا استبدال النّصّ بقيمةٍ متغيّرة. ربّما كنت تتساءل بخصوص الوسيط الأوّل scope، لنقم بإضافة متحكّمٍ يقوم بإنشاء العنصر message في مجاله. angular.module('app') .controller('MessageController', function($scope) { $scope.message = 'hello'; });والآن لنقم باستخدام هذا العنصر في التّوجيه، وسنُسمّيه message أيضًا. angular.module('app') .directive('message', function() { return function(scope, element) { element.text(scope.message); }; });من الواضح أنّه يتوجّب علينا استخدام هذا التّوجيه في المكان داخل المستند الذي يكون تابعًا للمتحكّم MessageController. <p ng-controller="MessageController"> The message is <span message></span>. </p>الناتج: The message is hello.قد تتساءل، لماذا يتمّ في المتحكّم تعريف وسيط المجال مسبوقًا برمز الدولار scope$ بينما في تابع الرّبط يكون هذا الوسيط بدون علامة الدولار في بدايته scope، والسّبب هو أنّ أسماء الوسطاء في تابع الرّبط غير مهمّة (إلّا بالنّسبة لنا فقط) على عكس الوسطاء التي يتم حقنها. الوسطاءكما تعلّمنا سابقًا عند استخدام التّوجيهات المبنيّة داخل Angular، فالتّوجيهات تقبل تمرير الوسطاء، وهذه الوسطاء تُمرّر داخل الوسيط الثّالث لتابع الرّبط، وهو الوسيط attrs. مرّةً أخرى، وعلى عكس حقن التّبعيّات، يُمكننا تسمية الوُسطاء بأيّ اسمٍ نريده، ويُفضّل استخدام أسماءٍ مفهومةٍ لتبقى الشّيفرة مفهومة. angular.module('app') .directive('welcomeMessage', function() { return function(scope, element, attrs) { element.text(attrs.welcomeMessage); }; });The message is <em welcome-message="bonjour"></em>.الناتج: The message is bonjour.لاحظ أنّه عندما نستخدم أكثر من كلمةٍ واحدةٍ في كتابة اسم التّوجيه بأنّنا نستخدم نمط camel case حيث نكتب أوّل حرفٍ في الكلمة الثّانية بحرفٍ كبير، وهذا هو النّمط المتعارف عليه في JavaScript، ولكنّ النمط المستخدم في HTML هو فصل الكلمات باستخدام إشارة (-) وهو ما يُدعى hyphenated style، وخلاصة الكلام هي أنّك عندما تريد استخدام welcome-message لتضمين التّوجيه الخاص بك في القالب، سيكون عليك استخدام الاسم welcomeMessage عندما تقوم بتعريفها في JavaScript. قد تكون لاحظت بأنّ بعض التّوجيهات المدمجة في Angular تقوم بمعالجة (evaluate) العبارات، إلّا أنّ هذا السلوك ليس هو السلوك الافتراضي. The message is <em welcome-message="'bon' + 'jour'"></em>.الناتج: The message is 'bon' + 'jour'.بما أنّنا نعمل داخل قالب Angular، سيكون بإمكاننا دومًا تمرير نتيجة عبارةٍ معالجَة (evaluated). The message is <em welcome-message="{{'bon' + 'jour'}}"></em>.الناتج: The message is bonjour.يُمكنك أيضًا إدخال سلسلةٍ نصّيّة معرّفة مسبقًا داخل السلسلة النّصّية العاديّة. <p ng-init="greeting = 'bonjour'"> The message is <span welcome-message="I say {{greeting}}, you say hello"></span>. </p>الناتج: The message is I say bonjour, you say hello.لتتمكّن من معالجة العبارة الممرّرة إلى التّوجيه كسلسلةٍ نظاميّة، ستحتاج إلى استخدام التّابع eval$. التّابع scope.$evalبالرغم من أنّ Angular لا تعتبر أنّ الوسيط المُمرّر إلى التّوجيه عبارةٌ تحتاج إلى معالجة، إلّا أنّه يمكننا ببساطةٍ أن نعالج هذه العبارة باستخدام التّابع eval$، ولأخذ العلم، فهذه الطّريقة لا تؤدّي إلى إعادة تحديث العرض (view) عند تغيُّر النّموذج، وهذا مفيدٌ فقط في حالات الاستخدام الأساسيّة. angular.module('app') .directive('welcomeMessage', function() { return function(scope, element, attrs) { var result = scope.$eval(attrs.welcomeMessage); element.text(result); }; });لنقم بتجربة حالة استخدامٍ بسيطة نقوم فيها بوصل سلسلتين نصّيّتين. The message is <em welcome-message="'bon' + 'jour'"></em>.الناتج: The message is bonjour.يُمكننا أيضًا باستخدام scope.$eval أن نقوم بتمرير كائنات، مّما يتيح لنا طريقةً لتلقّي العديد من الوسطاء معًا، وسنلقي نظرةً في فقرةٍ لاحقةٍ من هذا الفصل علىتوجيهات العناصر، والتي تقبل أيضًا العديد من الوسطاء المُسمّاة عن طريقة إتاحة وجود العديد من الخصائص للعنصر، أمّا الآن فسنرى في المثال التالي معالجةً لكائنٍ من نوع options، حيث نقوم بتمرير العديد من الوسطاء إلى التّوجيه. angular.module('app') .directive('welcomeMessage', function() { return function(scope, element, attrs) { var options = scope.$eval(attrs.welcomeMessage); var result = options.emoticon + ' ' + options.message + ' ' + options.emoticon; element.text(result); }; });وبهذه الطّريقة استطعنا إدخال وسيطين بفعّاليّة، message وemoticon، واستطعنا إنجاز الوصل بينهما داخل التّوجيه. The message is <em welcome-message="{message: 'bonjour', emoticon: '\u263a'}"></em>.الناتج: The message is ☺ bonjour ☺.صِرنا الآن نفهم كيفيّة استخدام توجيهاتٍ مخصّصةٍ مع المجالات والمتحكّمات. ولكن ماذا لو أردنا استخدام خدماتٍ أخرى داخل التّوجيه؟ حقن التبعيةإن أردنا الوصول إلى موارد مُدارة مثل خدمةٍ ما، أو مُرشّحٍ ما من داخل توجيهنا الخاصّ، فلن يكون علينا سوى التّصريح عن التّبعيّات كوسيطٍ في التّابع الخارجيّ. (راجع فصل حقن التّبعيّة وفصل الخدمات إن لم يكن هذا المفهوم مألوفًا لديك.) على سبيل المثال، لنفترض بأنّنا نريد توجيهًا يُمكنه تطبيق حسمٍ على السّعر وأيضًا القيام بتنسيقٍ للعملة لقيمة المتغيّر price في المجال. سيحتاج هذا التّوجيه للوصول إلى الخدمة المخصّصة calculateDiscount المُعرّفة كما يلي. angular.module('app') .value('discountRate', 0.8) .factory('calculateDiscount', function(discountRate) { return function(amount) { return amount * discountRate; }; });والآن سنستخدم تابع الوصفة (recipe) التّغليفيّ لحقن الخدمة calculateDiscount مع المُرشّح currency أيضًا. angular.module('app') .directive('discount', function(calculateDiscount, currencyFilter) { return function(scope, element, attrs) { var price = scope.$eval(attrs.discount); var discountPrice = calculateDiscount(price); element.html(currencyFilter(discountPrice)); }; });لاحظ بأنّنا قمنا بتمرير العنصر price كقيمةٍ لوسيط التّوجيه في القالب، ويُمكننا الحصول على هذه القيمة للوسيط عن طريق الوسيط attrs حيث نصل إلى الخاصّيّة (discount) وهي اسم التّوجيه الخاصّ بنا. في المثال التّالي، سنقوم بإسناد قيمة هذا العنصر في المجال (الّتي نحصل عليها عن طريق scope.$eval) إلى المتغيّر المحلّي المُسمّى price. <table ng-init="price = 100"> <tr> <td>Full price:</td> <td ng-bind="price | currency"></td> </tr> <tr> <td>Sale price:</td> <td discount="price"></td> </tr> </table>الناتج: Full price: $100.00 Sale price: $80.00قد ترغب في وضع مُرشّحٍ بدلًا من التّوجيه، فالتّحويل الذي قمنا به بسيطٌ للغاية، حيث لم نضِف إلى المستند أيّ سلوكٍ تفاعليّ. لننتقل الآن إلى مثالٍ أكثر تعقيدًا بحيث يحتاج إلى إضافة وسمٍ باستخدام القالب. القوالبكُلّ العمليّات التي قمنا بتنفيذها في الأمثلة السّابقة على الوسيط element كانت سهلةً إلى حدٍّ ما، ولكن ماذا لو أردنا إضافة قطعةٍ متغيّرة الحجم من محتوىً جديدٍ إلى المستند؟ لنقم بمحاولة حلّ هذه المشكلة بالتّدريج. في البداية، لنقُل بأننا نريد فقط تغليف سلسلةٍ نصّيّةٍ نُمرّرها كوسيطٍ للتّوجيه الخاصّ بنا باستخدام عنصر strong. The message is <span strong-message="a strong hello!"></span>يُمكننا دومًا استخدام عمليّة وصل السّلاسل (concatenation). angular.module('app') .directive('strongMessage', function() { return function(scope, element, attrs) { element.html('<strong>' + attrs.strongMessage + '</strong>'); }; });الناتج: The message is a strong hello!أو يُمكننا بناء المحتوى بطريقةٍ برمجيّة باستخدام jqLite أو jQuery. angular.module('app') .directive('strongMessage', function() { return function(scope, element, attrs) { element.html('<strong>'); element.find('strong').text(attrs.strongMessage); }; });الناتج: The message is a strong hello!عمليّة وصل السّلاسل والتّغييرات البرمجيّة على المستند طريقةٌ جيّدةٌ عند استخدامها للأمور الصّغيرة كعنصرٍ واحدٍ مثلًا، ولكن ماذا سنفعل عندما يكون المحتوى يتضمّن العديد من العناصر المتداخلة؟ أو عندما يكون لدينا قائمةٌ متغيّرة الحجم؟ سيجيبك عن هذا السّؤال أيّ خبيرٍ في تطوير النهاية الأماميّة (front-end) بأنّ مكتبة القوالب ستسهّل الأمر كثيرًا، ولحسن الحظّ، فـAngular مكتبة قوالب، وستجعل الأمر أكثر سهولة. يُمكن لـAngular أن تقوم بإخراج (render) قوالب متداخلةٍ يقدّمها التّوجيه الخاصّ بنا، وذلك في الوقت نفسه الّذي تقوم فيه بإخراج قالب التّطبيق الخاصّ بنا. لإضافة قالبٍ للتّوجيه، سنحتاج إلى تغيير أسلوبنا السّابق، فبدلًا من إعادة (return) تابع الرّبط فقط، سنقوم بإعادة كائنٍ يحوي تابع الربط ومعه قالب. هذا تغيير كبير، لذا فلنكرّر ذلك: سيقوم المُغلّف الوصفيّ (recipe) الآن بإعادة كائنٍ بدلًا من تابع، وسنستخدم العنصر link في هذا الكائن المُعاد للوصول إلى التّابع. فيما يلي مثالٌ يستخدم قالبًا لإنشاء عددٍ غير محدّدٍ مسبقًا من عناصر li. angular.module('app') .directive('wordList', function() { return { link: function(scope, element, attrs) { scope.words = attrs.wordList.split(" "); }, template: "<li ng-repeat='word in words'>\ {{word}}\ </li>" }; });ستستلم Angular مهمّة تضمين القالب في المجال الصحيح، ثمّ إلحاق المخرجات بالعنصر الذي استخدم التّوجيه. بما أنّ مخرجات هذا التّوجيه ستكون عناصر في قائمة، لنقم بتضمين التّوجيه في العنصر ul. <ul word-list="we love templates"></ul>الناتج: . we . love . templatesأظنّ بأن المثال بدأ يوضّح لك قوّة وأناقة التّوجيهات المخصّصة. العنصر templateUrlاستخدمنا في المثال البسيط السّابق رمز الشرطة الخلفيّة (\) مرّتين لنتمكّن من كتابة نصّ القالب، وكما ترى، فالقوالب السّطريّة سُرعان ما تصبح مستهجنةً داخل شيفرة JavaScript، ولذلك سنستعين بالقوالب الخارجيّة التي تدعمها Angular، سواء كانت عناصر خاصّة مخفيّة في المستند، أو موارد ويب منفصلةً تمامًا في مكانٍ مستقل. <li ng-repeat="word in words"> {{word}} </li>لقد قمنا باستخراج شيفرة القالب إلى مورد ويب موجودٍ في الرابط النّسبيّ views/word-list.html، والآن سنقوم بإضافة العنصر templateUrl إلى كائن الإعدادات الذي يعيده التّوجيه الخاص بنا، وسنقوم بإسناد مسار القالب إليه، وذلك بدلًا من استخدام العنصر template وإسناد شيفرة القالب إليه مباشرةً. angular.module('app') .directive('wordList', function() { return { link: function(scope, element, attrs) { scope.words = attrs.wordList.split(" "); }, templateUrl: '/views/word-list.html' }; });أمّا الاستخدام فيبقى كما هو دون تغيير. <ul word-list="external templates rock"></ul>الناتج: . external . templates . rockالقوالب الخارجيّة عمومًا أسهلُ بكثيرٍ للقراءة وللتّعديل من القوالب السّطريّة. العنصر replaceلا بُدّ من استخدام التّوجيه في المثال السابق ضمن قائمة، ولكن ماذا لو لم يقم المستخدم باستخدام التّوجيه بشكلٍ صحيح؟ أحد الحلول هو استبدال العنصر الذي يتضمّن التّوجيه بدلًا من إلحاق العناصر به. إذًا لنقُم بإضافة عنصر ul لتغليف مخرجات قالبنا، سنحتاج أيضًا إلى عنصرٍ جديدٍ في كائن الإعدادات هو العنصر replace، وسنُسند إليه القيمة true. (القيمة الافتراضيّة له هي بالطّبع false.) لنقم أوّلًا بإضافة العنصر ul إلى قالبنا. <ul> <li ng-bind="word" ng-repeat="word in words"></li> </ul>التّغيير الوحيد الهامّ في التّوجيه هو استخدام العنصر الجديد replace. angular.module('app') .directive('wordList', function() { return { link: function(scope, element, attrs) { scope.words = attrs.wordList.split(" "); }, templateUrl: '/views/word-list-ul.html', replace: true }; });بما أنّ العنصر الذي يتضمّن التّوجيه سيتمّ استبداله مهما كان، يُمكننا تضمين التّوجيه في عنصرٍ غريبٍ وغير ملائمٍ مثل h1. <h1 word-list="lists not headlines"></h1>الناتج: . lists . not . headlinesيبيّن المثال السّابق قوّة تأثير التّوجيهات على المستند، ولكنّني أُفضّل أن يكون التّصميم أكثر وضوحًا، ودلالته صحيحة، ولذلك فبدلًا من استبدال وسوم HTML، أرى أنّ الصّواب هو التّحقّق من صحّة العنصر الذي قام بتضمين التّوجيه، وإن لم يكن صحيحًا يتمّ إلقاء خطأ (throw an error). العنصر controllerفي بداية هذا الفصل، رأينا مثالًا يحوي متحكّمًا يقوم بتحضير عنصرٍ في المجال، ثمّ استخدمنا هذا العنصر لاحقًا داخل تابع الربط الخاصّ بالتّوجيه. والآن بعد أن تعرّفنا على طريقة استخدام القوالب، لنقُم بإعادة كتابة هذا المثال، واستبدال تابع الرّبط الذي يحدّد نصّ العنصر في المستند بقالب. angular.module('app') .controller('MessageController', function($scope) { $scope.message = 'hello, from the external controller'; }) .directive('message', function() { return { template: "<strong>{{message}}</strong>" }; });بما أنّ المتحكّم والتّوجيه منفصلان، فكلاهما يحتاج إلى التّضمين بشكلٍ منفصلٍ في المثال التالي. يُمكننا أن نضع المتحكّم في نفس العنصر الذي يحوي التّوجيه أو في عنصرٍ مغلّف، فلا فرق. The message is <span message ng-controller="MessageController"></span>.الناتج: The message is hello, from the external controller.كما ترى في المثال السّابق، لقد قُمنا بإزالة تابع الرّبط، فوجوده أمرٌ اختياريّ، واقتصرنا على العنصر template، وذلك لأنّ استخدام تابع الرّبط يكون للقيام بالتّغييرات البرمجيّة على المستند بطريقةٍ أكثر أُلفةً لمطوّري النّهاية الأماميّة. أمّا الآن بعد أن انتقلنا إلى استخدام القوالب، فسنحتاج إليه للقيام بمهمّاتٍ نحتاج فيها إلى الوصول إلى العنصر الحاوي للتّوجيه أو إلى خصائص العنصر (مُتضمّنةً التّوجيه بذاته، كما رأينا سابقًا.) إضافةً إلى ذلك، فإنّ تحضير بيانات النّموذج لا يحتاج إلى الوصول إلى العنصر أو إلى خصائصه، وهو من مهمّات المتحكّم، وهذا بالرّغم من إمكانيّة القيام بذلك داخل تابع الرّبط كما بيّنّا في فقرة القوالب. لذا، سيكون من الأفضل أن ننقل مسؤوليّة تحضير النّموذج إلى داخل المتحكّم، إلّا أنّ المشكلة الآن هي أنّ المتحكّم منفصلٌ ويحتاج إلى إعداداتٍ خاصّةٍ به، فهل يُمكننا نقل المتحكّم إلى داخل التّوجيه؟ نعم، يمكننا ذلك. angular.module('app') .directive('message', function() { return { template: "<strong>{{message}}</strong>", controller: function($scope) { $scope.message = 'hello, from the internal controller'; } }; });أليس هذا رائعًا؟ لم نحصل الآن على مكوِّنٍ مغلّفٍ جيّدًا وحسب، بل تخلّصنا الآن من تضمين المتحكّم واستخدام التّوجيه ng-controller في الوسم. The message is <span message></span>.الناتج: The message is hello, from the internal controller.ويمكننا طبعًا القيام بكلّ الأشياء المعتادة مع المتحكّم، كتعريف تابعٍ في المجال وتضمين هذا التّابع من عنصر تحكّمٍ داخل القالب. راجع فصل المتحكّمات للاستزادة. الخيار restrictتُوجد أربع طرقٍ لتضمين توجيهٍ ما، إلّا أنّنا لم نرَ حتّى الآن إلّا طريقة تضمين التّوجيهات كخصائص لعناصر HTML، وذلك لأنّه الأسلوب الأكثر شيوعًا وفائدةً للقيام بذلك، وهي أيضًا القيمة الافتراضيّة للخيار restrict الذي يُمكن أن يملك واحدةً أو أكثر من الخيارات التّالية: 'A' يتمّ تضمينه كخاصّيّة (Attribute)'C' يتمّ تضمينه كفئة (Class)'E' يتمّ تضمينه كعنصر (Element)'M' يتمّ تضمينه كتعليق (Comment)بالنّسبة للأساليب الثّلاثة الأخرى في التّضمين (الفئة، العنصر والتّعليق) فأسلوب التضمين كعنصرٍ هو الأكثر إثارةً للاهتمام من النّاحية العمليّة، فأسلوبا الفئة والتّعليق موجودان لدعم الحالات الهامشيّة مثل وجود تحقّق صارمٍ لصحّة نصوص HTML. أعتبر أنّ عنصر التّوجيه يُشبه مدفعًا كبيرًا، يجب عليك ألا تستخدمه إلّا إن كان الأمر يستحق ذلك. نموذجيًّا، يتمّ استخدام هذه الطّريقة عندما نحتاج إلى وضع عددٍ كبيرٍ من الوسوم داخل مكوّنٍ من مكوّنات HTML ويكون أيضًا عليك تمرير العديد من الوسطاء إليه، فعندما تستخدم عنصر التّوجيه، ستتمكّن من تمرير قيم الوسطاء على شكل خصائص لهذا العنصر. في المثال التّالي لن يكون لدينا هذا العدد الكبير من الوسوم إلّا أنّنا سنصمّم جدولًا مع كلّ الزخرفات الخاصة به. <table class="table table-condensed"> <thead> <tr> <th ng-bind="column" ng-repeat="column in columns"></th> </tr> </thead> <tbody> <tr ng-repeat="model in models"> <td ng-bind="model[column]" ng-repeat="column in columns"></td> </tr> </tbody> </table>سيستخدم تابع الرّبط الآن وسيطين في تعريف التّوجيه الخاصّ بنا، وسنصل إلى هذا الوسيطين عن طريق attrs.models و attrs.columns. angular.module('app') .directive('modelsTable', function() { return { restrict: 'E', templateUrl: '/views/models-table.html', link: function(scope, element, attrs) { scope.models = scope.$eval(attrs.models); scope.columns = attrs.columns.split(","); } }; });قد ترغب بإسناد القيمة true إلى العنصر replace في عنصر التّوجيه إن كانت البيئة التي تعمل عليها تُسبّب لك بعض المشاكل عند استخدام أسماء عناصر غير معياريّة، على الجانب الآخر، تركُ هذا العنصر المخصص في مستندك قد يساعدك أثناء تصحيح الأخطاء في الشيفرة. سنقوم مرّةً أخرى باستخدام متحكّمٍ خارجيّ في هذا المثال. angular.module('app') .controller('ItemsController', function($scope) { $scope.items = [ {name: 'Item 1', color: 'green', price: 5.0}, {name: 'Item 2', color: 'blue', price: 4.93} ]; });وأخيرًا، سنقوم بإضافة Bootstrap إلى صفحتنا لجعل مثالنا الأخير أكثر أناقة. <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script>الخطوة الأخيرة الآن هي تضمين عنصر التّوجيه الخاصّ بنا مع وسيطيه models وcolumns حيث سنضيفهما على شكل خصائص للوسم. <p ng-controller="ItemsController"> <models-table models="items" columns="name,color,price"></models-table> </p>رائع، لقد قمنا بإخفاء الشيفرات المزعجة الخاصّة بجدول HTML بإحكام. خاتمةلقد قدّم لك هذا الفصل عن التّوجيهات معرفةً ستعطيك قوّةً كبيرةً عند استخدامك لـAngular في تطوير تطبيقات طرف المستخدم، فكما رأينا في المثال الأخير، يُمكنك الآن تعريف توجيهٍ مخصّصٍ يستخدم قوّة قوالب Angular ويستخدم التّوجيهات المدمجة معها، مع قدرةٍ كبيرةٍ على التّقليل من الشّيفرات المزعجة ومن ثمّ زيادة الإنتاجيّة. لا يزال علينا فهم أمورٍ أخرى بخصوص التّوجيهات، خصوصًا عندما تدخل مشكلة الحالة المشتركة إلى السّاحة. سيعرض الفصل القادم هذه السيناريوهات المتقدّمة، فإن كنت تشعر بأنك حصلت على المعرفة الكافية لخدمة أهدافك، فيُمكنك القفز مباشرةً إلى فصل HTTP. أما إن كنت ترغب في تعلُّم المزيد، فتابع مباشرةً إلى الجزء الثّاني من فصل التّوجيهات. ترجمة وبتصرّف للفصل العاشر من كتاب: Angular Basics لصاحبه: Chris Smith.
-
تَستخدم المُرشّحات في Angular نمط خطّ الأنابيب (pipeline) بشكلٍ مشابه للـUnix shell، فقد استعارت رمز الخطّ العمودي المشابه لرمز الأنبوب من Unix لتستخدمه لتمرير عبارات التّهيئة أو خواص المجال للمُرشّح وأيضًا لربط المُرشّحات معًا. سنقوم بتحميل إطار عمل Bootstrap الخاصّ بـCSS لأننا سنقوم بإنشاء بعض الجداول في أمثلة هذا الفصل ممّا سيجعلها أكثر ترتيبًا، وسنجلبه من شبكة توصيل المحتوى (CDN) كما تُبيّن الشّيفرة التّالية. <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script>ستسخدم الأمثلة أيضًا المتحكمات و الخدمات لتحضير البيانات للعرض، لذا سنقوم بتعريف وحدة جذرٍ لإداراتهم. angular.module('app', []);سنقوم بإسناد اسم وحدة الجذر لتطبيقنا إلى التّوجيه ng-app. <body ng-app="app"> <!-- الأمثلة توضع هنا --> </body>مفهوم المُرشّحات سهلُ التّعلّم، لذا سيكون هذا الفصل مرورًا سريعًا على المُرشّحات المبنيّة داخل Angular، ثُمّ سنستعرض كيف يمكننا إنشاء المُرشّحات الخاصّة بنا. lowercase و uppercaseلنبدأ جولتنا مع المُرشّحات المبنيّة في Angular ببعض التّحويلات على السّلاسل النّصية، والمُرشّح lowercase بدايةٌ ممتازة. نبدأ بكتابة أيّ عبارة (في مثالنا استخدمنا عبارة "AngularJS")، ثم نضع رمز الأنبوب (|)، وبعده نكتب اسم المُرشّح (lowercase). <p>{{"AngularJS" | lowercase}}</p>الناتج: angularjsيُمكننا تطبيق المرشّح uppercase على النّتيجة عن طريق إضافة رمز أنبوبٍ آخر، ثمّ اسم المرشّح، وهذا ما نُسمّيه بسلسلة المُرشّحات. <p>{{"AngularJS" | lowercase | uppercase }}</p>الناتج: ANGULARJSبالطّبع ليس هناك فائدةٌ من وضع lowercase قبل uppercase، وسنرى في نهاية هذا الفصل مثالًا أفضل على سَلسَلة المُرشّحات، ولكن على الأقل رأينا كيف نقوم بذلك الآن. numberالمُرشّح التّالي سهلٌ أيضًا، المُرشّح number يُقدّم المساعدة الأساسيّة لتنسيق الأعداد وتقريبها. <p>{{1000.12345 | number}}</p>الناتج: 1,000.123بالنّسبة لإعداداتي المحلّيّة، en-us، المُخرجات في المثال هي 1,000.123، يقوم المُرشّح بتقريب العدد المُمرّر إليه إلى 3 خاناتٍ بعد الفاصلة، ومن الواضح أنّ هذا لن يناسب جميع الاستخدامات، ولحسن الحظ، يمكننا تمرير وسطاء إلى المرشّحات التي تقبل القيام بذلك، والمُرشّح number يقبل ذلك. الوسيط الأول الذي نقوم بتمريره هو القيمة التي نريد تطبيق المُرشّح عليها، أما الوسيط الإضافي الثّاني فيجب أن يتبع اسم المُرشّح ويكون مفصولًا عنه بنقطتين (:). الوسيط الإضافيّ الثّاني للمُرشّح number هو fractionSize، وهو يسمح لنا بالتّحكّم بعدد المنازل التي يتم عرضها بعد الفاصلة، وبهذا يمكننا رؤية القيمة كاملةً. <p>{{1000.12345 | number:5}}</p>الناتج: 1,000.12345ماذا يحدث لو قمنا بتمرير قيمةٍ سالبةٍ إلى الوسيط fractionSize؟ استبدل 5 بـ 3- ثمّ جرّب 4-، هل يُمكنك تخمين السّبب؟ لا مشكلة، حتى أنا لا يمكنني ذلك. currencyالمُرشّح التّالي مفيدٌ بعض الشّيء في التّطبيقات العمليّة: يُقدّم المُرشّح currency الدّعم الأساسيّ لتنسيق العملة وتقريب قيمة العدد. <p>{{1999.995 | currency}}</p>الناتج: $2,000.00وهو يقبل وسيطين إضافيين، أوّلهما هو رمز العملة، استبدل currency بـ'€':currency في المثال السّابق لتجريبه. الوسيط الثّاني هو fractionSize. انتبه عند استخدامك للمُرشّح currency عند تقريب المبَالِغ الماليّة، فليس هناك طريقةٌ واحدةٌ فقط هي الصّحيحة عندما ننظر للأمر على أنّه مسألةٌ ماليٌّةٌ وليست تقنيًّة، فهذا المُرشّح يستخدم تقريب النصف للأعلى وقد لا يكون هذا جيّدًا دومًا، كما رأينا في المثال السّابق. لا يوجد حاليًّا طريقةٌ موثّقةٌ لتجاوز طريقة المُرشّح في التّقريب، ما رأيك بأن تحاول التّعديل على المثال السّابق لتجعل المُرشّح يعرض القيمة الصّحيحة. dateسيبيّن المُرشّح date لك إلى أيّ مدىً تذهب بك إعدادات المُرشّحات. لا تسمح Angular بتهيئة كائنٍ من نوع Date داخل عبارة، لذا سنستخدم متحكّمًا لتحضير النّموذج الخاصّ بالمثال. angular.module('app') .controller('DateController', function($scope) { $scope.now = Date.now(); });يبدأ المثال التّالي بعنصرٍ في المجال اسمه now، بدلًا من كتابة عبارة. <p ng-controller="DateController"> {{now | date}} </p>الناتج: Oct 28, 2015إن قمت بإزالة date | من المثال السّابق سترى القيمة الأصليّة للعنصر now، وهي عدد الميللي ثانية منذ شهر كانون الثّاني عام 1970 حسب التوقيت العالميّ UTC. والآن قم بتجربة بعض التّنسيقات المُعرّفة سابقًا، والتي يدعمها هذا المُرشّح، قم بتجربة 'date:'medium | ثمّ 'date:'shortTime | وبعدها 'date:'yyyy-MM-dd HH:mm:ss Z |، وجرّب بعض التّعديلات من عندك بعد ذلك. يُسمح بكتابة السّلاسل المحرفية داخل نمط التّنسيق، مثلًا "date:"MMMM 'of the year' yyyy |، وبدلًا من استخدام محارف الهروب مثل % لوضع رموز التّنسيق، سيكون عليك وضع السّلاسل المحرفية بين علامتَي تنصّيّصٍ مفردة (') وأن تكون عبارة التّنسيق الكاملة بين علامتَي تنصّيّصٍ مزدوجة ("). كما أنّ الفراغات في عبارة التّنسيق تدخل في تنسيق العبارة. orderByيُمكن للمُرشّحات القيام بما هو أكثر من تنسيق المخرجات، حيث يُمكن استخدمها لتحويل المجموعات، كما سنرى في المثال التّالي. لنفترض أنّ خدمةً في النهاية الخلفية (backend) أَرجعت سجلًّا مرتًبًا بطريقةٍ غير مرغوبةٍ لأسباب تتعلّق بالعرض. (لتبسيط المثال، ستكون البيانات مكتوبةً يدويًّا داخل الخدمة items التي تراها أدناه، وذلك بدلًا من التّعامل مع نهايةٍ خلفيّة بعيدة حقيقيّة.) angular.module('app') .value('items', [ {name: 'Item 3', position: 3, color: 'red', price: 2.75}, {name: 'Item 1', position: 1, color: 'red', price: 0.92}, {name: 'Item 4', position: 4, color: 'blue', price: 3.09}, {name: 'Item 2', position: 2, color: 'red', price: 1.95} ]);سنقوم بعد ذلك بحقن المصفوفة items في داخل متحكّم. angular.module('app') .controller('ItemsController', function($scope, items) { $scope.items = items; });بناءً على ترتيب العناصر الحالي في المصفوفة، كيف يمكننا ترتيب العناصر حسب قيمة العنصر position؟ إن كنتَ قد قفزتَ من مقعدك الآن وصرخت قائلًا "Underscore" (أو "lodash")، فتستحقّ منّي وسام الوفاء لـJavaScript، ولكنّها ليست الإجابة التي نبحث عنها، فـAngular تملك مُرشّحًا للقيام بذلك، إنّه المُرشّح orderBy. <ol ng-controller="ItemsController"> <li ng-repeat="item in items | orderBy:'position'"> {{item.name}} </li> </ol>الناتج: 1. Item 1 2. Item 2 3. Item 3 4. Item 4هذا رائع، ولكن ماذا لو أردنا أن يكون التّرتيب بالجهة المعاكسة؟ في هذا المثال البسيط يمكننا القيام بذلك بطريقتين، الأولى هي إضافة الرمز - قبل اسم العنصر، أي أن نكتب 'orderBy:'-position بدلًا من 'orderBy:'position. (يمكنك أيضًا إضافة الرمز + لاختيار التّرتيب التّصاعدي، إلا أنّه لا يؤثّر على مثالنا السّابق.) أمّا الطّريقة الثّانية فهي إعطاء قيمةٍ للوسيط البولياني reverse بعد اسم العنصر المطلوب التّرتيبُ حسب قيمته. في مثالنا، قم بكتابة orderBy:'position':true بدلًا من 'orderBy:'position. فائدةٌ إضافيّة: عدّل سجلّ البيانات في المتحكّم بحيث تجعل عنصرين في المصفوفة لهما القيمة نفسها بالنّسبة للعنصر position، ألا يزال بإمكانك استخدام المُرشّحorderBy لترتيب العناصر بشكلٍ صحيح؟ ماهي الخاصّيّة التي استخدمتها؟ انتبه للتحديثاتبفضل طريقة Angular في الربط ثنائيّ الاتّجاه، سيكون من السّهل جدًّا أن تجعل النّماذج في قائمة أو جدول العرض قابلةً للتّعديل، كلّ ما تحتاجه هو إضافة خانة إدخالٍ داخل الحلقة مع إسناد اسم عنصر المجموعة إلى التّوجيه ng-model. ولكن انتبه، فبما أنّ Angular جاهزةٌ دومًا وتنتظر إعادة توليد العرض عند أيّ تغيير في الحالة، فقد يتمّ إعادة ترتيب قائمتك قبل أن تنتهي من تعديل العنصر حتّى. <table class="table table-condensed" ng-controller="ItemsController"> <tr ng-repeat="item in items | orderBy:'name'"> <td ng-bind="item.name"></td> <td><input type="text" ng-model="item.name"></td> </tr> </table> جرّب تعديل اسم أحد العناصر في المثال السّابق بحذف المحتوى أوّلًا ثمّ كتابة الأحرف، وستلاحظ مشكلةً كبيرةً في الاستخدام. فـAngular تقوم بإعادة توليد الجدول كاملًا، ونقل السطر إلى مكانٍ آخر قبل أن تنتهي من كتابته. أفضل الحلول لهذه المشكلة قد يكون استخدام عرضٍ(view) آخر منفصلٍ للتّعديل، كما يمكنك نقل المُرشّح orderBy من العرض إلى المتحكّم حيث يتمّ استدعاؤه مرّةً واحدة، وهذا ما سنتكّلم عنه في الفقرة القادمة، استخدام المرشحات في JavaScript. limitToالمُرشّح limitTo مفيدٌ عندما ترغب بعرض مجموعةٍ جزئيّةٍ فقط من مجموعةٍ مرتّبة. مثلًا، يمكننا استخدامها بالتّعاون مع المُرشّح orderBy لنعرض فقط أغلى عنصرين في المجموعة. <ol ng-controller="ItemsController"> <li ng-repeat="item in items | orderBy:'-price' | limitTo:2"> {{item.price | currency}} </li> </ol>الناتج: 1. $3.09 2. $2.75لا تُقدّم Angular طريقةً لتمرير وسيط offset إلى المُرشّح limitTo حاليًّا. ولتقديم حلٍّ كاملٍ للتصحيف (pagination) سيتضمّن ذلك استخدام التّابع slice داخل المتحكّم. filterاسم هذا المُرشّح ظريفٌ نوعًا ما، فهو مرشّحٌ اسمه filter، ربّما لم يكن من المناسب تسمية المُرشّحات بهذا الاسم العام (ربّما يكون الاسم مزخرِفات أو مساعِدات أفضل)، إلّا أنّ هذا المرشّح يقوم فعلًا بالتّرشيح. فهو يقوم بإعادة مصفوفةٍ تحوي فقط على العناصر التي تطابق الشرط المُسند. لنرى ما هي العناصر التي ستطابق وضعنا لشرطٍ يقول بأنّ القيمة العدديّة تساوي 3. <table class="table table-condensed" ng-controller="ItemsController"> <thead> <tr><th>name</th><th>color</th><th>price</th></tr> <thead> <tbody> <tr ng-repeat="item in items | filter:3"> <td ng-bind="item.name"></td> <td ng-bind="item.color"></td> <td ng-bind="item.price | currency"></td> </tr> <tbody> </table>لقد حصلنا على تطابقات في كلا العنصرين name وprice. ماذا سيحدث لو استبدلنا الشرط 3 بالسّلسلة النّصية '3' أو بسلسلة نصّيّة خالية؟ أو لو أضفنا فراغًا في البداية ('3 ')؟ لتجنُّب مطابقة أيّ عنصرٍ، يمكنك تمرير كائنٍ باعتباره شرط التّطابق. <table class="table table-condensed" ng-controller="ItemsController"> <thead> <tr><th>name</th><th>color</th><th>price</th></tr> <thead> <tbody> <tr ng-repeat="item in items | filter:{price: 2, color: 'red'}"> <td ng-bind="item.name"></td> <td ng-bind="item.color"></td> <td ng-bind="item.price | currency"></td> </tr> <tbody> </table>في المثال السّابق، لم يتمّ مطابقة 2 مع item 2 لأننا حدّدنا اسم عنصر التّطابق بأنّه price. أكثر ما يجعل المُرشّح filter رائعًا، هو السهولة الفائقة في دمجه مع الربط ثنائيّ الاتّجاه في Angular، لإنتاج صندوق ترشيحٍ قويّ. Filter: <input type="text" ng-model="q"> <table class="table table-condensed" ng-controller="ItemsController"> <thead> <tr><th>name</th><th>color</th><th>price</th></tr> <thead> <tbody> <tr ng-repeat="item in items | filter:q"> <td ng-bind="item.name"></td> <td ng-bind="item.color"></td> <td ng-bind="item.price | currency"></td> </tr> <tbody> </table>إن كان هناك مثالٌ واحدٌ يبيّن بأفضل طريقةٍ مرونة وقوّة امتدادات HTML التي توفّرها Angular، فقد يكون هذا هو. استخدام المرشحات في JavaScriptتحتاج أحيانًا إلى استخدام مُرشّح ما عندما يكون لديك القوّة الكاملة لـJavaScript، بدلًا من أن تكون محدودًا بما تسمح لك العبارات بالقيام به. مثلًا، أمرٌ بسيطٌ كمعرفة عدد العناصر التي أنتجها المُرشّح، لا يبدو ممكنًا. ربما يمكنك أن تقول بأنّه عندما تصبح العبارة كبيرةً وتحتاج إلى منطقٍ برمجيٍّ فمن الأفضل نقل شيفرتها إلى داخل متحكّم. <p ng-controller="ItemsController"> <!-- Invalid code! --> {{items | filter:{color:'red'}.length}} red items </p>الناتج: [{"name":"Item 3","position":3,"color":"red","price":2.75},{"name":"Item 1","position":1,"color":"red","price":0.92},{"name":"Item 4","position":4,"color":"blue","price":3.09},{"name":"Item 2","position":2,"color":"red","price":1.95}] red itemsيُمكنك حقن المُرشّحات أينما أردت، كما أنّه ليس من الغريب استخدام المُرشّح filter للتعامل مع مصفوفةٍ داخل متحكّم. يكون اسم التّابع الذي يقوم بالترشيح من الشكل filtername>Filter>. مثلًا لاستخدام المُرشّح date نقوم بحقن dateFilter، لاستخدام المُرشّح currency نقوم بحقن currencyFilter، ولاستخدام المُرشّح filter نقوم بحقن filterFilter. (أعرف ذلك، أعرف ذلك) angular.module('app') .controller('FilteredItemsController', function($scope, items, filterFilter) { $scope.redItemsCount = filterFilter(items, {color: 'red'}).length; });<p ng-controller="FilteredItemsController"> {{redItemsCount}} red items </p>الناتج: 3 red itemsما عليك إبقاؤه في ذهنك بشكلٍ أساسيّ هو أنّ استخدام المُرشّح داخل المتحكّم بدلًا من أن يكون في العرض، سيؤدي إلى عدم استدعاء المُرشّح عند حدوث تغييرات في واجهة المستخدم إن كانت تغييراتٍ لا تتضمّن إعادة استدعاء المتحكّم. ولحلّ هذه المشكلة يمكنك ربط استخدام المُرشّح يدويًّا بعنصرٍ في المجال باستخدام التابع scope.$watch. مرشح مخصصالمُرشّح المخصّص هو أفضل مكانٍ لتضع فيه الشيفرات التي تقوم بتحويل بيانات النّموذج للعرض، وذلك لأنّ المُرشّح نموذجيًّا مكوِّنٌ ثابت الحالة (stateless) ويُمكن استخدامه في أيّ مكانٍ داخل القوالب دون خوفٍ من أيّ تأثيراتٍ جانبيّة، كما أنّ إنشاء مُرشّح مخصّص ليس صعبًا، فبشكلٍ مشابهٍ لبقيّة المكوّنات في Angular، نقوم بتسجيل وصفةٍ للمُرشّح داخل الوحدة، باستخدام التّابع filter. angular.module('app') .filter('smiley', function() { return function(text) { return '\u263A ' + text + ' \u263A'; }; });في المثال السّابق، المُرشّح الفعليّ هو التابع الدّاخليّ الذي يقوم بوصل السّلسلة النّصية الممرّرة إليه عن طريق الوسيط text مع رمز الوجه الضاحك. يتم استدعاءُ هذا التابع في كلّ مرّة يتمّ فيها استدعاء المُرشّح، أمّا التابع الخارجيّ الذي يحيط بالمُرشّح فهو وصفة (recipe) إنشائية، وقد ناقشنا هذه الفكرة بعمقٍ في الفصل السّابق، الخدمات. يتمّ تشغيل الوصفة مرّةً واحدةً، أو لا يتمّ تشغيلها أبدًا إن لم يكن لها استخدامٌ في العرض. المُرشّحات المخصّصة تكون متوفّرة تلقائيًّا في أيّ مكانٍ في العرض، ويتمّ استخدامها تمامًا مثل المُرشّحات الأصليّة في المكتبة، حيث نقوم ببساطةٍ بإدخال العبارة فيأنبوب الترشيح داخل العرض، وذلك باستخدام رمز الأنبوب (|)، ويتم تمرير قيم الوسطاء الإضافيّة باستخدام رمز النّقطتين (:). <strong ng-bind="'hello' | smiley"></strong>الناتج: ☺ hello ☺إليك هذا التّحدّي: هل يمكنك إعادة تشكيل المُرشّح بحيث يصبح بإمكاننا تمرير رمز الوجه الضّاحك كوسيط؟ حقن التبعيةبالرغم من كون المُرشّحات ثابتة الحالة وبسيطةً نوعًا ما، إلّا أنّها يمكن أن تملك تبعيّاتٍ تحتاج إلى أن تُحقن. يسمح تابع الوصفة الإنشائيّة الخارجيّ بالتّصريح عن التّبعيّات تمامًا كما نقوم بذلك في الوحدات. فلنلقِ نظرةً على الكيفيّة التي نقوم بها بحقن التّبعيّات في داخل مُرشّح مخصّص للمحلّيّات (localization). سنقوم بتسمية المُرشّح بالاسم localize، ولكن قبل ذلك نحتاج إلى شيءٍ ما لنقوم بالحقن. يمكننا إنشاء جدول لمحتوى المتغيّر locales، حيث يتمّ تعريفها كخدمةٍ قابلةٍ للحقن باستخدام التّابع value. angular.module('app') .value('locales', { 'de': {greeting: 'guten Tag'}, 'en-us': {greeting: 'howdy'}, 'fr': {greeting: 'bonjour'} });يمكننا حقن هذه البيانات في داخل مُرشّحنا البسيط المخصّص بالتّصريح عن وسيطٍ اسمه locales، ولنتمكّن من الحصول على الموضع الحاليّ للمستخدم، يُمكننا استخدام الخدمة locale$ المبنيّة داخل Angular . angular.module('app') .filter('localize', function(locales, $locale) { return function(key) { var locale = locales[$locale.id] || locales['en-us']; return locale[key]; }; });سيعمل المثال السّابق معك فقط إن كانت إعدادات جهازك المحلّيّة من ضمن الأماكن المحدّدة في المثال، إن لم يكن موضعك موجودًا فقم بإضافته بنفسك مع عبارةٍ ترحيبيّة بلغتك. <p> The app says <strong ng-bind="'greeting' | localize"></strong>. </p>الناتج: The app says howdy.يُمكنك أيضًا أن تقوم بحقن المُرشّحات في داخل مُرشّح آخر. يمكنك نقل الوسطاء الكثيرة وسلاسل المُرشّحات الطويلة إلى مُرشّح مخصّص، وهذه طريقةٌ رائعةٌ لإزالة بعض الضجّة من شيفرة العرض. خاتمةالمُرشّحات المبنيّة في Angular جزءٌ هامٌّ من قدراتها الرائعة، كما أنّ استخدامها سهلٌ وبديهيّ بشكلٍ عامٍّ، كما أنّها مرنة بفضل الوسطاء الإضافيّة وسَلسلة المُرشّحات، وأيضًا فإنّ إنشاء مرشّحٍ مخصّص سيكون سهلًا فورَ إتقانك لنمط الوصفة الإنشائيّة (creation recipe) التي تسمح لك بحقن التّبعيّات. والآن بعد أن اعتدنا استخدام هذه الأنماط، فنحن الآن جاهزون للتّحدّي في الفصل القادم، حيث سنقوم بإعداد التّوجيهات الخاصّة بنا. ترجمة وبتصرّف للفصل التاسع من كتاب: Angular Basics لصاحبه: Chris Smith.
-
تعاملنا حتّى الآن مع نوعٍ واحد من مكوّنات JavaScript المعدّلة في Angular، وهو المتحكّم. المتحكّمات هي نوعٌ مخصّص من المكوّنات، وكذلك سنجد المرشحات والتوجيهات، التي سنغطّيها قريبًا. تدعم Angular مكوّنًا غير مخصّص، يُسمّى بالخدمات، تعريف الخدمات في Angular فضفاضٌ نوعًا ما، ولكنّ السّمة المميّزة لها هي عدم اقترانها مباشرةً بالقالب، مما يفترض قرابةً بينها وبين نمط طبقة الخدمة في الهيكليّة التّقليديّة للمشاريع. تتضمّن وحدة القلب ng في Angular عددًا من الخدمات المدمجة، ونذكر الخدمات location$ وlog$ وq$ وwindow$ على سبيل المثال، وسنستكشف الخدمة http$ في الفصل الأخير من هذه السلسلة، فصل HTTP. في تطبيقٍ نموذجيٍّ ستحتاج إلى تعريف خدماتك الخاصّة لأيّ سلوكٍ مشترك بين مكوّنات JavaScript المخصّصة التي تنشئها، مثل المتحكّمات. يُمكن للخدمات أن تكون متابعةً للحالة (stateful) إن احتجت إلى ذلك، فمثلًا، إن أردت مكانًا لتخزين نقاط اللاعب في لعبةٍ ما، يُمكنك إنشاء خدمة score لتتمكّن من جلب وعرض النّقاط الحاليّة في عدّة أماكن ضمن تطبيقك. جميع الخدمات متفرّدة (singletons) أي أنه لا يوجد غير نسخةٍ واحدةٍ من خدمةٍ معيّنة طوال دورة حياة تطبيق Angular. الخدمة في Angular يُمكن أن تكون كائنًا، تابعًا، أو حتّى قيمةً أوّليّة (كالأعداد مثلًا)، أنت من يحدّد ذلك. في الواقع، هذا الفصل لن يشرح إلا القليل من الأمثلة عن الخدمات، وذلك لأنّ الخدمات يمكن أن تحوي أيّ شيفرة من شيفرات JavaScript العاديّة. ما سيُغطّيه هذا الفصل هو كيفيّة تسجيل (register) الخدمات في Angular. نقوم بذلك عن طريق توابع معرّفة بواسطة الخدمة provide$، ومغلّفة بواسطة angular.Module. لنتمكّن من تسجيل الخدمة نحتاج إلى مرجعٍ (reference) إلى وحدةٍ ما. لنقُم بتعريف وحدةٍ جذريّة لتطبيقنا الآن. angular.module('app', []);كما تمّ الشّرح في فصل الوحدات، نقوم بتحميل وحدة الجذر في تطبيقنا عن طريق تمرير اسمها إلى التّوجيه ng-app ضمن مستند HTML. <body ng-app="app"> <!-- الأمثلة توضع هنا --> </body>جيّد، أصبحت وحدتنا جاهزة. تُقدّم الخدمة provide$ خمس توابع مختلفةً للقيام بتسجيل الخدمة، يُمكننا تصنيف هذه التّوابع إلى صنفين، أوّلهما سهلٌ جدًّا والثّاني أصعبُ قليلًا. إنشاء خدمات دون استخدام حقن التبعيةقد لا تحتاج إلى حقن أيّ تبعيّاتٍ إلى خدمتك. (قُمنا بتغطية حقن التّبعيّة في الفصل الماضي. وكمثالٍ على تبعيّةٍ محقونة، فالخدمة المدمجة http$ يُمكن لخدمتك أن تستخدمها لجلب بياناتٍ من النّهاية الخلفيّة (backend) البعيدة.) ولكن حتّى لو لم تكن تريد حقن تبعيّاتٍ في خدمتك، ستحتاج إلى وضعها ضمن وحدةٍ لتجعلها متاحةً للحقن ضمن مكوِّناتٍ أخرى. إنّ جعل الخدمة متاحةً للحقن هو ما يجعلها خدمةً في Angular، وإلّا ستكون مجرّد شيفرة JavaScript عاديّة. هناك تابعان خدميّان يسمحان بتسجيل الخدمات من دون تبعيّات: التّابع value والتّابع constant. الفرق بينهما في Angular معقّدٌ قليلًا ويخُصّ المحترفين، فـالثّوابت تكون متاحًةً لتطبيق Angular أثناء الـbootstrapping، أما القِيمة فلا. لن نقوم بتغطية constant في هذه السلسلة. التابع valueلنفترض أنّنا نريد كتابة شيفرةٍ للعبة حيث يحصل فيها اللاعب على نقاطٍ تبدأ من الصفر. يمكننا باستخدام التّابع value من الخدمة provide$ أن نقوم بتسجيل (register) قيمةٍ عدديّةٍ أوّليّة باسم 'score' وستكون متاحةً للمتحكّمات، الخدمات، ومكوّناتٍ أخرى. angular.module('app') .value('score', 0);يُمكننا حقن خدمتنا score عن طريق وضعها ضمن قائمة الوسطاء للمتحكّم، وقد شرحنا ذلك بالتّفصيل في فصل حقن التّبعيّة. angular.module('app') .controller('ScoreController', function($scope, score) { $scope.score = score; $scope.increment = function() { $scope.score++; }; });يستخدم قالب هذا المثال نُسختين من المتحكّم ScoreController لإثبات الطّبيعيّة المتفرّدة (singleton) للخدمة score. <p ng-controller="ScoreController"> Score: {{score}} </p> <p ng-controller="ScoreController"> <button ng-click="increment()">Increment</button> </p>أعتذر، لقد تعمّدت إفشال المثال السّابق، فعند النّقر على زرّ Increment لن يتمّ تغيير قيمة المتغيّر score المعروضة في النّسخة الأولى من المتحكّم. هذه ليست مشكلة مجالات، ولكنّها بسبب الطّبيعة الثّابتة (immutable) للخدمات التي تمثّل قيمًا أوّليّة. يُمكننا إصلاح المثال السّابق بتغيير الخدمة من قيمةٍ أوّليّة إلى كائنٍ يحوي العنصر points. angular.module('app') .value('score', {points: 0});سنستخدم نفس جسم المتحكّم تقريبًا، إلّا أنّنا سنغيّر ++scope.score$ إلى ++scope.score.points$. angular.module('app') .controller('ScoreController', function($scope, score) { $scope.score = score; $scope.increment = function() { $scope.score.points++; }; });بطريقةٍ مماثلة، سنغيّر في القالب العبارة score إلى score.points. <p ng-controller="ScoreController"> Score: {{score.points}} </p> <p ng-controller="ScoreController"> <button ng-click="increment()">Increment</button> </p>لقد استخدمنا للتّو خدمةً تشارك بياناتٍ متغيّرة (mutable) ضمن تطبيقنا، هذا رائع. إضافةً إلى القيمة الأوّليّة والكائنات، يمكن لخدمات Angular أن تمثّل توابعًا. لنفترض أنّنا بحاجةٍ إلى خدمةٍ ثابتةٍ (stateless) تُعيد عدًدا عشوائيًّا بين 1 و 10. angular.module('app') .value('randomScore', function() { return Math.ceil(Math.random() * 10); });إنّ حقن هذه الخدمة أمرٌ سهل، سنقوم ببساطةٍ بإضافة اسمها randomScore إلى قائمة الوسطاء في متحكّمنا. angular.module('app') .controller('ScoreController', function($scope, score, randomScore) { $scope.score = score; $scope.increment = function() { $scope.score.points += randomScore(); }; });لقد تعلّمنا الآن كيفيّة تعريف الخدمات وحقنها ضمن المتحكّم كتبعيّات. ولكن ماذا لو أردنا حقن تبعيّاتٍ إلى خدماتنا؟ إنشاء خدمات باستخدام حقن التبعيةلعبتنا تبدأ دومًا بنقاطٍ قيمتها صفر، هذا منطقيٌّ، ولكن لنفترض أننا نريد أن تكون القيمة البدائيّة عددًا عشوائيًّا. كيف يمكننا جلب مرجعٍ للخدمة randomScore عندما ننشئ الكائن score للمرّة الأولى؟ إنّ التّابع value الذي كُنّا نستخدمه بسيط جدًّا، فأيّ شيءٍ نقوم بتمريره إليه سيكون هو القيمة أو التّابع أو الكائن الكامل والنّهائي الذي ستقوم Angular بحقنه لاحقًا، وهذا يعني بأنّنا لن نحظى بأيّ فرصةٍ لحقن أيّ تبعيّاتٍ لاحقًا. تُقدّم Angular عدّة حلول لهذه المشكلة، وسنبدأ بالحلّ الأوّل، التّابع service كائنيُّ التّوجّه. التابع serviceتدعم JavaScript أسلوب البرمجة كائنيّة التّوجّه، ولذلك يمكننا كتابة خدمة في Angular تقبل التّبعيّات عن طريق الحقن بواسطة الباني (constructor injection). كُلّ ما نحتاج إليه هو كتابة التّابع الباني لخدمتنا score، بدلّا من تهيئتها بقيمةٍ بدائيّة عن طريق مهيِّئ الكائن ({}) الذي استخدمناه قبل قليل. يجب أن يكون أوّل حرفٍ من اسم الباني في JavaScript حرفًا كبيرًا، ولذلك سنُسمّي الباني بالاسم Score، وسنقوم بوضع الخدمة randomScore ضمن قائمة الوسطاء لهذا الباني. function Score(randomScore) { this.points = randomScore(); }التّابع service يحتاج إلى تمرير الباني الخاصّ بالخدمة بدلًا من تمرير الخدمة ذاتها، وعندما تقوم Angular باستدعاء الباني عن طريق العمليّة new ستقوم آليّة حقن التّابعيّة بإسناد الخدمة randomScore إلى وسيط الباني. angular.module('app') .service('score', Score);توصف طريقة إنشاء نُسخة الخدمة في Angular بأنّها كسولة، أي أنّ النّسخة ستُنشأ فقط عندما تُشكِّل تبعيّةً لأحد المكوّنات التي يتمّ إخراجها في القالب. التابع factoryإن كانت خدمتك شيئًا آخر غير الكائن، أو إن أردت الحصول على مرونةٍ أكبر من طريقة إنشاء بانٍ للكائن، عندها يُمكنك استخدام التّابع factory بدلًا من service، حيث نقوم بتمرير تابع استدعاءٍ خلفيٍّ (callback) وسيتمّ حقنه لاحقًا بالتّبعيّات التي نكتب أسماءها في قائمة الوسطاء. يُمكنك كتابة ما تشاء داخل هذا التّابع، ولكن يجب عليك في النّهاية أن تعيد قيمةً، تابعًا أو كائنًا يُمثّل الخدمة. يُطلق على هذا النّوع من الإنشاء عن طريق الاستدعاء الخلفيّ في المرجع الرّسميّ اسمُ الوصفة(recipe). angular.module('app') .factory('score', function(randomScore) { return {points: randomScore()}; });المثال السّابق مكافئٌ للمثال الذي يستخدم تركيبة الباني مع التّابع service الذي رأيناه في الفقرة الماضية. التابع decoratorإن كنت تريد تعديل خدمةٍ موجودةٍ سابقًا، يُمكنك استخدام التّابع decorator الّذي تُوفّره الخدمة provide$. هذا التّابع ليس مُغلّفًا باستخدام angular.Module كحال بقيّة التّوابع، لذا يجب أن يتمّ استدعاؤه مباشرةً من الخدمة provide$. يُمكنك الحصول على مرجعٍ للخدمة provide$ عن طريق تسجيل تابع استدعاءٍ خلفيٍّ (callback) باستخدام التّابع config. angular.module('app') .config(function($provide) { $provide.decorator('score', function($delegate) { $delegate.points = 1000000; return $delegate; }); });يُمكن لتابع decorator أن يقوم بإعادة الخدمة الأصليّة التي تمّ تمريها ضمن الوسيط ذي علامات الاقتباس ' ' أو أن يقوم بإعادة نُسخةٍ (instance) من خدمةٍ جديدةٍ كُلّيًّا. ولكن عليك بالحذر من التّأثيرات الجانبيّة غير المرغوبة. التغليف (Encapsulation)في الفقرة السّابقة أسأنا استخدام التّابع decorator، ونتيجةً لذلك تمّ تقييد الوصول إلى العنصر points. لحسن الحظّ، يُقدّم التّغليف الخاصّ بحقن التّابع factory مجالًا مُغلقًا (closure)، ممّا يسمح لنا بإخفاء بعض المعلومات، أو بعبارةٍ أخرى، التّغليف (encapsulation). سنقوم باستبدال العنصر points المرئيّ في المجال العام بالمتغيّر المحلّي points المحدود الرؤية ضمن التّابع الّذي يغلّفه، وهذا سيسمح لنا بحمايته من التّعديل خارج التّابع. والآن سنقوم بجعل كائن الخدمة يكشف عن تابعٍ للوصول إلى قيمة المتغيّر، getPoints، وعن تابعٍ يقيَّد التعديل فيه فقط، increment. angular.module('app') .factory('score', function(randomScore) { var points = randomScore(); return { increment: function() { return ++points; }, getPoints: function() { return points; } }; });سنحتاج إلى تغيير بسيطٍ في المتحكّم كي نسمح له باستدعاء التّابع increment من الخدمة. angular.module('app') .controller('ScoreController', function($scope, score) { $scope.score = score; $scope.increment = function() { $scope.score.increment(); }; });وسنغيّر أيضًا القالب، ليكون مرتبطًا بتابع الوصول ()score.getPoints بدلًا من الوصول إلى العنصر points مباشرةً. <p ng-controller="ScoreController"> Score: {{score.getPoints()}} </p> <p ng-controller="ScoreController"> <button ng-click="increment()">Increment</button> </p>بما أنّ التابع increment يقوم أيضًا بإعادة قيمة المتغيّر points بعد التّعديل، إذًا بإمكاننا الكشف (expose) عنها ضمن العرض (view) في عبارة. قد تُفاجئك النّتيجة، قُم باستبدال الاستدعاء في السّطر الثاني، وضع ()score.increment بدلًا من ()score.getPoints، ثُمّ قُم بنقر الزّر عدّة مرّات. هل يُمكنك معرفة سبب زيادة القيمة بسرعةٍ كبيرة؟ هذا صحيح: تقوم Angular غالبًا باستدعاء العناصر والتّوابع المرتبطة بالقالب عدّة مرّات قبل أن تنتهي دورة الإخراج. هذه معلومةٌ هامّة يجب معرفتها لنفهم التّأثير الجانبيّ لها كالمثال السّابق، وأيضًا لتحسين كفاءة التّطبيق. خاتمةوصلنا إلى ختام الفصول الثّلاثة التي تتحدّث عن دعم Angular للبرمجة باستخدام الوحدات. بدأنا مع فصل الوحدات، وتابعنا مع الفصل القصير عن حقن التّبعيّة، وختمنا هذه الثّلاثيّة بهذا الفصل عن الخدمات. قد تستغرب إفراد الخدمات بفصلٍ مستقلٍّ عندما ترى أنّها ليست سوى JavaScript المعتادة ليس إلّا، ولكنّنا لم نقم بتغطيةٍ كاملة لما يجب معرفته، فإضافةً إلى التّوابع constant وvalue و service وfactory و decorator لا يزال هناك تابعٌ منخفض المستوى هو التّابع provider الذي يقدّم تعلّقات دورة الحياة (lifecycle hooks) لإعداد خدماتك بطريقةٍ متقدّمة. إن كنت تتساءل فيما إذا كنت تحتاج إلى مساعدة Angular في هذا المجال بالفعل، فلتبقِ في ذهنك بأنّك لست مضطرًّا لإدارة كلّ شيفراتك باستخدام Angular، ورغم ذلك فإنّ Angular 2.0 ستنتقل إلى نظام وحدات ES6 كما أشار العرض التّقديمي "RIP angular.module" الّذي قُدّم في خطاب فريق التّطوير في ng-europe 2014. وإلى أن يتمّ ذلك فإنّك تحتاج بالفعل إلى أن تضع شيفراتك داخل وحدات Angular عندما تحتاج إلى استخدام مكوّناتٍ مدمجة في Angular (أو مكوّناتٍ طوّرها طرفٌ ثالث) يتمُّ الوصول إليها عن طريق حقن التّبعيّة. لقد قمتُ بعرض بديلٍ لحقن التّبعيّة في نهاية الفصل الماضي. فقط أبقِ في ذهنك أنّ دعم اختبار الوحدة (unit testing support) في Angular قد عزّز من أهمّيّة استخدام حقن التّبعيّة، لذا عليك أن تهتمّ باستخدامك له أيضًا. هناك أيضًا بعض المكوِّنات المخصّصة التي يجب عليك تسجيلها باستخدام نظام الوحدات في Angular، لتجعلها متوفّرة ضمن القالب. أحد هذه المكوّنات هو المُرشّحات، وسنُغطّيها في الفصل القادم. ترجمة وبتصرّف للفصل الثامن من كتاب: Angular Basics لصاحبه: Chris Smith.
-
تدير Angular الموارد باستخدام نظام الوحدات، ولهذا السّبب فهي تحتاج أيضًا إلى تقديم طريقةٍ للوصول إلى هذه الموارد المنظّمة ضمن حاويات (container-managed)، حيث تقوم Angular بذلك باستخدام نمطٍ إنشائيّ يُدعى حقن التبعية. إن لم يكن لديك معرفةٌ مسبقةٌ بهذا المفهوم فسأقوم هنا بأفضل ما لديّ لتلخيصه لك: لنفترض أنّك تحتاج إلى الحصول على مَوردٍ (resource) من Angular، المَورد هو الحاوية. للحصول عليه يجب أن تكتب تابعًا يُعرّف وسيطًا له نفس اسم هذا المَورد بالضّبط. تقوم الحاوية باكتشاف التّابع الذي كتبته، وتتعرّف على توافق الوسيط مع المَورد المطلوب، فتقوم بإنشاء نُسخةٍ (instance) من هذا المَورد (أو تقوم بأخذ النُّسخة المنفردة (singleton) من هذا المَورد، إن كان من النّوع المنفرد) ثُمّ تقوم باستدعاء التّابع الخاص بك وجعل هذه النّسخة هي قيمة الوسيط الخاص بالتابع. بما أنّ الحاوية هنا تلعب الدّور الفعّال (على عكس الطلب الذي تقوم به للحصول على نسخة من المَورد)، لذلك يُعتبر حقن التّابعيّة عكسًا للتّحكّم (inversion of control)، ويُقدّم على أنّه تطبيق لمبدأ Hollywood، "لا تكلّمنا، نحن سوف نكلِّمُك". أعتقد بأنّ مخترعي Angular قد اختاروا طريقة حقن التّابعيّة بسبب تاريخ Google الطّويل مع منصّة Java، بيئة برمجة ساكنة يقوم فيها حقنُ التّابعيّة بدورٍ فعّال في عمليّات تغيير الأنواع أثناء وقت التّشغيل. (لقد تخطّت Google الحدود في ذلك إلى درجة أنّها أنشأت إطار عمل Java خاصٍّ بها، Guice، الذي يعتمد على حقن التّابعيّة كُلّيًّا.) أمّا في JavaScript، وهي البيئة المرنة والديناميكيّة، حيث يمكن أن تتغيّر الأنواع أثناء وقت التّشغيل، فقد حظِيَ فيها أسلوب محدّد موضع الخدمة (service locator) البسيط والمباشَر بشعبيّة هائلة عن طريق الواجهة البرمجيّة لـCommonJS، وهي عبارة require المستخدمة في Node.js وbrowserify. والأكثر من ذلك، أنّ الطريقة البدائيّة في القيام بحقن التّابعيّة قد تبيّن أنّ لها نقطة ضعفٍ عند استخدامها في JavaScript من طرف العميل، وذلك بسبب تعديل أسماء الوُسطاء باستخدام المُقصّرات (minifiers). ولنستطيع التّعامل مع هذه المشكلة، من الضّروريّ أن توجد أداة بناءٍ بديلة، أو أن نتعلّم ونستخدم حواشي التّبعيّات، وسنصل إلى ذلك بعد قليل، ولكن لنتعرّف أوّلًا على النّموذج البدائيّ. التبعيات الضمنيةلن نستخدم خارج هذا الفصل للقيام بحقن التّبعيّة غير طريقة التّبعيّات الضّمنيّة لأنّها الأسهل بين جميع الطُّرق. كما ذكرت سابقًا، يمكننا جلب مرجعٍ لأحد الموارد المتاحة عن طريق وضعه كوسيطٍ في تابعنا الإنشائيّ، ولقد قمنا بذلك للمرّة الأولى عندما أضفنا الوسيط scope$ الذي يعطي تابع التّحكّم مرجعًا إلى كائن المجال المنظّمٍ ضمن حاوية (container-managed). يقوم نظام حقن التّبعيّة في Angular بالعثور على نسخةٍ للمَورد المطلوب (أو بإنشاء نسخةٍ لهذا المَورد) وتمريره إلى المتحكّم. لنرى مرّةً أخرى كيف نقوم بذلك على المَورد المدمج في Angular، الخدمة locale$. نحتاج إلى تعريف وحدتنا app لأنّ بعض أمثلة هذا الفصل تستخدم نظام الوحدات. angular.module('app', []);وبعد ذلك سنقوم بتحميل الوحدة عن طريق تمرير اسمها إلى التّوجيه ng-app. (أكرّر التّنبيه إلى أنّ الاسم app ليس إجباريًّا ويمكن استخدام أيّ اسم آخر.) <body ng-app="app"> <!-- الأمثلة توضع هنا --> </body>نحن الآن جاهزون للانطلاق مع مثالنا الأوّل، وبطريقةٍ مماثلةٍ لإضافة الوسيط scope$ سنضيف الوسيط locale$، وستلاحظ Angular هذين الوسيطين وتقوم بحقنهما. تقوم Angular بحقن الموارد داخل التّابع لأنّ هذا التّابع يمثّلُ متحكّمًا. angular.module('app') .controller('LocaleController', function($scope, $locale) { $scope.locale = $locale.id; });داخل جسم المتحكّم قمنا ببساطةٍ بإسناد المتغيّر locale الذي يمكننا تغيير اسمه إلى أيّ اسمٍ آخر، إلى العنصر id من الوسيط locale$ الذي لا يمكننا استخدام اسمٍ آخر له. <p ng-controller="LocaleController"> Your locale is <strong ng-bind="locale"></strong> </p>الناتج: Your locale is en-usيُفترض أن ترى شيئًا مماثلًا لعبارة "Your locale is en-us" في المخرجات. ما الذي سيحدث لو أنّك أضفت الوسيط myResource إلى تابع التّحكّم؟ هل عطّلت عمل المثال؟ هل يمكنك قراءة الخطأ في الـconsole الخاصة بـJavaScript في متصفّحك؟ تعرض أخطاء Angular غالبًا رابطا مفيدا، يمكنك عن طريق هذا الرابط أن تحصل على درسٍ رائع وقصير عن حلّ مشاكل حقن التّبعيّة، انقر على الرّابط السّابق واقرأ الشرح إن أردت. الحواشي (Annotations)تمثّلُ الحاشية في لغة برمجةٍ مثل Java، معلوماتٍ إضافيّة تُضاف إلى الشيفرة المصدريّة ويتجاهلها المترجم. أمّا في سياق الحديث عن حقن التّبعيّة في Angular، فتعني كلمة الحاشية التّصريح عن الموارد التي نريد حقنها بطريقةٍ صريحةٍ تحافظ على أسماء الوسطاء قصيرةً باستخدام أسلوب التّصغير (minification). (استخدام أداة البناء UglifyJS مثالٌ على أسلوب التّصغير.) لنتابع الآن طريقة Angular التي تسمح لنا فيها بإضافة الحواشي إلى التّبعيّات. ng-annotateمن الطرق الملائمة للتخلص من إزعاج مهمّة بناء ما، استخدام مهمّة بناءٍ أخرى. ستقوم أداة البناء ng-annotate بإضافة الحواشي للتّبعيّات، ومن ثمّ ستسمح لنا بمواصلة استخدام طريقة التّبعيّات الضّمنيّة التي شرحناها في الفقرة السّابقة. ستحتاج فقط إلى تشغيل ng-annotate قبل أن تُشغّل المقصّر (minifier) الخاصّ بك. هذه الأداة المركزيّة صُمّمت لتُستخدم عن طريق سطر الأوامر، إلّا أنّه يوجد غلافٌ لها لتفادي التّعامل مع سطر الأوامر، كما يوجد غلافٌ لمعظم أدوات البناء وحزم الأدوات، من ضمنها grunt-ng-annotate وgulp-ng-annotate وbrowserify-ngannotate وng-annotate-webpack-plugin. الحاشية السطريةهناك أسلوبان لكتابة الحواشي، وأكثرهما شُهرةً يُعرف باسم الحاشية السّطريّة (inline annotation)، وهي تتضمّن وضع تابع التّحكّم داخل مصفوفةٍ كما يلي. angular.module('app') .controller('LocaleController', ['$scope', '$locale', function(s, l) { s.locale = l.id; }]);الناتج: Your locale is en-usفي المثال السّابق، قُمت بمحاكاة تأثير التّقصير عن طريق تغيير أسماء الوُسطاء من scope$ و locale$ إلى s وl بالترتيب. بالطّبع لا يمكن لـAngular أن تجد مَوردين لهما الأسماء s وl لو اعتمدنا على أسلوب إضافة الحواشي الضّمنيّ، ولكن هنا، ولأنّ المصفوفة تحوي أسماء الوسطاء الحقيقيّين ستتمكّن من حقنهم بنجاح. إضافة الحاشية بواسطة inject$إن كان قد انتابك شعورٌ بأن طريقة إضافة الحواشي السّطريّة، قبيحة، فالأرجح أنك ستجد الطريقة البديلة هذه أكثر قبحًا. فهي تعتمد على إنشاء عنصرٍ ذا اسمٍ مخصّص، inject$، في التّابع الإنشائيّ، وهذا سبب تسمية الطّريقة بهذا الاسم. var LocaleController = function(s, l) { s.locale = l.id; }; LocaleController['$inject'] = ['$scope', '$locale']; angular.module('app') .controller('LocaleController', LocaleController);الناتج: Your locale is en-usالمهمّ أنّك الآن تعرف الخيارات المتاحة للقيام بإضافة الحواشي. angular.injectorأرجو أن تكون عند هذه النّقطة قد قمت بإضافة ng-annotate إلى بُنيتك، لتتمكّن من استخدام الأسلوب البدائيّ في التّبعيّات الضّمنيّة التي ستراها في هذه السلسلة دون مشاكل. على أيّ حال، إن لم تكن قد قمت بذلك، لديّ اقتراح لك، إلّا أنّه ليس اقتراحًا جادًّا: قم بكتابة تابع require الخاصّ بك، وأفصح عن التّبعيّات الّتي تريد ضمن عمليّة البحث المباشر عن الخدمات، وذلك بدلًا من استخدام أسلوب حقن التّبعيّة. يمكنك القيام بذلك عن طريق استدعاء تابع angular.injector مباشرةً، وعليك تمرير وسيط وحيدٍ إليه هو مصفوفةٌ فيها أسماء الوحدات التي ترغب بالبحث ضمنها، ويجب عليك تمرير اسم الوحدة ng دومًا وجعلها في بداية القائمة. var require = function(name) { return angular.injector(['ng','app']).get(name); }; angular.module('app') .controller('LocaleController', ['$scope', function($scope) { var $locale = require('$locale'); $scope.locale = $locale.id; }]);الناتج: Your locale is en-usلاحظ أنّ المثال لايزال يعتمد على حقن التّبعيّة في تمرير كائن المجال الصّحيح للمتحكّم، سأسعى في الفقرة القادمة لأبيّن لك أحد أهمّ المشاكل العمليّة مع الحواشي في الأنظمة الضّخمة، وذلك خشية أن يتبادر إلى ذهنك بأنّ الاستثناء الّذي واجهناه للتّو يلغي الحاجة إلى تبنّي طريقة محدّد موضع الخدمة. مخاطرالمثال التّالي سيستبق الأحداث، ويأخذنا إلى الفصل القادم، الخدمات. سنستخدم في هذا المثال التّابع factory، ورغم أنّك لم تألف استخدامه من قبل إلّا أنّه بإمكانك تخمين وظيفته، فهو يقوم بإعداد مصنعٍ للمكوّن باستخدام نظام الوحدات. لاحظ أيضًا أنّ الخدمة http$ (التي سنناقشها في فصلٍ لاحق HTTP) ستكون خدمةً نموذجيّةً ومثالًا من الحياة العمليّة لاستخدام حقن التّبعيّة داخل خدماتٍ كهذه. angular.module('app') .factory('fullPrice', ['$http', function($http) { return function() { // Use $http to fetch remote data. return 100; } }]) .factory('discountPrice', ['$http', function($http) { return function() { // Use $http to fetch remote data. return 40; } }]);استلهمتُ المثال التّالي من تطبيق Angular حقيقيّ، حيث استُبدلت التّبعيّات بقائمةً طويلةً من الخدمات المدمجة في Angular، ولكنّ طول القائمة هو نفسه تقريبًا. angular.module('app') .controller('PriceController', ['$scope', '$anchorScroll', '$animate', '$cacheFactory', '$compile', '$controller', '$document', '$exceptionHandler', '$filter', '$http', '$httpBackend', '$interpolate', '$interval', 'fullPrice', 'discountPrice', '$locale', '$location', '$log', '$parse', '$q', '$rootElement', '$rootScope', '$sce', '$sceDelegate', '$templateCache', '$timeout', '$window', function( $scope, $anchorScroll, $animate, $cacheFactory, $compile, $controller, $document, $exceptionHandler, $filter, $http, $httpBackend, $interpolate, $interval, discountPrice, fullPrice, $locale, $location, $log, $parse, $q, $rootElement, $rootScope, $sce, $sceDelegate, $templateCache, $timeout, $window) { $scope.fullPrice = fullPrice(); $scope.discountPrice = discountPrice(); }]);من الواضح أنني اختصرت جسم المتحكّم PriceController وكذلك اختصرت شيفرة العرض التّالية. <table ng-controller="PriceController"> <tr> <td>Full price:</td> <td>{{fullPrice}}</td> </tr> <tr> <td>Discount price:</td> <td>{{discountPrice}}</td> </tr> </table>الناتج: Full price: 40 Discount price: 100لقد تعمّدت القيام بخطأٍ في المثال السابق، فالمفترض أن يكون السعر الكلّي أكبر دومًا من السّعر المخفّض، ولكنّهما انعكسا لسببٍ ما، والمشكلة ليست في شيفرة العرض أو في جسم المتحكّم، هل يمكنك إيجادها؟ ماذا تستنتج من ذلك بخصوص كتابة حاشية التّبعيّات يدويًّا في المشاريع الحقيقيّة؟ خاتمةهذا هو الفصل الثّاني من أصل ثلاثة فصول في هذه السلسلة تغطّي البنية التّحتيّة لـAngular الخاصّة بإدارة شيفرة التّطبيق. الفصل الأوّل، الوحدات، بيّنَ كيف سنقوم بتهيئة خشبة المسرح الّتي سيقف عليها الممثّلون ليقوم كلٌّ منهم بدوره. وفي هذا الفصل شرحنا ببعض التّفصيل الآليّات المتاحة لجعل كلِّ ممثّلٍ يقفُ في مكانه الصّحيح. أمّا الفصل الثّالث ضمن هذا المجال، الخدمات، سيشرح كيفيّة إنشاء الممثّلين أنفسهم. ترجمة وبتصرّف للفصل السابع من كتاب: Angular Basics لصاحبه: Chris Smith.
-
- 1
-
- locale
- ng-annotate
-
(و 5 أكثر)
موسوم في:
-
إنّ هدف الوحدات عمومًا هو توزيع المهام عن طريق تعريف توابع الواجهة البرمجية (API) والحدّ من رؤية السلوك (أجسام التوابع) والبيانات (العناصر والمتغيّرات). معظم المنصّات البرمجيّة تتضمّن دعمًا داخليًّا للوحدات، حتّى أصبح استخدامها أمرًا مسلّمًا به. ولكنّ JavaScript المستخدمة من طرف العميل لا تستخدم الوحدات حاليًّا، وتؤدي إلى احتدام النقاشات المليئة بين مؤيّد ومعارض للحلّ المشهور (الإضافات add-ons) مثل CommonJS و تعريف الوحدة غير المتزامن (AMD). أبقِ في ذهنك عندما تقوم بمقارنة الحلول المطروحة، أنّ نظام الوحدات في Angular لا يقوم بتحميل الوحدة. فهو لا يقوم بتحميل الشيفرة المصدرية من ملفّاتٍ أو من الإنترنت. فالحلّ الذي تقدّمه Angular هو نظام الوحدات المدمج فيها، والذي يرتبط ارتباطًا وثيقًا بنظام حقن التابعية المدمج أيضًا في Angular. معًا، يكوّن هذان النّظامان جزءًا كبيرًا من البنية التّحتيّة لـAngular. ولكن هل نحن بحاجةٍ إليها حقًّا؟ صحيحٌ أنّ بإمكان أيّ مطوّر تعلّمها، ولكن لمَ نحتاجها؟ تُشكّل الوحدات وحقن التّابعيّة كلّ بنية Angular، ولكن كما رأينا فإنّ Angular تسمح لنا بالبدء دون استخدام نظام الوحدات. حتّى الآن، كان بإمكاننا إنجاز الكثير ببساطة عن طريق تعريف المتحكمات كتوابع معرّفة في المجال العامّ. ولكنّ ذلك لم يكن ليعمل لولا القليل من الإعدادات، وهذه الإعدادات سمحت للتّوجيه ng-controller بالبحث عن توابع التّحكّم في المجال العام وأيضًا في نظام الوحدات. هذه الطريقة السهلة في استخدام المجال العام مسموحةٌ في Angular، ولكن فقط إلى هذه النقطة. لماذا نستخدم وحدات Angular؟ لابدّ لك من فهم وإتقان نظام الوحدات في Angular لتتمكّن من تجاوز الأساسيّات فيها والغوص في المفاهيم العميقة. لا تسعى هذه السلسلة لإقناعك بالأهمّيّة الكامنة في الوحدات وفي حقن التّابعيّة في Angular، فعندما تواجهك مشكلة إدارة التّعقيد في تطبيقك فالأمر يعود إلى تجربتك الشّخصيّة في أفضل الممارسات البرمجيّة، والحلّ الأبسط سيكون الأفضل. سيغطّي هذا الكتاب الوحدات في Angular ومبدأ حقن التّابعيّة بما يشمل الجزء الأهمّ منها في Angular. لنبدأ بنظرةٍ عامّة إلى المزايا الهامّة التي تدفعنا لاستخدام نظام الوحدات: مكوّنات مخصّصة - لتعريف المتحكّمات الخاص بك، التّوجيهات، المرشّحات، والمؤثّرات الخاصّة، لا بدّ من استخدام نظام الوحدات.(يمكننا استثناء المتحكّمات كما أشرنا سابقًا.) حقن التّابعيّة - الخدمات ليست إلّا كائنات وتوابع JavaScript عاديّة، إلّا أنّ الخدمات التي يتمّ إنشاؤها في نظام الوحدات يمكن أن تُحقن بسهولة مع التّبعيّات الخاصّة بها، وذلك إن كانت تتيح حقن التّابعيّة فيها. الوحدات الخارجيّة - هناك نظامٌ بيئيّ مثيرٌ للاهتمام، مجّانيٌّ ومفتوح المصدر من الإضافات لـAngular، وقد قام بكتابة بعض هذه الإضافات فريق تطوير قلب Angular وكذلك المطوّرون كطرفٍ ثالث. لاستخدام أيٍّ من هذه المكاتب في تطبيقك لا بُدّ من استخدام نظام الوحدات. إعدادات وقت-التّحميل - يسمح نظام الوحدات في Angular بالوصول إلى تعلّقات دورة الحياة (lifecycle hooks) المتعلّقة بالإعدادات الدّاخليّة لـAngular، وكذلك المتعلّقة بإعدادات الوحدات المضافة، وسنتطرّق لها في نهاية الفصل الأخير، حيث سنقوم بإعداد رأس HTML مخصص. الاختبار - إنّ البنية التّحتيّة الخاصّة بالاختبار في Angular تبيّن فاعليّة نظام حقن التّابعيّة والّذي يعتمد بدوره على نظام الوحدات. سيبيّن فصلٌ لاحقٌ، فصل الخدمات، كيف نقوم بتعريف مقدم لمكوّنات JavaScript المخصّصة. سنرى في هذا الفصل كيف نعرّف متحكّمًا، وسنقوم أيضًا بتحميل وحدةٍ خارجيّةٍ، وأثناء تنفيذ ذلك سنتعلّم أساسيّات نظام الوحدات في Angular. إنشاء وحدة التطبيق يمكن تهيئة تطبيق Angular باستخدام وحدةٍ جذريّة واحدة فقط. سنقوم بتسمية الوحدة في أمثلة هذا الفصل بالاسم app ويمكنك اختيار أيّ اسمٍ آخر إن أردت، إلّا أنّ هذا الاسم شائع الاستخدام في مرجع Angular. angular.module('app', []); أرجو أن تركّز على الوسيط الثاني للتابع module في مثالنا السابق، ورغم أنّه يبدو فارغًا، وله مظهر مصفوفة بريئة خالية ([]) إلّا أنّ وجود هذا الوسيط الثاني شديد الأهمّيّة، فبدونه سيتغيّر سلوك التّابع angular.module كليا، وسأشرح لك سلوك هذا التابع كما يلي: عند استدعاء التابع angular.module مع وجود الوسيط الثاني، سيعمل التّابع في نمط الإنشاء (create)، وسيقوم بإنشاء وحدةٍ ويسمّيها app إن لم يكن هناك وحدة بنفس الاسم سابقًا، أمّا عند استدعائه دون الوسيط الثّاني فسيعمل في نمط البحث (lookup)، فإن عثر على وحدةٍ بالاسم المعطى سيقوم بإعادتها، أمّا إن لم يجدها فسيقوم برمي خطأٍ يطلب تمرير الوسيط الثاني. قمنا في المثال السابق بالاستخدام الصحيح للتابع، ولكن كن حذرًا دومًا من الفرق بين الطريقتين في استدعاء هذا التّابع, أمّا بالنّسبة لهذه المصفوفة التي تمرر كوسيط، فهي تُستخدم لجلب الوحدات الخارجية التي سنناقشها في هذا الفصل، باستخدام وحدة animation في أمثلتنا. تحميل وحدة التطبيق في مستند HTML الذي يستضيف التّطبيق، يمكننا أن نأمر Angular بتحميل وحدة هذا التّطبيق عن طريق تمرير اسمه إلى التّوجيه ng-app. <body ng-app="app"> <!-- الأمثلة توضع هنا --> </body> ستكون جميع المكوّنات التي نضيفها إلى وحدة التّطبيق متاحةً للاستخدام. لنرى الآن كيف نقوم بتعريف متحكّم داخل وحدةً بدلًا من تعريفه في المجال العام. تعريف متحكم تتضمّن الواجهة البرمجية للعنصر module توابع لتعريف مكوّنات Angular المخصّصة. والمكوّن الوحيد الذي تعرفنا عليه سابقًا هو المتحكّم، لذا لنقم باستخدام التابع controller لإنشاء متحكّم. سنحتاج أوّلًا إلى جلب مرجعٍ لوحدة التّطبيق، وكما ذكرنا سابقًا، يجب أن نقوم باستدعاء التابع angular.module بدون تمرير الوسيط الثاني، وذلك كي يعمل في نمط البحث lookup. var app = angular.module('app'); app.controller('MessageController', function($scope) { $scope.message = "This is a model."; }); والآن، في مكان ما ضمن العنصر الذي قمنا فيه باستدعاء الوحدة عن طريق "ng-app="app، يمكننا استخدام التّوجيه ng-controller لتحميل المتحكّم. <p ng-controller="MessageController"> {{message}} </p> الناتج This is a model. وكما يمكنك أن ترى، فتعريف المتحكّم داخل الوحدة يحتاج إلى عمل أكثر بقليل من تعريفه في المجال العام، ولكنّه ليس بهذه الصعوبة أبدًا. سلسلة التعريفات لنفترض بأننا نريد تعريف متحكّمين للقالب التالي. <p ng-controller="MessageController"> {{message}} </p> <p ng-controller="AnotherMessageController"> {{message}} </p> إنّ توابع التعريف ضمن العنصر Module قابلةٌ للسَّلسَلة، حيث يمكننا إنشاء متحكّمين باستخدام عبارةٍ واحدة. angular.module('app') .controller('MessageController', function($scope) { $scope.message = "This is a model."; }) .controller('AnotherMessageController', function($scope) { $scope.message = "This is another model."; }); الناتج This is a model. This is another model. لاحظ أن الاستدعاء الأول للتابع controller لم تتبعه فاصلةٌ منقوطة. إن لم تعجبك هذه الطريقة في سَلسَلة الاستدعاءات، يمكنك استدعاء التابع module كلّما أردت الحصول على مرجعٍ للوحدة، أو يمكنك تخزين هذا المرجع في متغيّر، كما يبيّن المثال التالي. عند استخدامك لمتغيّر، سيكون من الجيّد أن تضعه ضمن تابعٍ يُستدعى آنيًّا (IIFE) أو أيّ مجالٍ مغلقٍ آخر، لمنع ذلك المتغيّر من أن يُنشأ في المجال العام. var app = angular.module('app'); app.controller('MessageController', function($scope) { $scope.message = "This is a model."; }); app.controller('AnotherMessageController', function($scope) { $scope.message = "This is another model."; }); يمكنك اختيار أيٍّ من الطريقتين، ولكنّ طريقة السَّلسلة هي الأكثر شيوعًا. تحميل الوحدات يُعدّ التحريك (animations) ميزةً جديدةً في Angular تمّت إضافتها إلى حزمة منفصلة اسمها ngAnimate. الخطوة الأولى لاستخدام التحريك هي تضمين ملف JavaScript المحتوي على الوحدة. الشيفرة المصدريّة للوحدة جزءٌ من قلب Angular، ولكنّها موجودةٌ في ملفٍّ منفصل، كما نرى أدناه. <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular-animate.js"></script> حان الوقت لنستخدم الوسيط الثّاني للتابع module كما يجب، فبدلًا من تمرير مصفوفةٍ خاليةٍ فيه سنقوم الآن بالتصريح عن الوحدات المتعلّقة بوحدتنا. angular.module('app', ['ngAnimate']); لقد قمنا الآن بتحميل ngAnimate إلى داخل وحدة التّطبيق، والآن ستقوم التّوجيهات ng-show و ng-hide و ng-class و العديد من التّوجيهات الأخرى بالتّحقّق من التحريك الخاصّ بـCSS وJavaScript أيضًا الذي يمكن تطبيقه على أحداث المستند مثل add و enter و leave و move و remove، وتطبيق هذا التّحريك شفّاف فهو لا يتطلّب أيّ تغيير على شيفرة القالب المكتوبة، وإن لم نكن قد طبّقنا هذا التّحريك سواء على شكل CSS أو كـJavaScript عن طريق تسجيل الحركة عبر module.animation، فإنّ الشيفرة ستسلك السّلوك الافتراضيّ لها بشكل طبيعيّ. <input type="checkbox" ng-model="showMessage"> Check the box to show the message <h2 ng-show="showMessage"> The secret message. </h2> بالنّقر على الـcheckbox ستقوم ng-show بعملها كالمعتاد، وسيتمّ التّبديل بين إظهار وإخفاء العنصر بشكلٍ فوريّ دون تدرّج أو تأثير اختفاءٍ بسيط، إلى أن نكتب شيفرة التّحريك في CSS أو JavaScript. في المثال التّالي سنستخدم CSS، وهي عمومًا الخيار الأفضل للتحريك بسبب الأسلوب التصريحي في كتابتها. يمكنك على أيّ حالٍ استخدام التابع animate في jQuery ليقوم بالمهمّة إن وجدت ذلك أسهل، أما Angular فتقوم بذلك عن طريق الوحدة module.animation. يتطلّب التّحكّم بتأثير الاختفاء التّدريجيّ استخدام أربع فئات CSS تقوم بالتّعلّق (hook)، وذلك حسب توثيق الواجهة البرمجية الخاصّ بالتّوجيه ng-show. هذه الفئات الأربعة هي ng-hide-add و ng-hide-add-active و ng-hide-remove و ng-hide-remove-active. .ng-hide-add, .ng-hide-remove { transition: all linear 1s; display: block !important; } .ng-hide-add.ng-hide-add-active, .ng-hide-remove { opacity: 0; } .ng-hide-add, .ng-hide-remove.ng-hide-remove-active { opacity: 1; } قد تحتاج إلى إضافة خصائص محدّدة من قبل المصنّع لبعض المتصفّحات حسب زوّار موقعك، مثل webkit-transition- الخاص بمتصفّح سفاري 6 لأنظمة iOS. (يُعدّ موقع caniuse.com مرجعًا جيّدًا لتوافق المتصفّحات مع HTML5 و CSS3.) أرجو أن تنتبه أيضًا إلى أنّ هذا التّحريك سيتمّ تطبيقه في كلّ الأماكن التي ستستخدم فيها ng-show و ng-hide في تطبيقك. يمكنك إضافة فئة CSS مخصّصة لتزيد قدرتك على التّحكّم، وذلك للعنصر ولمحدّد CSS الذي تتعامل معه، مثلًا يمكنك كتابة my-class.ng-hide-add. و my-class.ng-hide-remove. وهكذا. خاتمة أتمنى أن تكون سهولة إضافة ngAnimate قد أقنعتك بأهمّية فهم نظام الوحدات في Angular. ومن المفرح أن تعلم بأنّ ngAnimate ليست إلّا البداية ضمن الكثير من الوحدات المُضافة إلى Angular. يمكنك أن تختار وحداتٍ أخرى متاحةّ بشكلٍ مجّانيّ من هذا النّظام البيئيّ سريع النموّ الذي يعتمد على إبداعات المطوّرين من مواقع مثل GitHub، وذلك إضافةً إلى الوحدات التي تتيحها Angular أصلًا مثل ngResource وngRoute. أحد أكثر المشاريع شُهرةً هو AngularUI، الذي يحوي وحداتٍ عظيمة الأهمّيّة مثل UI Router وUI Bootstrap، ومعظم هذه الوحدات يمكن تحميله بسهولة في مشروعك باستخدام Bower، مدير حزم الويب، ولربّما ستبدأ بنشر وحدات Angular الخاصّة بك مفتوحة المصدر على GitHub وBower عندما تملك زمام Angular، وهذا ما أتمنّاه. تقدّم الوحدة ngAnimate إثباتًا واضحًا للقوّة المتاحة في الوحدات المضافة، ولكنّها لا تتضمّن استخدام البنية التّحتيّة لحقن التّابعيّة في Angular بسبب أسلوبها الجزئيّ في التكامل. سنغطّي حقن التّابعيّة في الفصل القادم، وذلك لأنّه يعمل يدًا بيد مع الوحدات. ترجمة وبتصرّف للفصل السادس من كتاب: Angular Basics لصاحبه: Chris Smith.
-
في الفصل السابق، المجالات، تعلّمنا أنّ Angular تقوم بإنشاء مجالٍ جديد في كلّ مرّةٍ يتمّ فيها استدعاء الباني الخاصّ بالمتحكّم عن طريق ng-controller. هناك حالاتٌ أخرى أيضًا تقوم فيها Angular بإنشاء مجالاتٍ جديدة، وربّما تكون الحالات الأكثر شيوعًا هي عند التّعامل مع مجموعاتٍ من كائناتٍ متشابهة، كما سنرى في هذا الفصل. لا تملك Angular مكوّنًا اسمه Collection على عكس Backbone، ولكنّ دعم Angular الواسع للمجموعات ذات الكائنات المتشابهة يستحقّ إفراد فصلٍ كاملٍ لها، كما سنرى الآن. التهيئةقمنا سابقًا بتضمين المكتبة المستضافة عند Google في رأس مستندات HTML الخاصة بأمثلة الفصول السابقة، وسنضيف في هذا الفصل أسلوب Bootstrap في تنسيق الجداول والقوائم لإعطائها مظهرًا أجمل. <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script>بعد ذلك نقوم بتحميل وحدة app بتمرير اسمها إلى التّوجيه ng-app. يمكنك اختيار أيّ اسم للوحدة بدلًا من app. <body ng-app="app"> <!-- الأمثلة توضع هنا --> </body>كما شرحنا سابقًا، سنحتاج إلى إضافة بعض الشّيفرات المعيارية لنتجنّب استخدام الوحدات في أمثلة هذا الدرس، وسنغطّي ذلك المفهوم في الفصل القادم في هذا الكتاب. angular.module('app', []); angular.module('app').config(['$controllerProvider', function($controllerProvider) { $controllerProvider.allowGlobals(); }]);نحن جاهزون الآن لنتقدّم في استكشافنا التّفاعليّ للمجموعات (collections) والمرور على العناصر (iteration) ولـAngular. المرور على العناصرفي JavaScript المعتادة، عندما تمرّ على عناصر مجموعة من كائنات متشابهة عن طريق حلقة for، قد تقوم بالتّصريح عن متغيّر محلّيّ ليحتفظ بمرجعٍ للعنصر الحاليّ، على سبيل المثال: var items = [{name: "Item 1"},{name: "Item 2"}]; for (var i = 0; i < items.length; i++) { var item = items[i]; } document.body.innerHTML = item.name;الناتج Item 2ربما تظنّ (تتمنّى؟ تأمل؟ ترجو؟) أن يكون المتغيّر item في المثال السابق موجودًا ضمن مجال أسماءٍ تُنشئه JavaScript عند كل دورةٍ لحلقة for، للأسف، JavaScript لا تقوم بذلك كما يبيّن السطر الأخير في المثال السّابق.فالمتغيّر item متاح للاستخدام خارج الحلقة، لقراءة المزيد عن هذه النقطة، يُرجى مراجعة دليل Mozilla في JavaScript. ولكنّ Angular تتجنّب هذا المأزق عن طريق دعمها الدّاخليّ للمرور على العناصر.وكي نرى كيف تقوم بذلك لنقم أوّلًا بنقل المصفوفة items إلى داخل متحكّم. function ItemsController($scope) { $scope.items = [ {name: 'Item 1', price: 1.99}, {name: 'Item 2', price: 2.99} ]; }تخيّل لو أنّ items مجموعةٌ غير معروفة الطّول، ونحتاج إلى المرور على جميع عناصرها، وإظهار قيمة العنصر name لكلّ عنصرٍ فيها، ما الذي سنقوم به؟ ng-repeatرأينا سابقًا كيف تقوم Angular بإنشاء مجالٍ للعبارات لحمايتنا من إنشاء المتغيّرات في مجال JavaScript العام، وبطريقةٍ مماثلةٍ لذلك، يقوم التّوجيه ng-repeat بحمايتنا من الحالة التي رأيناها في المثال الأوّل، وذلك بإنشاء مجال Angular لكلّ دورة تكرار للحلقة. <ol ng-controller="ItemsController"> <li ng-repeat="item in items" ng-bind="item.name"></li> <li ng-bind="item.name"></li> </ol>الناتج ْ1. Item 1 2. Item 2 3. كما ترى، العنصر item غير متاحٍ خارج حلقة ng-repeat. تقوم ng-repeat بإنشاء مجالٍ ابنٍ جديد لكلّ عنصرٍ جديد من المجموعة مع الخصائص التي تحددها العبارة المحددة للمجموعة. في مثالنا السّابق، كان اسم الخاصّيّة هو item ولكن يمكن أن نستبدله بأيّ اسم. جرّب تغيير المثال السّابق وغيّر item إلى شيءٍ آخر، جرّب i أو x. عناصر الكائناتالتركيب النّحوي (مفتاح، قيمة) للكائن يسمح لك بالمرور على عناصر الكائنات. سيفيدك ذلك إن كنت تحتاج إلى كتابة محتوى كائنٍ ما بشكلٍ كامل في العرض. <table class="table table-condensed"> <tr ng-repeat="(propertyName, propertyValue) in {b: 'two', a: 1.0, c: 3}"> <td ng-bind="propertyName"></td> <td ng-bind="propertyValue"></td> </tr> </table>الناتج b two a 1 c 3ماذا لو أردنا استخراج عنصرٍ معيّن؟ كيف نقوم باستخراج عنصرٍ له اسمٌ محدّد من جميع الكائنات الموجودة في المجموعة؟ التركيب النحوي item in items الذي استخدمناه في مثال ng-repeat السابق، يشبه كثيرًا باني القائمة، ولكنّ ng-repeat لا تسمح للأسف بإرجاع أيّ شيءٍ غير عناصر الكائن أو المصفوفة في الطرف الأيمن للعبارة، لنحاول على أيّ حال. <ol ng-controller="ItemsController"> <!-- Invalid code! Syntax error, because 'for' is not supported! --> <li ng-repeat="item.name for item in items" ng-bind="item.name"></li> </ol> لم تعمل كما أخبرتك. باني القائمة الحقيقيّ يسمح لك بإرجاع أيّ شيء تريده من التعداد الأصليّ، عادةً باستخدام الكلمة المفتاحيّة for. تقدّم CoffeeScript مثالًا ممتازًا على ذلك. index$تقوم ng-repeat بإسناد الرقم التسلسلي للعنصر الحالي إلى متغيّرٍ خاصٍ هو index$ وذلك إضافةً إلى المتغيّر الّذي يحوي قيمة العنصر. في المثال التّالي سنقوم بإعادة اختراع العجلة، وسننشئ طريقةً جديدةً لكتابة قوائم HTML المرقّمة، حيث سنستخدم قيمة المتغيّر index$ في الإخراج. <div ng-controller="ItemsController"> <div ng-repeat="item in items"> {{$index + 1}}. {{item.name}} </div> </div>الناتج 1. Item 1 2. Item 2لنحاول الآن القيام باستخدامٍ متداخلٍ للتّوجيه ng-repeat. أوّلًا، لننشئ هيكل نموذج بياناتٍ أكثر تعقيدًا حيث يحوي كلّ عنصرٍ في الطبقة العلويّة مصفوفةً من الأبناء. function ItemsController($scope) { $scope.items = [ {name: 'Item 1', items: [ {name: 'Nested Item 1.1'}, {name: 'Nested Item 1.2'} ] }, {name: 'Item 2', items: [ {name: 'Nested Item 2.1'}, {name: 'Nested Item 2.2'} ] } ]; }والآن لنقم بإدخال حلقةٍ داخل أخرى، ستقوم الشّيفرة التّالية بإضافة اسم كلّ عنصرٍ إلى قائمةٍ مرقّمة. <div ng-controller="ItemsController"> <ol> <li ng-repeat="item in items"> {{item.name}} <ol> <li ng-repeat="item in item.items"> {{item.name}} </li> </ol> </li> </ol> </div>الناتج 1. Item 1 1. Nested Item 1.1 2. Nested Item 1.2 2. Item 2 1. Nested Item 2.1 2. Nested Item 2.2ولكن ماذا لو أردنا إنشاء عدّاداتٍ متداخلة؟ كيف سنمنع المتغيّر index$ الّذي تمّت تحديد قيمته في الحلقة الخارجيّة من أن يتمّ تظليله بالمتغيّر ذي الاسم نفسه الّذي تُنشئه الحلقة الدّاخليّة؟ ng-initربّما تتذكّر من الفصل الأول، المبادئ، أنّ Angular تسمح لنا بتهيئة متغيّرات المجال في منطقة العرض باستخدام التّوجيه ng-init. حلّ مشكلتنا يكمُن في استخدام ng-init للقيام بإعادة تعيين، أو عمل اسمٍ مستعارٍ، لمتغيّر الحلقة الخارجيّة index$ قبل أن يتمّ تظليله. <div ng-controller="ItemsController"> <div ng-repeat="item in items" ng-init="outerCount = $index"> {{outerCount + 1}}. {{item.name}} <div ng-repeat="item in item.items"> {{outerCount + 1}}.{{$index + 1}}. {{item.name}} </div> </div> </div>الناتج 1. Item 1 1.1. Nested Item 1.1 1.2. Nested Item 1.2 2. Item 2 2.1. Nested Item 2.1 2.2. Nested Item 2.2لا تكتفي ng-repeat بالمتغيّر index$ فهي تقوم بإسناد عددٍ من المتغيّرات البوليانيّة في كلّ دورٍ للحلقة، وهي: first$ وmiddle$ وlast$ وeven$ وodd$. يمكنك تجربة كلِّ واحدٍ منها ضمن المثال التّالي، وهذا المثال يستخدم التّوجيه المفيد ng-class حيث يتمّ وسم الخانة باللون الأخضر عندما تكون العبارة الشّرطيّة محقّقة. هل يمكنك اكتشاف الطّريقة الّتي تجعل العنصرين الأوّل والأخير فقط موسومين باللون الأخضر؟ (ملاحظة: ستحتاج إلى إضافة العمليّة ! فقط.) <ol> <li ng-repeat="val in [1,2,3,4,5]"> <span class="label label-default" ng-class="{'label-success': $middle}"> {{val}} </span> </li> </ol>هل لاحظت شيئًا عند استخدام المتغيّرين even$ وodd$؟ تقوم Angular بتحديد قيمة المتغيّرات وفق الطّريقة التّقليديّة التي تعتمد بدء التّرقيم من الصّفر في حلقات for. قد لا يبدو الأمر مثيرًا للاهتمام للوهلة الأولى، ولكن لو لاحظت جيّدًا فترقيم العناصر يبدأ من الصّفر بينما يبدأ ترقيم القائمة المرتّبة من الواحد، وهذا يعني أنّ المتغيّرين even$ وodd$ سيعملات بشكلٍ معاكسٍ لاسمهما بالنّسبة لترقيم القائمة. التفردكملاحظةٍ جانبيّة، يجب عليك الانتباه عند استخدام المتغيّرات البدائيّة (primitives) داخل ng-repeat بأن لا تكون جميع عناصر المجموعة متفرّدة، أي أنّه لا يوجد أي تكرار لأي قيمة في المجموعة، ومن يحدّد تطابق العناصر هو عمليّة المساواة المتشدّدة === في JavaScript. المساواة المتشددةلنقم ببعض التّجارب لإنعاش معلوماتنا في كيفيّة عمل عمليّة المساواة الصّارمة === في JavaScript. يختبر القالب التّالي المساواة بين أعدادٍ، سلاسل نصّيّةٍ وكائنات. <table class="table table-condensed"> <tr> <td>1 === 1</td> <td>{{1 === 1}}</td> </tr> <tr> <td>'1' === '1'</td> <td>{{'1' === '1'}}</td> </tr> <tr> <td>1 === '1'</td> <td>{{1 === '1'}}</td> </tr> <tr> <td>{} === {}</td> <td>{{ {} === {} }}</td> </tr> <tr> <td>{name: 1} === {name: 1}</td> <td>{{ {name: 1} === {name: 1} }}</td> </tr> </table>الناتج 1 === 1 true '1' === '1' true 1 === '1' false {} === {} false {name: 1} === {name: 1} falseبيّنّا بأنّ التّساوي بين عناصر المجموعة مرفوض، وسيبيّن المثال التّالي الخطأ الّذي سينتج عن تكرار العدد 1 في المجموعة. <ol> <!-- Invalid code! Duplicate element error, because '1' is repeated! --> <li ng-repeat="val in [1,2,1]" ng-bind="val"></li> </ol>قم بتعديل المثال السّابق، واستخدم خليطًا من الأعداد والسّلاسل النّصّيّة بدلًا من الأعداد فقط، جرّب المصفوفة التّالية [1,2,'1']. ماذا كانت النّتيجة؟ والآن حاول تغييرها باستخدام الكائنات بدلًا من الأعداد، جرّب المصفوفة التّالية [{name: 1},{name: 2},{name: 1}]. ستحتاج إلى تغيير العبارة "ng-bind="val إلى "ng-bind="val.name لتتمكّن من رؤية القيم. track byإنّ حلّ مشكلة مصفوفة الأعداد السّابقة يكمُن في إضافة تعليمة track by إلى عبارة ng-repeat وذلك لكي يتمّ تجاوز اختبار المساواة الّذي يتمّ القيام به تلقائيًّا للعناصر. يمكنك تتبّع عناصر المجموعة باستخدام أيّ متغيّر متفرّد (لا تتكرّر القيم الّتي يأخذها عند كلّ عنصر)، وإن لم يكن لديك متغيّر متفرّد (فالقيم العدديّة الموجودة داخل المصفوفة ليست كذلك)، يمكنك استخدام المتغيّر index$. <ol> <li ng-repeat="val in [1,2,1] track by $index" ng-bind="val"></li> </ol>الناتج 1. 1 2. 2 3. 1يجب عليك استخدام متغيّرات متفرّدة في نماذج كائناتك كلّما أمكنك ذلك، كأن تستخدم معرّفًا رقميًّا متفرّدًا يتمّ إنشاؤه عن طريق مخزن بيانات في الـbackend أو عن طريق نوعٍ ما من مولّد UUID في طرف المستخدم. إن لم يكن لديك مفرٌّ من استخدام المتغيّر index$ فكن حذرًا لأنّ التّغييرات في المجموعة قد تٌسبّب مشاكل في الأحداث الخاصة بالصفحة. توابع الاستدعاء الخلفيتسهّل Angular الحصول على مرجعٍ لعنصرٍ ما في المجموعة لتتمكّن من استخدامه داخل متحكّم ما. ليس عليك إلّا أن تمرّر عنصر المجموعة إلى تابع استدعاءٍ خلفيّ (callback) في التّوجيه الذي يسمح بتمرير تابع استدعاءٍ خلفيّ، مثل ng-click. على سبيل المثال، لنقم بكتابة شيفرة للسماح للمستخدم بحذف أحد عناصر المجموعة، سنحتاج إلى تعريف تابع استدعاء خلفيّ في المتحكّم بحيث يقبل مرجع العنصر المُراد حذفه كوسيطه الوحيد. يمكننا تسمية هذا التّابع بأي اسم، لنقم بتسميته destroy (سيكون هذا ملائمًا له). function ItemsController($scope) { $scope.items = [ {id: 1, name: 'Item 1'}, {id: 2, name: 'Item 2'}, {id: 3, name: 'Item 3'}, {id: 4, name: 'Item 4'} ]; $scope.destroy = function(item) { var index = $scope.items.indexOf(item); $scope.items.splice(index, 1); }; }قد تبدو طريقة حذف العنصر من المصفوفة في الشّيفرة السابقة مزعجة بعض الشيء إلا أنها الطريقة المُتّبعة في JavaScript للقيام بذلك، ولن تُسبّب أي خطأ مع Angular في ذلك. كُلّ ما بقي هو إضافة "(ng-click="destroy(item إلى عنصرٍ ما داخل الحلقة. كما يشير التّوجيه ng-click، فالزر هو الخيار الأفضل، ولكن لا بدّ من الإشارة إلى أنّه يمكننا استخدام التّوجيه ng-click مع أيّ عنصرٍ قابلٍ للنّقر. <div ng-controller="ItemsController"> <h4 ng-pluralize count="items.length" when="{'one': '1 item', 'other': '{} items'}"> </h4> <table class="table table-condensed"> <tr ng-repeat="item in items"> <td ng-bind="item.name"></td> <td> <button class="btn btn-xs btn-default" ng-click="destroy(item)"> destroy </button> </td> </tr> </table> </div>أعتبرُ طريقة Angular في ربط تابع الاستدعاء الخلفيّ destroy مثالًا واضحًا على الأسلوب التّصريحيّ الذي تتبعه Angular. كفائدةٍ إضافيّة، يعرض المثال السّابق استخدام التّوجيه ng-pluralize لكتابة نصٍّ يميّز بين المفرد والجمع بشكلٍ مشروط، حسب عدد العناصر ضمن مجموعة ما. إعداد هذا التّوجيه صعبٌ قليلًا ولكنك قد تحتاج لاستخدامه. start- وend-رغم أنه ليس شائعًا، إلّا أنك قد ترغب بإخراج عناصر "إخوة" لكلّ عضوٍ في المجموعة. وكمثالٍ على ذلك قائمة الوصف، أو العنصر dl، الذي يحوي زوجي العناصر dt وdd. وهنا تظهر المشكلة، فالتّوجيه ng-repeat مُصمّمٌ ليتمّ تطبيقه على عنصر HTML واحد. ولكنّ حلّ هذه المشكلة هو بتوسيع نطاق التنفيذ لهذا التّوجيه ليشمل العديد من عناصر HTML باستخدام اللاحقتين start- وend-. <dl ng-controller="ItemsController"> <dt ng-repeat-start="item in items">name</dt> <dd ng-bind="item.name"></dd> <dt>price</dt> <dd ng-repeat-end ng-bind="item.price"></dd> </dl>هناك العديد من التّوجيهات الأخرى التي تستخدم اللاحقتين start- وend- وليس التّوجيه ng-repeat فقط، ويجب عليك أن تتأكد من عدم انتهاء التّوجيهات المخصصة التي تقوم بإنشائها (كما ستتعلم في فصل التوجيهات اللاحق) بأيٍّ من اللاحقتين start- وend-. خلاصةتدعم Angular المجموعات بقوّةٍ ومرونة عن طريق التّوجيه ng-repeat، وتسمح لنا ببناء واجهة مستخدمٍ سريعة لتطبيقات CRUD. تنتهي في هذا الفصل نظرتنا العامّة للوظائف الغنيّة في تطوير الويب التي يمكننا تعلّمها دون الغوص كثيرًا في بنية Angular الدّاخليّة. ولكن من الآن فصاعدًا، سنبدأ بالغوص أعمق فأعمق في Angular الموسّعة، ولنتمكّن من القيام بذلك سيكون علينا تعلُّم كيفيّة قيام Angular بإدارة المكوّنات. وسيغطّي الفصل القادم النّظام الّذي طوّرته Angular لذلك، الوحدات. ترجمة وبتصرّف للفصل الخامس من كتاب: Angular Basics لصاحبه: Chris Smith.
-
ما الذي تعنيه كلمة "مجال" برأيك؟ قد تبدو من اسمها بأنّها تشير إلى جزءٍ من شيفرة التّطبيق، ربّما تم عملها لنتجنّب استخدام المجال العام الذي يسبّب العديد من المشاكل. يبدو أنّ الأمر بسيط، وإنّه لمن الحكمة أن يقوم إطار العمل بتطبيق شيءٍ كهذا، ربّما ليس علينا التّفكير في أمر المجالات أكثر من ذلك، هل يمكننا الانتقال للفصل التالي؟ ليس بهذه السّرعة، لن يكون هذا الفصل طويلًا ولكنه سيغطي أمرين شديدي الأهمّيّة: وراثة المجالات، وهرميّة المجالات، فقد يحوي تطبيق Angular نموذجيًّا على العشرات، المئات وربّما الآلاف من المجالات. قبل أن نبدأ، لنقم بإعداد البيئة لتُلائم أمثلة هذا الفصل. التهيئةفي فصل المبادئ، تعلّمنا كيف نقوم بإضافة التّوجيه ng-app إلى العنصر الذي نرغب بأن تقوم Angular بمعالجته. <body ng-app="app"> <!-- الأمثلة هنا --> </body>الوسيط الذي نمرره إلى التّوجيه ng-app هو اسم الوحدة التي تشكل جذر التطبيق الحالي (في المثال قمنا باستخدام الاسم app على سبيل الاصطلاح). سنقوم بتغطية الوحدات بعمق في فصل لاحق. أما الآن فاعتبر السّطور التّالية مجرّد شيفرات اصطلاحية يمكنك نسخها دون فهم محتواها. angular.module('app', []); angular.module('app').config(['$controllerProvider', function($controllerProvider) { $controllerProvider.allowGlobals(); }]);والآن بعد كتابة تلك الشّيفرات الخارجة عن الدّرس، يمكننا الدخول في الموضوع. scope$في الفصل الماضي، المتحكمات، تعلّمنا كيفيّة تحضير النّموذج عن طريق ربط العناصر (الخصائص properties) إلى المرجع scope$. لنقم بتكرار التمرين ثانيةّ. function NameController($scope) { $scope.name = "First"; }باستخدام التّوجيه ng-controller يمكننا استدعاء تابع التّحكم المكتوب أعلاه، وذلك ضمن سياق أحد عناصر المستند، وعندها ستكون أيّ بيانات قمنا بإسنادها للمجال متاحةً للاستخدام داخل العنصر. <p ng-controller="NameController"> {{name}} </p>الناتج Firstعناصر المستند التي تقع خارج العنصر الذي قمنا باستدعاء المتحكّم NameController فيه، لن تكون قادرةً على الوصول إلى العناصر المرتبطة بالمتحكّم، لنجرّب ذلك. <div> <p> Outside the scope: {{name}} </p> <div ng-controller="NameController"> <p> Inside the scope: {{name}} </p> </div> </div>الناتج Outside the scope: Inside the scope: Firstوهذا المثال يوضّح ارتباط عمل المتحكّم والمجال معًا، والآن لننظر إلى الحالة المعاكسة، تطبيق Angular بمجال واحد. rootScope$تحاول Angular جاهدةً أن تتأكد من ابتعادنا عن المشاكل أثناء استخدام المجالات، لذا تقوم بإنشاء مجالٍ جديد لكلّ متحكّم. يتمّ بناء المجالات وفق بنيةٍ هرميّة، ويتربّع في جذر هرم المجالات في كلّ تطبيقات Angular مجالٌ أبٌ وحيدٌ لجميع المجالات، يمكننا الوصول إلى هذا الجذر باستخدام الوسيط ذو الاسم المخصص rootScope$ داخل المتحكّم، واستخدامه بدلًا من المرجع المعتاد (والمستحسن) scope$. function RootNameController($rootScope) { $rootScope.name = "First"; }الشّيفرة واضحة وليس فيها صعوبة، وهي تعمل بشكل طبيعي. <p ng-controller="RootNameController"> {{name}} </p>الناتج Firstولكنّ المشاكل تبدأ بالظّهور عندما نحاول الوصول إلى عنصر له الاسم نفسه من متحكّم آخر. function SecondRootNameController($rootScope) { $rootScope.name = "Second"; }هذا لا يبشّر بالخير، فالمرجع rootScope$ وحيد (singleton) داخل التّطبيق، ولا يمكن أن يكون هناك سوى عنصر واحد له الاسم name. <p ng-controller="RootNameController"> {{name}} </p> <p ng-controller="SecondRootNameController"> {{name}} </p>الناتج Second Secondمن الواضح أنّ المتحكّم SecondRootNameController قد استبدل القيمة التي أسندها RootNameController للعنصر name. حسنًا، هذه هي مشكلة المتغيرات العامة، أليس كذلك؟ العزلتقدم Angular لنا بيئة آمنة عن طريق إنشاء مجالٍ خاصّ بكلّ متحكّم، لنقم بإعادة كتابة المتحكّمات السّابقة لتقوم بتهيئة النّموذج تهيئةً صحيحة، باستخدام المرجع scope$ بدلًا من rootScope$، وحاول مقارنة ذلك مع المثال السّابق الذي استخدمنا فيه rootScope$ لتعرف سبب إنشاء مجال لكلّ متحكّم، سنستخدم المتحكّمين التاليين: function SecondNameController($scope) { $scope.name = "Second"; }<p ng-controller="NameController"> {{name}} </p> <p ng-controller="SecondNameController"> {{name}} </p>الناتج First Secondأظهر المثال المخرجات الصحيحة، القيمة الصحيحة للعنصر name لكلا المتحكّمين. هذا العزل هو النّتيجة التي نحصل عليها عندما لا يكون أيٌّ من المتحكّمين ابنًا للآخر، أيّ أننا لم نقم بتعريف أحد المتحكّمين داخل عنصر في المستند محتوًى داخل متحكّمٍ آخر. ما الذي سيحدث لو قمنا بجعلهم متداخلين بدلًا من ذلك؟ المجالات المتداخلةبتعديلٍ صغيرٍ على المثال السابق، سنقوم بتحريك SecondNameController إلى عنصرٍ ابن للعنصر div الذي يقوم بتحميل المتحكّم NameController، وهذه الحالة هي ما يسمّى بالمجالات المتداخلة (nested). <div ng-controller="NameController"> <p> {{name}} </p> <p ng-controller="SecondNameController"> {{name}} </p> </div>الناتج First Secondالأمور تسير على ما يُرام، ولا يزال العنصر name معزولًا في كلا المجالين، ماذا لو قمتَ بعكس ترتيب العنصرين p في المثال السابق؟ هيا قم بتجربة ذلك، يُفترض بأنك سترى أنّ العنصرين name قد انعكس ترتيبهما. في الحقيقة، لا يعني ذلك بأنّ المجالين معزولين، فـAngular تقوم بتنظيم المتحكّمات في بنية هرميّة بالاعتماد على موضع المتحكّم في بنية المستند، والمتحكّم الابن يرث عناصر وخصائص أبيه. يرجع السبب في عدم حدوث تغيير في قيمة العنصر name في المثال السابق، إلى خاصّيّة التظليل، حيث يقوم العنصر name في الابن بتظليل قيمة العنصر name في الأب. لنجرّب الحصول على دليلٍ على هذا السلوك عن طريق تغيير اسم العنصر في المتحكّم الابن. function ChildController($scope) { $scope.childName = "Child"; }سنحاول إخراج قيمتي العنصرين في كلا المجالين، الأب والابن. <div ng-controller="NameController"> <p> {{name}} and {{childName}} </p> <p ng-controller="ChildController"> {{name}} and {{childName}} </p> </div>الناتج First and First and Childمن الواضح أنّ المتحكّم الأب NameController لا يملك صلاحيّةً للوصول إلى عناصر المتحكّم الابن، بينما يمكن للمتحكّم الابن ChildController أن يصل إلى عناصره وعناصر أبيه. بما أنّ العنصر name هو عنصر موروث من الأب، لا بدّ من أنّ التغيير على قيمته في مجال المتحكّم الابن سيؤدّي إلى تغيير قيمته في مجال الأب. سنضيف خانة إدخال ونربطها بالعنصر name. <div ng-controller="NameController"> <p> {{name}} </p> <p ng-controller="ChildController"> {{name}} </p> <input type='text' ng-model='name'> </div>إذا حاولت تعديل قيمة العنصر name في المثال السابق، ستجد أنّه يعمل كما هو متوقّع، فقيمة العنصر name تتغيّر في كلا المجالين، ولكن لاحظ بأنّنا نقوم بالتّغيير ضمن مجال الأب فقط. الوراثةقد ترغب أيضًا بتغيير قيمة العنصر name أيضًا في مجال الابن، ربّما تظنّ بأنّ هذا التغيير يجب أن ينعكس أيضًا على المتغير name في مجال الأب، لنجرّب ذلك، سنقوم بإضافة خانة إدخال لتسمح لنا بتعديل العنصر name في مجال الابن أيضًا. في العرض الخاصّ بالمثال، قم بالخطوات التالية بنفس الترتيب الذي سأذكره لك:أوّلًا، غيّر القيمة في خانة الإدخال العليا، ستلاحظ أنّ جميع القيم المرتبطة بالعنصر name قد تمّ تحديثها.ثانيًا، غيّر القيمة في خانة الإدخال السّفلى. <div ng-controller="NameController"> <p> name: {{name}} <br> <input type='text' ng-model='name'> </p> <p ng-controller="ChildController"> name: {{name}} <br> <input type='text' ng-model='name'> </p> </div>هل فاجَأَتك النّتيجة؟ تستخدم Angular طريقة JavaScript في وراثة الهيكل الخارجي (prototypal inheritance) هذا جيّدٌ إن كنت متمرّسًا فيها، فليس عليك تعلّم أيّ شيءٍ جديدٍ هنا، ولكنّه سيّءٌ لمن لم يواجه هذا النّوع من الوراثة من قبل، ففهمه ليس أمرًا بديهيًّا. تقول القاعدة: “إنّ تغيير قيمة عنصر ما في كائن في JavaScript، يؤدّي إلى إنشاء هذا العنصر في الكائن”، وهذه القاعدة البسيطة تشكّل مشكلةً أثناء التّعامل مع العناصر الموروثة التي تكون مظلّلة بالعنصر الخاص بالكائن الابن. حدث الأمر كالتّالي: في البداية لم يكن هناك أيّ عنصر اسمه name داخل الابن، ولكن عندما قمنا بتعديل النّصّ في خانة الإدخال السّفلى قامت Angular بإسناد النّص إلى العنصر name في الابن حيث تمّ إنشاء هذا العنصر في الابن، وعند حدوث ذلك، قام هذا العنصر بتظليل العنصر name في الأب، ومن ثمّ لم يعُد بإمكاننا الوصول إليه من الابن. كيف يمكننا التّعامل مع هذا الأمر في Angular، بحيث نتمكّن من تعديل بيانات النّموذج في مجالٍ موروث؟ لن يكون ذلك صعبًا، سيكون علينا فقط نقل العنصر name إلى داخل كائنٍ آخر. function InfoController($scope) { $scope.info = {name: "First"}; }function ChildInfoController($scope) { $scope.info.childName = "Child"; }<div ng-controller="InfoController"> <p> {{info.name}} and {{info.childName}} <br> <input type='text' ng-model='info.name'> </p> <p ng-controller="ChildInfoController"> {{info.name}} and {{info.childName}} <br> <input type='text' ng-model='info.name'> </p> </div>لاحظ أن ChildInfoController يعتمد على أبيه في إنشاء الكائن info، ما الذي سيحدث لو قمنا بتعديل شيفرة المتحكّم ChildInfoController واستبدلنا جسم التابع بالعبارة: scope.info = {childName: "Second$"};جرّب ذلك، سترى بأننا عدنا إلى إنشاء عنصرٍ جديد في الابن، مع تأثير التظليل الذي رأيناه سابقًا. scope.$watchتتعامل Angular أثناء عمليات الربط ثنائيّ الاتجاه مع العناصر في المجال بالطريقة التالية: عندما تستخدم خانة الدّخل المرتبطة للقيام بأيّ تغيير، يتمّ تحديث واجهة المستخدم في كلّ مكان، وهذا يختلف عن “الخصائص المحسوبة”(computed properties) وهي البيانات المأخوذة من مجال بيانات آخر. في المثال التالي، العنصر sum هو خصيصة محسوبة. function SumController($scope) { $scope.values = [1,2]; $scope.newValue = 1; $scope.add = function() { $scope.values.push(parseInt($scope.newValue)); }; // Broken -- doesn't trigger UI update $scope.sum = $scope.values.reduce(function(a, b) { return a + b; }); }يتم حساب قيمة العنصر sum فعليًّا عن طريق عملية بسيطة تستخدم التابع reduce في العبارة الأخيرة في المتحكّم SumController. في القالب الخاص بهذا المثال، سنستخدم أداة الدّخل select للسّماح للمستخدم باختيار الرّقم (2،1 أو 3) لإضافته إلى نهاية المصفوفة values.(كنقطة جانبيّة: لاحظ أنّ المتحكّم يعطي قيمةً ابتدائيّة للمتغيّر newValue, ولو لم نقم بذلك لكانت Angular ستضيف الاختيار الفارغ للقائمة في العنصر select، وذلك لتجنّب القيمة العشوائيّة التي يخزّنها newValue للخيار الأوّل المولّد عن طريق التّوجيه ng-options. هذا السلوك لا علاقة له بالمجالات ولكنّ العلم به أمرٌ مفيد.) <p ng-controller="SumController"> <select ng-model="newValue" ng-options="n for n in [1,2,3]"></select> <input type="button" value="Add" ng-click="add()"> The sum of {{values}} is {{sum}}. </p>عند النقر على Add ستتغيّر القيمة المعروضة لـsum، ولكن للأسف، الشّيفرة الخاصة بالمتحكّم تحتوي على أخطاء، ولن يعمل المتحكّم كما توقّعنا. لنقم الآن بتصحيح الخطأ، وذلك بنقل العبارة التي تقوم بالحساب الحقيقي للقيمة sum إلى تابع استدعاءٍ خلفيّ (callback). وعندما سنمرر هذا التابع كوسيط إلى scope.$watch$ مع وسيط آخر يمثّل عبارة المتابعة (في هذه الحالة هو اسم العنصر الذي يتم حساب sum منه)، سيؤدّي ذلك إلى جعل sum يتمّ إعادة حسابها كلّما تغيّر values. function SumController($scope) { $scope.values = [1,2]; $scope.newValue = 1; $scope.add = function() { $scope.values.push(parseInt($scope.newValue)); }; $scope.$watch('values', function () { $scope.sum = $scope.values.reduce(function(a, b) { return a + b; }); }, true); }والآن ستتغيّر قيمة العنصر sum ديناميكيًّا عند إضافة العناصر للمصفوفة. scope.$applyجميع التّوجيهات المدمجة في Angular والخاصة بعمليّة الربط ثنائيّ الاتجاه كاملة المزايا، ولكنّك قد تجد من فترة إلى أخرى سلوكًا تحتاج إلى إضافته. مثلًا، ماذا لو أردنا أن يقوم المستخدم بمسح الحالتين الحالة الحاليّة لخانة الدخل النّصّيّة و الحالة المرتبطة بها وذلك عندما يضغط زرّ esc؟ كيف يمكننا كتابة شيفرة التّعامل مع هذا الحدث؟ <div ng-controller="EscapeController"> <input type="text" ng-model="message"> is bound to "<strong ng-bind="message"></strong>". Press <code>esc</code> to clear it! </div>يجب علينا أوّلًا أن نصرّح عن المتغيّر ذو الاسم المخصّص element$ وتمريره كوسيط للمتحكّم، وذلك للسّماح لـAngular بحقن مرجعٍ في العنصر المرتبط بالمتحكّم. يمكننا استخدام التابع bind لتسجيل استدعاء خلفيّ لحدث keyup الذي يختبر ضغط الزّرّ esc، وداخل تابع الاستدعاء الخلفيّ هذا، سنقوم بتحديث عنصر المجال. قم بتجربة المثال التالي، اكتب شيئًا ثم اضغط esc. function EscapeController($scope, $element) { $scope.message = ''; $element.bind('keyup', function (event) { if (event.keyCode === 27) { // esc key // Broken -- doesn't trigger UI update $scope.message = ''; } }); }ليس بعد، بما أنّنا نتعامل مباشرةً (تقريبًا) مع عناصر المستند، سنحتاج إلى إخبار Angular عندما نريد أن تقوم بإعادة رسم العرض. نقوم بذلك عن طريق تغليف التّغييرات التي نقوم بها بتابع استدعاءٍ خلفيّ نمرّره إلى scope.$apply$. function EscapeController($scope, $element) { $scope.message = ''; $element.bind('keyup', function (event) { if (event.keyCode === 27) { // esc key $scope.$apply(function() { $scope.message = ''; }); } }); }جرّبها الآن، بعد أن أخبرنا Angular بما نريد، سيعمل كلّ شيء كما يجب. خلاصةإن حاولت تطبيق مفاهيم نموذج-عرض-متحكم (Model-view-controller (MVC على Angular، ستكون المجالات لغزًا بالنّسبة لك، قد تظنّ بأنّ الأمر بسيط، وأنّ المجالات هي جزءٌ من طبقة النّموذج، ولكن في Angular لا يكون الكائن نموذجًا حتّى يكون قابلًا للوصول إليه كعنصرٍ في المجال. ولكنّ القصّة تصبح أكثر إثارةً عندما ترى طريقة ارتباط المجالات بالمستند عن طريق المتحكّمات والتّوجيهات. بوضع سؤالنا الأكاديميّ جانبًا، فإن المجالات بديهيّةٌ وسهلة الاستخدام كما بيّنت أمثلة هذا الفصل. ترجمة وبتصرّف للفصل الرابع من كتاب: Angular Basics لصاحبه: Chris Smith.
-
طلبت منك في الفصل السابق (المبادئ) أن تكبح جماح JavaScript بداخلك ريثما نستكشف القيمة الحقيقيةّ التي تقدّمها Angular، وهي تقديم امتدادات قويّة لـHTML لمطوّر النّهاية الأمامية front-end. لا تنتهي قصّة Angular هنا بالطّبع، والمنهج في مشاريع Angular هو تخصيص السّلوكيات عن طريق JavaScript، فإذا كنت تتحرّق شوقًا في الفصل الأوّل للبدء بكتابة شيفرات حقيقيّة فقد حان الوقت لذلك، إنه وقت كتابة شيفرات JavaScript. أكثر الطرق شيوعًا لتعديل العرض في Angular باستخدام JavaScript هي عن طريق استخدام متحّكم، وأبسط طريقة لكتابة متحكّم هي باستخدام تابع بناء (constructor) بسيط. لنبدأ بمثالٍ بسيط لنفهم تمامًا ما الذي يحدث، المثال التّالي لا يقوم بأي شيء، ولا يطبع "!Hello World" حتّى. ها هو متحكّمنا البسيط. function EmptyController() { };سنحتاج إلى تضمين مكتبة Angular ضمن الصفحة لتتم معالجة خصائص المجال التي تم تحضيرها عن طريق المتحكّم، ولذا يجب نسخ الشّيفرة التّالية ولصقها داخل العنصر head في كلّ الأمثلة. <html> <head> <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script> </head> <body ng-app="app"> <!-- كل الأمثلة توضع هنا --> </body> </html>يجب علينا ضبط إعدادات تطبيق Angular داخل مستند HTML ليقوم بتحميل الوحدة app، فقد تعرفّنا في الوحدة الأولى المبادئ على التّوجيه ng-app الذي نضيفه لأيّ عنصر في المستند لتقوم Angular بمعالجة ما بداخله، انظر الآن إلى استخدام ng-app في المثال التّالي، هل يمكنك الانتباه إلى الإضافة الجديدة؟ <body ng-app="app"> </body>يُمكن تعريف المتحكّمات كتوابع بناء معرّفة في المجال العام (global scope)، وهذه هي الطّريقة التي سنستخدمها في أمثلتنا في هذا الفصل. كان الشكل البسيط للمتحكّمات مدعومًا في Angular حتى النسخة 1.3 لتسهيل انطلاق المطوّرين مع المكتبة، ولكنّ ذلك يتطلب الآن إنشاء وتسمية وحدة للتطبيق. سنناقش تفاصيل استخدام angular.module في الفصول القادمة الوحدات، حقن التابعة، والخدمات. يمكنك التعامل مع المثال التّالي على أنّه شيفرة معياريّة مطلوبة: angular.module('app', []); angular.module('app').config(['$controllerProvider', function($controllerProvider) { $controllerProvider.allowGlobals(); }]);والآن بعد كتابة هذه الشّيفرة الخارجة عن سياق الحديث، لنعد إلى متحكّمنا عديم الفائدة. ng-controllerكالعادة، الطّريقة التي سنتّبعها للقيام بأيّ شيء، هي استخدام الموجّهات، سنستخدم الموجه ng-controller الذي يقوم بالعثور على التّابع الذي يتمّ تمرير اسمه إليه ثم استدعائه. <p ng-controller="EmptyController"> </p>وكما توقّعتَ، لن يقوم هذا التّابع بأيّ شيء، والآن إذًا ما هي هذه المتحكّمات وكيف نستخدمها؟ بناء النموذجينصّ المرجع الرسمي في النظرة العامة للمفهوم على أنّ وظيفة المتحكّم هي "الكشف عن المتغيرات والوظائفيّة للعبارات والتّوجيهات" وتشير كلمة "الوظائفية" إلى توابع الاستدعاء الخلفي (callback)، وسنتطرق إليها قريبًا، أمّا الآن فسنتكلّم عن تهيئة "المتغيّرات"، أو بعبارة أخرى تحضير النّموذج. أرجو منك أن تبقي في ذهنك أنّ وحدة Angular ليست سوى JavaScript عاديّة يمكن الوصول إليها ضمن مجال عبارة ما، وبذلك سيكون تحضير النّموذج أمرًا سهلًا للغاية. سنتمكّن من كتابة JavaScript عاديّة دون أيّ قيود، لنقم بإضافة عنصر إلى المتحكّم الفارغ، وليكن هذا العنصر سلسلة نصّيّة. function MessageController() { this.message = "This is a model."; }أمرٌ بسيط، أليس كذلك؟ هل قمنا الآن بإنشاء وحدة؟ ماذا تتوقّع؟ الإجابة هي "تقريبًا"، فعندما يكون بإمكاننا الوصول إلى العنصر message ضمن مجال العرض، ستكون هذه وحدةً بالفعل. أحد طرق القيام بذلك هي الكشف عن كامل المتحكّم كعنصرٍ بداخل المجال، ولن يكون هذا صعبًا كما تظنّ، كلّ ما عليك معرفته هو التركيب النحويّ المناسب للقيام بذلك. Controller as propertyNameيشرح توثيق المكتبة بالنّسبة للتّوجيه ng-controller بأنه يمكنك تمرير عبارة Controller as propertyName كوسيط للتّوجيه ng-controller، وهذه الميزة متاحة في النسخة 1.2 فما فوق. <p ng-controller="MessageController as controller"> {{controller.message}} </p>الناتج This is a model.رائع، لقد قمنا للتّو بتحضير بيانات النّموذج باستخدام متحكّم. صحيحٌ بأنّ هذه الطّريقة في ربط الخاصّية بالمرجع this مباشرة وليس فيها الكثير من التّعقيد، إلّا أنها تعطي انطباعًا خاطئًا بأنّ المتحكّم جزءٌ من النّموذج، وهذا غير صحيح، فالمتحكّم في الحقيقة يقوم بـتحضير النّموذج فقط. سيكون اختبار الشّيفرة وتتبّع أخطائها أكثر سهولةً عندما يتم الحدّ من الوصول إلى البيانات بمقدار ما يحتاج إليه العمل لا أكثر، كما أنّ الطّريقة السّابقة تضيف بعض الضجّة إلى شيفرة العرض، فجميع العناصر سيتم الوصول إليها عبر مرجع (reference) للمتحكّم. شيفرة العرض هي المكان الأكثر حساسيةً للضجيج في تطبيق ويب، فهي أوّل مكانٍ قد نبدأ فيه بفقد القدرة على فهم الهدف من الشّيفرة بلمحة سريعة. أخيرًا، المتحكّمات تعمل يدًا بيد مع المجال، لذا ولأهدافٍ تعليميّة سيكون من الأفضل رؤية مرجع (reference) للمجال وامتلاك القدرة على التّحكم به، ومع متحكّم كهذا سيكون بإمكاننا الكشف عن العناصر التي نحتاجها في العرض فقط، بدل الكشف عن المتحكّم بما فيه، وسنتّبع في هذا الكتاب هذه الطّريقة بالكشف الصّريح عن العناصر المستخدمة في العرض داخل المتحكّم. أوّل سؤالٍ يتبادر إلى الذّهن الآن هو: "كيف سنحصل على مرجعٍ للمجال؟"، هل سننشئ واحدًا عن طريق new أم سنطلب الحصول على واحدٍ بطريقة ما؟ حقن التابعيةسنحصل عليه عن طريق حقن التابعية، ربما تكون قد لاحظت أيضًا أنني لم أكتب أي شيفرة لإنشاء المتحكّم مثل: var controller = new MessageController(); مثلًا، فمن الذي يقوم بإنشاء المتحكّم المستخدم في العرض إذًا؟ Angular هي من يقوم بذلك طبعًا، فـAngular هي حاوية لتبادل التحكم Inversion of Control تدير دورة حياة مكونات التّطبيق، فعندما نحتاج إلى متحكّم جديد أو إلى مكوّنٍ آخر، ستقوم Angular ببنائه تلقائيًّا، وهذا يوفر الجهد، ولكنّ الأمر الأكثر أهمية هو أنه يسمح لـAngular بـحقن المصادر، أو التّبعيّات إلى المكوّن الجديد. scope$يحتاج متحكّمنا فقط إلى مُعامل يُسمّى scope$ وذلك بفضل إطار العمل الذي يُدار عن طريق حقن التّابعية، كما أنّ تسمية هذا المُعامل هامّة جدًّا. ويجب عليك أن تعرف خطورة تقصير شفرة JavaScript داخل مشروعك، فهذا قد يؤدي إلى تدمير آلية عمل حقن التّابعية. (ولكن لا تقلق، فهناك حلّ التفافيّ لهذه المشكلة، سنتطرّق إليه في فصل حقن التابعية). function MessageController($scope) { $scope.message = "This is a model."; }بإضافتنا للمعامل scope$ إلى تابع البناء نكون قد أخبرنا Angular بحاجتنا إلى مرجع لمجال العرض، والآن بدلًا من تعريف العنصر message داخل this (أي داخل المتحكّم)، قمنا بتعريفه مباشرةً داخل المجال. في المثال التّالي، قمنا بإزالة as controller من التّوجيه ng-controller، كما أنّ العبارة controller.message أصبحت فقط message وهي العنصر الوحيد الذي قمنا بربطه بالمجال. <p ng-controller="MessageController"> {{message}} </p> الناتج This is a model.رغم أن الطّريقتين تعملان بشكل صحيح، إلا أننا سنستخدم طريقة المرجع scope$، فكما ترى، حقن التّابعية هو جزء مكمّل لاستخدام Angular، ويجب أن يصير مألوفًا لنا. النموذج-العرض-المتحكم MVCيشير تعريف المتحكّم في المرجع الرسمي إلى أنّه يكشف “الوظائفيّة” للعرض، وقبل أن ننتقل إلى هذا الجزء يجدر بنا الحديث عن الفرق بين متحكّمات Angular وبين نموذج MVC التقليدي.(قد ترغب في تجاوز هذه الفقرة). تنصّ ويكيبيديا في مقال النّموذج-العرض-المتحكّم (MVC) على أنّ المتحكّم “يستلم المدخلات ويحوّلها إلى أوامر للنموذج أو للعرض.”، قد تكون عبارة “أوامر للنّموذج أو للعرض” صعبة الفهم في سياق الحديث عن Angular، فـAngular قامت بتبسيط نموذج MVC الخاص بها، باستخدام النّماذج الضّعيفة (anemic models) التي لا تحوي منطقًا برمجيًّا فيها. النّمط السّائد في Angular هذه الأيّام هو وضع كل منطق العمل، أو ما يُعرف أيضًا بمنطق النّطاق (domain logic) داخل المتحكّم. بعبارةٍ أخرى، تتّجه Angular نحو نموذج نحيل ومتحكّم سمين. إذا كنت تألف بعض الأنماط مثل نموذج النطاق الغنيّ (rich domain model) فربّما تجد طريقة Angular متخلّفةً بعض الشيء، أما أنا فأرى الأمر مجرّد طريقة لترتيب الأولويّات. فعند برمجة طرف المستخدم يكون الفرق الأكثر أهمّية هو بين شيفرة العرض التّريحية الموجّهة نحو المستند من جهة، وبين شيفرة JavaScript التي تتبع أسلوب الأوامر والمقادة بالبيانات والتي تعالج منطق العمل والبنية التحتية للتطبيق من جهة أخرى. أنا سعيدٌ لأن Angular ركّزت على ذلك فهذا انتصارٌ كبيرٌ لها، وقد يصبح ذات يومٍ فصل منطق العمل عن باقي مسؤوليات المتحكّم هو الأمر الأكثر أولويّة، وقد نرى توجّهات نحو نموذج أغنى. التوابعلنقم بكتابة متحكّم يكشف عن تابع بسيطٍ جدًّا، فقط لنختبر إمكانية استدعاء التوابع داخل العرض. لا بد من أنّك تذكر من الفصل الماضي أن Angular لا تسمح بتعريف التوابع داخل العرض. المتحكّم التّالي يقوم بإسناد تابع بسيط إلى عنصرٍ في العرض. function CountController($scope) { $scope.count = function() { return 12; } }ويمكننا استدعاء التّابع في العرض بكتابة شيفرة JavaScript عاديّة، اسم العنصر مع أقواس، فقط. <p ng-controller="CountController"> There are {{count()}} months in a year. </p>الناتج There are 12 months in a year.لاحظ أن التّابع لم يتم استدعاؤه ونسيانه بعد ذلك ببساطة، بل تمّ ربطه بالنّموذج. ما الذي يعنيه ذلك؟ ربّما بدأت بالتسليم بأن البنية التحتية في Angular هي الربط (binding)، ومرّة أخرى سأكرّر: لا تقوم Angular باستدعاء التّابع فقط عندما يتم إخراج العرض للمرة الأولى فقط، بل في أيّ وقتٍ يتم فيه تغيير النّموذج المرتبط، لنتمكّن من رؤية ذلك أثناء العمل سيتوجّب علينا كتابة تابع يستخدم عنصرًا من النّموذج. يستخدم التّابع add في المثال التّالي عنصرين في النّموذج، تم تعريفهما في المجال، وهما operand1 و operand1. ستقوم Angular باستدعاء التّابع وإخراج النتيجة كلّما تغيّر أحد العنصرين أو كليهما. function AdditionController($scope) { $scope.operand1 = 0; $scope.operand2 = 0; $scope.add = function() { return $scope.operand1 + $scope.operand2; } $scope.options = [0,1,2,3,4]; }لقد قمنا في الفصل الماضي بتغطية العديد من الأمثلة عن التّوجيه input في Angular، لذا سنستخدم الآن التّوجيه select لتغيير قيم النّموذج. لا بدّ من أنك لاحظت السّطر الأخير في الشّيفرة السابقة، حيث تمّ فيه تحضير نموذج خيارات options، وسنستخدم هذا النّموذج لبناء سلسلة داخل وسيط التّوجيه ng-options. سنستخدم العبارة x for x في بناء السّلسلة، ورغم أنها قد تبدو بلا فائدة لأنها تعيد عناصر السلسلة الأصلية كما هي، إلا أن التّوجيه يحتاج إلى كتابتها على أي حال.(عندما يكون النّموذج مكوّنًا من مصفوفة كائنات objects وهو الأكثر شيوعًا، يمكنك كتابة بانٍ للسّلسلة يقوم بإخراج العنصر name من الخيار المحدد، وذلك باستخدام x.name for x. <p ng-controller="AdditionController"> <select ng-model="operand1" ng-options="x for x in options"></select> + <select ng-model="operand2" ng-options="x for x in options"></select> = {{add()}} </p> إنه يعمل بشكل جيّد، ولكن بإمكاننا تحسين تصميم التّابع add عن طريق التّعميم، الذي سيكون مفيدًا إن أردنا استخدام التّابع مع وسطاء غير operand1 و operand2. فلنبدأ إذا بتعديل الشفرة ولنقم باستخراج الوسطاء من التابع. function AdditionController($scope) { $scope.number = 2; $scope.add = function(operand1, operand2) { return operand1 + operand2; } }يمكنك تمرير العناصر والقيم الفوريّة داخل العبارات، كما في JavaScript المعتادة. <p ng-controller="AdditionController"> {{add(number, 2)}} is not the same as {{add(number, "2")}} <br> 2 + 2 + 2 + 2 = {{add(2, add(2, add(2, 2)))}} </p>الناتج 4 is not the same as 22 2 + 2 + 2 + 2 = 8لنقم الآن بالكشف عن تابع استدعاء خلفي (callback) يمكنه معالجة عملٍ ما. الاستدعاءات الخلفية (Callbacks)في الفصل السابق، قمنا بتمرير عبارة إلى التّوجيه ng-click واستخدمناها للتّقليب بين قيمتين بوليانيتين للعنصر authorized، حيث قمنا بتهيئة القيمة الابتدائية للنموذج authorized باستخدام التّوجيه ng-init ثم تلاعبنا بقيمته عن طريق استدعاء خلفيّ سطريّ، فكتبنا "ng-click="authorized = !authorized، لنقم الآن بتعديل المثال عن طريق نقل التهيئة ووضع المتغيّر في مكانه الصحيح، في المتحكّم. function AuthController($scope) { $scope.authorized = true; $scope.toggle = function() { $scope.authorized = !$scope.authorized }; }والآن صار التّابع toggle متاحًا للاستخدام داخل المجال، والوسيط الذي سيتمّ تمريره للتوجيه ng-click سيبدو كأنه استدعاء للتابع: ()toggle ولكنه ليس كذلك كما ذكرنا سابقًا، إنها فقط سلسلة نصّيّة سيتم معالجتها لاحقًا عندما يقوم المستخدم بالنقر على الزر. <div ng-controller="AuthController"> <p> The secret code is <span ng-show="authorized">0123</span> <span ng-hide="authorized">not for you to see</span> </p> <input class="btn btn-xs btn-default" type="button" value="toggle" ng-click="toggle()"> </div>لا يزال المثال يعمل، والآن صار منطق التّابع toggle البسيط في مكانٍ أفضل. تشرح الكتب عادةً تقنيّاتٍ لإدارة التعقيدات باستخدام أمثلة شديدة البساطة لإيصال الإحساس العام بفائدة هذه التقنية، وقد قمنا بذلك هنا أيضًا. لقد طلبت إليك في الفصل الماضي أن تؤجّل حكمك على استخدام العبارات داخل نصوص HTML، فإذا كنت الآن قد أحببت طريقة Angular في تحسين نصوص HTML مع سلوك تفاعليّ، فقد تتساءل لم هذا التعقيد الزائد في المتحكّمات. أحد الأسباب التّقليدية لنقل الشّيفرة من سياقٍ معقّد(كالقالب) إلى واحدٍ أبسط (كالمتحكّم) هو تسهيل الاختبار وتصحيح الأخطاء لاحقًا. تتضح الفائدة من تسليم التعامل مع سلوك المستخدم إلى المتحكّم عندما نحتاج إلى كتابة شيفرات معقّدة، مثل مزامنة بيانات النّموذج مع المخدّم البعيد. خلاصةتعرفنا في هذا الفصل على JavaScript بداخل Angular، مكتوبة على شكل متحكّمات، والتي تتحمل مسؤولية تحضير البيانات للعرض كما ينصّ نمط MVC. تجعل المتحكّمات البيانات متاحةً للعرض عن طريق التصريح عن العناصر في كائن المجال scope$. في الفصل القادم سنأخذ نظرة أقرب لكائنات المجال، وسنتعرّف على كيفيّة تنظيمها في بنيةٍ هرميّة تتبع تقريبًا بنية الجزء الذي نعالجة في تطبيقنا ضمن مستند. ترجمة وبتصرّف للفصل الثالث من كتاب: Angular Basics لصاحبه: Chris Smith.