ToCode
Kanalga Telegram’da o‘tish
טיפים קצרים למתכנתים מאת ינון פרק
Ko'proq ko'rsatish1 421
Obunachilar
+324 soatlar
+17 kunlar
+230 kunlar
Postlar arxiv
1 421
# למה לא נשלח מייל הבוקר (או: מה בא אחרי I)
אלה מכם שרשומים לקבלת הפוסטים דרך המייל אולי הופתעו הבוקר כשהמייל היומי לא נשלח. האמת שאם אתם קוראים יותר מכמה חודשים אתם כבר יודעים שהמערכת מדלגת על שליחת מיילים בחגים. מה שאתם אולי לא יודעים זה שמנגנון הדילוג הוא בסך הכל סקריפט bash שמשתמש ב hebcal ו grep. בעיקרון hebcal מדפיס לנו את כל התאריכים החשובים בלוח השנה העברי:
hebcal | head -10
1/3/2023 Asara B'Tevet
1/23/2023 Rosh Chodesh Sh'vat
2/6/2023 Tu B'Shvat
2/18/2023 Shabbat Shekalim
2/21/2023 Rosh Chodesh Adar
2/22/2023 Rosh Chodesh Adar
3/4/2023 Shabbat Zachor
3/6/2023 Ta'anit Esther
3/7/2023 Purim
3/8/2023 Shushan Purim
הסקריפט מחפש רק את התאריך החשוב של היום, ומשתמש ב grep כדי להבין אם תאריך זה הוא אחד מרשימה של תאריכים שבהם לא צריך לשלוח מייל. הבלוק הרלוונטי כולו היה:
evenings_ok=(
"Pesach I"
"Pesach VII"
"Shavuot I"
"Tish'a B'Av"
"Rosh Hashana II"
"Yom Kippur"
"Sukkot I"
"Shmini Atzeret"
)
for send_in_evening in "${evenings_ok[@]}"
do
# echo "^[0-9/]+ ${send_in_evening}"
if hebcal $(date +"%m %d %Y") | egrep -v "CH''M" | egrep "^[0-9/]+ ${send_in_evening}" >& /dev/null
then
if (( $(date +%"_H") < 20 ))
then
exit 1
fi
fi
done
אז מה שבור כאן? הפסח כמובן. המחרוזת Pesach I שמייצגת את חג הפסח באמת מתאימה לטקסט Pesach I שיודפס באותו יום, אבל היא מתאימה גם ל Pesach II:
hebcal | grep 'Pesach I'
4/6/2023 Pesach I
4/7/2023 Pesach II
4/8/2023 Pesach III (CH''M)
4/9/2023 Pesach IV (CH''M)
ולכן כשהסקריפט ניסה לשלוח מיילים הבוקר הוא חשב שזה חג וויתר על המייל.
וכמו הרבה פעמים ברגע שרואים את הבעיה התיקון כבר כותב את עצמו. במקרה שלנו רק צריך לתקן את הביטוי הרגולארי:
$ hebcal | grep 'Pesach I$'
4/6/2023 Pesach I
נ.ב. זה הסקריפט אחרי תיקון. אם יש לכם רעיונות לבאגים נוספים שהוא מכיל שאולי פספסתי מוזמנים לשלוח, או אם אתם צריכים סקריפט לזיהוי חגים מוזמנים להשתמש בו לכל מטרה:
evenings_ok=(
"Pesach I$"
"Pesach VII"
"Shavuot I$"
"Tish'a B'Av"
"Rosh Hashana II"
"Yom Kippur"
"Sukkot I"
"Shmini Atzeret"
)
no_emails=(
"Rosh Hashana"
)
no_emails_erev=(
"Erev Pesach"
"Erev Shavuot"
"Erev Rosh Hashana"
"Erev Sukkot"
"Erev Yom Kippur"
)
for send_in_evening in "${evenings_ok[@]}"
do
# echo "^[0-9/]+ ${send_in_evening}"
if hebcal $(date +"%m %d %Y") | egrep -v "CH''M" | egrep "^[0-9/]+ ${send_in_evening}" >& /dev/null
then
if (( $(date +%"_H") < 20 ))
then
exit 1
fi
fi
done
for holiday in "${no_emails[@]}"
do
if hebcal $(date +"%m %d %Y") | egrep "^[0-9/]+ ${holiday}" >& /dev/null
then
exit 1
fi
done
for erev_holiday in "${no_emails_erev[@]}"
do
if hebcal $(date +"%m %d %Y") | egrep "^[0-9/]+ ${erev_holiday}" >& /dev/null && [$(date +"_H") > 19]
then
exit 1
fi
done1 421
# טיפ פייתון: פיתוח עם docker compose
דוקר קומפוז מספק דרך קלה להתחיל לכתוב יישומים בתוך קונטיינר. למרות שהרבה אנשים לא אוהבים להשתמש בו לסביבת פרודקשן (בגלל היעדר יכולות לניהול קונטיינרים רצים), בסביבת פיתוח עדיין מאוד נוח להשתמש בו במצבים שעובדים על כמה פרויקטים שונים ושכל פרויקט צריך את התלויות והתוכנות הנוספות שלו.
בדוגמה של פייתון אפשר לדמיין שנרצה לעבוד על כמה פרויקטי פייתון שכל אחד מהם משתמש בגירסת פייתון אחרת וספריות אחרות, וכל אחד מהם גם משתמש בבסיס נתונים שונה או גירסה שונה של בסיס הנתונים. בעוד שקל לנהל סט חבילות ספציפי לפרויקט עם סביבות וירטואליות, כשמתחילים להוסיף את התוכנות מסביב דברים עלולים להסתבך.
באמצעות דוקר קומפוז אפשר לבנות מערכת שמורכבת מקונטיינר של אפליקציה שמתחברת לבסיס נתונים מסוג מסוים, לבחור גירסת פייתון וסביבה וירטואלית וגירסה של בסיס הנתונים ולהריץ ולהוריד את כל המערכות בפקודה אחת.
בשביל לפתח על המחשב שלנו קוד פייתון עם docker-compose נצטרך להתמודד עם האתגרים הבאים:
1. נרצה שהקוד בתוך הקונטיינר יהיה זהה לקוד בספריית הפיתוח.
2. נרצה לשמור את המודולים שהפרויקט משתמש בהם בסביבה וירטואלית ספציפית לפרויקט, שלא קשורה לגירסאות פייתון שמותקנות אצלי במחשב המארח.
3. נרצה לאפשר לקוד לתקשר עם בסיס נתונים שגם יעלה מתוך ה compose.
4. נעדיף לא לבנות אימג' בשביל הפיתוח כדי לחסוך פעולות build.
## תיאור הפיתרון
עם docker compose אפשר לבנות פיתרון שעונה על כל האתגרים:
1. נמפה את הקוד מתיקיית הפיתוח לתיקייה בתוך הקונטיינר
2. נשתמש בסקריפט entrypoint שימופה לתוך הקונטיינר שיתקין את התלויות מקובץ
requirements.txt לתוך סביבה וירטואלית בתוך הקונטיינר.
3. נשמור את הסביבה הוירטואלית על volume כדי שבהפעלות הבאות לא נצטרך להתקין מחדש את התלויות.
## מימוש קוד הפיתרון
נראה דוגמה באמצעות תוכנית flask שמתחברת לבסיס נתונים Postgresql ושולפת משם מידע להחזיר ללקוח. אני יוצר תיקיה עבור הפרויקט ובתוכה קובץ בשם main.py עם התוכן הבא:
from flask import Flask
import psycopg2
import os
conn = psycopg2.connect(
database="postgres",
user=os.environ['DB_USER'],
password=os.environ['DB_PASS'],
host=os.environ['DB_HOST'],
port= os.environ['DB_PORT']
)
app = Flask(__name__)
@app.route("/")
def hello_world():
cursor = conn.cursor()
cursor.execute("select version()")
data = cursor.fetchone()
return f"Using database version {data}"
וקובץ בשם requirements.txt עם התוכן הבא:
flask
psycopg2
עכשיו אני יוצר קובץ בשם docker-compose.yml עם התוכן הבא:
version: "3.9"
services:
app:
image: python:3.11-bullseye
command: /bin/bash /usr/local/bin/entrypoint.sh
ports:
- 5000:5000
volumes:
- .:/app
- ./entrypoint-dev.sh:/usr/local/bin/entrypoint.sh
- venv:/venv
environment:
DB_USER: "postgres"
DB_PASS: "monkey"
DB_HOST: "db"
DB_PORT: "5432"
db:
image: postgres:15.2
environment:
POSTGRES_PASSWORD: "monkey"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
venv:
pgdata:
וקובץ בשם entrypoint-dev.sh עם התוכן הבא:
#!/bin/bash
cd /app
if [[ ! -d /venv/venv ]]
then
python -m venv /venv/venv
fi
source /venv/venv/bin/activate
pip install -r requirements.txt
flask --app main run --host 0.0.0.0 --port 5000
אחרי כל אלה אפשר לצאת לדרך. מפעילים:
$ docker compose up
ויכולים לגלוש מהדפדפן לכתובת localhost:5000 כדי לראות את העמוד הראשי שידפיס את הגירסה של בסיס הנתונים אליה אנחנו מחוברים.1 421
# פתוחים לרעיונות חדשים
מעבר לצורך להישאר רלוונטים, יש עוד שני יתרונות בגללם אנחנו חייבים להישאר פתוחים לרעיונות חדשים-
1. חלק מהרעיונות יכולים ללמד אותנו דרכי עבודה חדשות עם אותם כלים שאנחנו כבר מכירים.
2. חלק מהרעיונות יכולים ללמד אותנו כלי עבודה חדשים, שיהפכו את שיטת העבודה הנוכחית שלנו לקלה יותר.
אני עדיין רואה מתכנתים ומתכנתות שמשתמשים ב Redux בלי Redux Toolkit, אחרי שבנו לבד חלקים גדולים ממה ש RTK נותן ובצורה פחות טובה. הסיבה היחידה להמשיך לעבוד כך היא מטעמי Legacy, וזו בדיוק האטימות שאנחנו מכירים לרעיונות חדשים.
אני עדיין רואה מתכנתים ומתכנתות שמוותרים על TypeScript כי "לכתוב הגדרות טיפוסים רק מאט את העבודה", למרות שהם שמחים לקבל השלמות אוטומטיות ובדיקות מהירות מה IDE ומשתמשים ב ESLint בלי בעיה. אם הם יפתחו את הדלת ל TypeScript אין ספק שההתחלה תהיה קשה, אבל מהר מאוד הם יקבלו חווית פיתוח טובה בהרבה.
מינון טוב של קבלת רעיונות חדשים עוזר לנו לבנות מערכות טובות יותר, ולא פחות חשוב משאיר את הכיף בפיתוח מערכות. הכיף של גילוי, כיף של יישום, כיף של להתווכח עם רעיון ולדבר איתו עד שמבינים את הכוונה וכיף כשבסוף דברים מתחברים ועובדים טוב יותר.
1 421
אפשר לראות את הגירסה עם Client Component בקישור הזה:
https://server-components-demo-2cr9zxka4-ynonp.vercel.app/
## שליפת מידע מ API ב Server Component
אחד היתרונות המעניינים של הרצת קוד צד-שרת ושליחת התוצאה בלבד לקלאיינט הוא היכולת לגשת ל APIs או לבסיס נתונים מתוך קומפוננטת ריאקט, גישה שתתבצע רק בצד השרת. ננסה את זה עם API. ניצור קומפוננטה להצגת מידע מ
swapi.dev שתציג מידע על כוכבים ממלחמת הכוכבים:
export default async function PlanetInfo({id=3}) {
const res = await fetch(`https://swapi.dev/api/planets/${id}/`);
const data = await res.json();
return (<div>
<p>Welcome to the planet {data.name}. Its diameter is {data.diameter} and its population is {data.population} inhabitants</p>
</div>)
}
אני מעדכן את קוד העמוד שיציג לי מידע על כמה כוכבים בקובץ page.tsx:
import PlanetInfo from './components/planet_info'
export default function Home() {
return (
<main>
<PlanetInfo id={3} />
<PlanetInfo id={4} />
</main>
)
}
וכבר במצב פיתוח אפשר לראות שקרה פה משהו מעניין - בטאב network אני לא רואה בכלל גישות ל swapi. המידע כולו הגיע מקומפוננטת צד השרת בטעינת העמוד, למרות שהקומפוננטה היא בכלל Promise ולא מתנהגת לפי מנגנון מחזור החיים הרגיל של קומפוננטת ריאקט.
למעשה אנחנו רואים כאן את היכולת המלהיבה השניה של קומפוננטות צד שרת, והיא האפשרות "לברוח" מדרך החשיבה הריאקטית ולהשתמש בקוד async/await רגיל כדי למשוך מידע ממקורות חיצוניים ולהכין את הקומפוננטה.
## מגבלות - העברת מידע מ Client Component ל Server Component
בשיחה על Server Components שווה לשים לב גם למגבלות שלהם, ובפרט למגבלה (היחסית צפויה) ש Server Components לא כוללים סטייט. בדוגמה של כוכבי הלכת ממלחמת הכוכבים, ייתכן ונרצה לאפשר למשתמש לבחור את מזהה הכוכב ואז להציג את המידע על אותו כוכב.
זה הקוד שהייתי רוצה לכתוב בקובץ planet_picker.tsx:
"use client";
import { useState } from "react";
import PlanetInfo from "./planet_info";
export default function PlanetPicker() {
const [id, setId] = useState(3);
return (
<div>
<input type="text" value={id} onChange={(e) => setId(Number(e.target.value))} />
<PlanetInfo id={id} />
</div>
)
}
אבל זה לא עובד - ה Server Component לא יודע עדיין מה המשתמש בחר בזמן יצירת העמוד (כי משתמש עדיין לא בחר כוכב) ולכן זה לא עובד. המנגנון הכי קרוב שיש ל next להציע הוא להטמיע את הסטייט, במקרה שלנו ה id של הכוכב, ב url של העמוד ואז כשמשתמש משנה את הערך להשתמש ב router.push כדי לעבור לעמוד חדש, שם הקומפוננטה תרונדר בצד השרת עם הערך העדכני שיילקח מה Route Param.
## סיכום
סך הכל פיצ'ר Server Components פותר שתי בעיות מרכזיות בפיתוח עם ריאקט:
1. הוא מסדר את כל שליפות המידע הראשוניות שאנחנו צריכים כדי להציג את העמוד, וחוסך למשוך מידע מצד הלקוח אחרי שהעמוד עולה.
2. הוא מאפשר חיבור קל יותר בין קומפוננטות ריאקט למידע שמגיע מבסיס נתונים כשהמידע הזה דרוש רק לטעינה ראשונית של העמוד.
הרבה אתרים סטטיים או סטטיים ברובם יוכלו להרוויח מהשינוי החדש, ובמיוחד מאחר וב next.js קומפוננטות צד שרת הן ברירת המחדל.1 421
# משחקים עם React Server Components
פיצ'ר Server Components של ריאקט הוא חלק מריאקט 18 ומאפשר סוג של Server Side Rendering לקומפוננטות ספציפיות. בגירסה 13 של Next.JS התווספה תמיכה ב Server Components מה שהפך את העבודה איתם להרבה יותר פשוטה. בואו נראה איך זה עובד ומה היתרונות שלהם על פני פיתוח ריאקט רגיל או על פני Server Side Rendering.
## מהם React Server Components
קומפוננטות צד שרת הן פשוט קומפוננטות ריאקט שמרונדרות לגמרי בצד שרת. המפתח כאן הוא השורה "קומפוננטות צד שרת לא משפיעות כלל על גודל הבאנדל שנשלח ללקוח" איתה נפתח ה RFC. המטרה של קומפוננטות צד שרת היא לאפשר לכתוב קוד צד שרת בסגנון קומפוננטות ריאקט, וכך לספק חווית פיתוח אוניברסלית טובה יותר.
## ה Server Component הראשון שלי
בשביל להבין איך זה עובד נלך לבנות מערכת עם Server Components ב Next.JS. אני מתחיל פרויקט next חדש:
$ npx create-next-app@latest server-components-demo
לשאלות שלו אני עונה:
1. שימוש ב TypeScript - כן
2. שימוש ב ESLint - כן
3. שימוש בתיקיית src - כן
4. שימוש בתיקיית app ניסיונית - כן
5. שימוש ב import alias ברירת המחדל
קיבלתי עץ תיקיות שנראה כך:
.
├── README.md
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── public
│ ├── next.svg
│ ├── thirteen.svg
│ └── vercel.svg
├── src
│ └── app
│ ├── api
│ │ └── hello
│ │ └── route.ts
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.module.css
│ └── page.tsx
└── tsconfig.json
6 directories, 15 files
בשלב ראשון אני מוסיף תיקיה בשם components ובתוכה יוצר קומפוננטה בשם HelloWorld עם הקוד הבא:
// file: src/app/components/hello_world.tsx
export default function HelloWorld({name="Guest"}) {
return <h1>Hello {name}</h1>
}
לאחר מכן אני מעדכן את הקובץ src/page.tsx לקוד הבא:
import HelloWorld from './components/hello_world'
export default function Home() {
return (
<main>
<HelloWorld name="Dave" />
<HelloWorld name="Dana" />
<HelloWorld />
</main>
)
}
ואנחנו מוכנים לצאת לדרך. אני מפעיל את הקוד עם:
$ npm run dev
ויכול לגלוש ל loalhost:3000 כדי לראות את הודעות הברכה לשלושת המשתמשים.
בשביל לראות את הקוד בפרודקשן אני מעלה אותו למאגר גיט בכתובת:
https://github.com/ynonp/server-components-demo
מחבר אותו ל vercel ויש לנו את המערכת באוויר בכתובת:
https://server-components-demo-aabippwya-ynonp.vercel.app
הפתעה ראשונה שתקבל את פנינו בכניסה לעמוד היא שאין צורך בהרצת JavaScript כדי לראות את הדף. גם אם אתם נכנסים עם תוסף NoScript או משהו דומה אתם עדיין תראו את אותן שלוש שורות של Hello, כי הרינדור של הקומפוננטות נעשה בצד שרת. נכון שספריית ריאקט עצמה עדיין נשלחת ללקוח, אבל קוד הקומפוננטות כבר לא.
אפשר לראות ההבדל אם נעדכן את הקוד של Hello World ונהפוך אותו לקומפוננטת צד לקוח. בשביל זה יש להוסיף את הטקסט "use client" בראש הקובץ בצורה כזאת:
"use client";
export default function HelloWorld({name="Guest"}) {
return <h1>Hello {name}</h1>
}
אחרי קומיט והעלאת הגירסה אני יכול לזהות בקובץ page.js בשרת את הקוד הבא:
7126: function(e, r, t) {
"use strict";
t.r(r),
t.d(r, {
default: function() {
return o
}
});
var n = t(9268);
function o(e) {
let {name: r="Guest"} = e;
return (0,
n.jsxs)("h1", {
children: ["Hello ", r]
})
}
},
שמראה לי שהקוד של הקומפוננטה נשלח ללקוח.
כן צריך להגיד שגם בגירסה הזו של ה Client Component עדיין אפשר לגשת לעמוד ולראות את כל התוכן גם בלי JavaScript מופעל, בזכות ה Server Side Rendering שאני מקבל מ next.js. במקרה הזה ההבדל המרכזי בין שתי הגישות היה גודל הבאנדל והתוכן שלו.1 421
# הסיבות בגללן בחרנו Micro Services
יש לא מעט סיבות שיכולות לשכנע אנשים לבחור בארכיטקטורת Micro Services, ביניהן:
יכולת גדילה אופקית - כשסרביס מסוים עמוס אפשר להוסיף עוד מופעים שלו
התמודדות עם בעיות - כשסרביס נופל אפשר להרים מופעים חדשים שלו, ובנוסף אם יש בעיה בסרביס אחד זה לא חייב לעצור את כל המערכת.
זריזות בפיתוח - אפשר לעדכן מיקרו סרביסים בלי לדאוג ששברת קוד במקומות אחרים ביישום.
גמישות - אפשר לכתוב סרביסים בכל מיני שפות תכנות, כאשר לכל חלק במערכת נבחר את הטכנולוגיה המתאימה ביותר.
דיפלוימנט קל יותר - כי אפשר להעלות רק סרביס אחד ולא לגעת בשאר המערכת.
עלות פיתוח נמוכה יותר - כי אפשר להיעזר יותר בקלות בפרילאנסרים או צוותים קטנים לכתיבה ותחזוקה של מיקרו סרביסים.
הבעיה שבהרבה מקרים הבחירה בארכיטקטורת Micro Services לא מביאה לתוצאה הרצויה: אי אפשר באמת להוסיף עוד עותקים של סרביס מסוים כי המקביליות גורמת לנעילות, כשסרביס נופל הסרביסים האחרים נכנסים ל Retry Loop ושוברים את כל המערכת כמו דומינו, סרביסים משתפים מידע דרך בסיס נתונים משותף ולכן אי אפשר באמת לשנות אחד בלי לשבור אחרים ואפילו ה Deployments נשארו מסובכים כי צריך להעלות גירסה של כמה סרביסים במקביל. כל זה לפני שדיברנו על הקושי להקים סביבת עבודה מקומית או היכולת לבדוק גירסה מסוימת של המערכת (או באופן כללי לדבר על "גירסה" של מערכת בעולם של מיקרו סרביסים).
ארכיטקטורת Micro Services באה עם עלות. אם היא נותנת לכם את כל היתרונות שתכננתם לקבל היא שווה את המחיר, אבל אם אתם חצי שנה לתוך הפיתוח ומגלים שבעצם העלות גבוהה מהתמורה, אולי צריך לדבר על לחזור למונולית.
1 421
# גיט add הכל הוא הרגל רע
פותח בראנץ, כותב קוד, בודק שהכל עובד, עושה קומיט, מגלה טעות, מתקן, שוב מוסיף הכל וקומיט, כותב בדיקה אוטומטית, מגלה עוד שתי טעויות, מתקן וקומיט.
אם התיאור הזה נשמע לכם מוכר זה זמן טוב לעצור, או לפחות להתעכב על השלב "עושה קומיט".
במהלך הפיתוח ובמיוחד בתוך רצף עבודה מאוד מפתה להשתמש בפקודות:
$ git add .
$ git commit
או בגירסה המתונה יותר git commit -a. הבעיה בשתי הגירסאות נובעת משילוב שני גורמים:
1. קומיטים ספונטניים נוטים לכלול שינויים שקשורים למספר נושאים. כשאותו קומיט מכיל שינויים במספר נושאים אנחנו רואים הודעות קומיט מורכבות (במקרה הטוב) או התעלמות משינויים קטנים במקרה הנפוץ. דוגמה קלה היא כשבמהלך העבודה על פיצ'ר מגלים שגיאת כתיב בקובץ לא-קשור-בכלל.
2. הרבה פעולות של גיט עובדות ברמת קומיט וכשהקומיטים לא מדויקים אנחנו מפסידים. בדוגמת תיקון שגיאת הכתיב, אם בעתיד ארצה לעשות revert לקומיט הזה אני מוותר גם על תיקון שגיאת הכתיב. אם אני צריך את תיקון האיות גם בענף אחר אני עכשיו צריך לקחת ב cherry pick גם את השינוי הגדול יותר.
מה עושים במקום? האפשרות המועדפת עליי היא להפעיל git status ו git diff לפני שמתחילים להכניס דברים לקומיט, ואז להכניס את השינויים לקומיט בצורה מבוקרת - או באמצעות הוספת קבצים מלאים או אפילו באמצעות git add -i שמאפשר לבחור איזה שורות בקובץ צריכות להיכנס לקומיט.
הכנסה מבוקרת של שינויים לקומיטים מייצרת קומיטים נקיים יותר ותחסוך לחברים שלכם התמודדות עם חצאי שינויים שבמקרה נכנסו לקומיט ששיתפתם.1 421
MATCH (neo4j:Category {name: "neo4j"})
MERGE (foo)-[:SUBSCRIBED_TO]-(spam)
MERGE (bar)-[:SUBSCRIBED_TO]-(neo4j)
MERGE (buz)-[:SUBSCRIBED_TO]-(spam)
MERGE (buz)-[:SUBSCRIBED_TO]-(neo4j)
## כתיבת שאילתות על יחסים
אז עכשיו שהפוסטים שייכים לקטגוריות והקוראים מנויים לקטגוריות אפשר לשפר את השאילתות שלנו, למשל אפשר לבדוק מהם כל הפוסטים שקורא מסוים צריך לקבל. בשלב ראשון ובגלל שעוד אין לי מידע על דיוורים שנשלחו במערכת, אלה יהיו פשוט כל הפוסטים בקטגוריות שמתאימות למנוי. עבור הקורא foo שמנוי לקטגוריית spam זאת השאילתה:
MATCH (foo:Reader {email: "foo@demomail.com"})
MATCH (foo)-[:SUBSCRIBED_TO]-(c:Category)
MATCH (p:Post)-[:BELONGS_TO]-(c)
RETURN p
## עדכון המודל כדי לאפשר קטגוריות-על
אחד הדברים היפים במודל של neo4j הוא הגמישות של הסכימה. בדוגמה שלנו אפשר לדמיין שנרצה לאפשר למנוי להירשם לכל הקטגוריות, כולל קטגוריות שעדיין לא נוצרו, או לקבוצה דינמית של קטגוריות. למשל אפשר לדמיין שקטגוריות כמו "פיתוח צד-לקוח", "פיתוח node.js" ו"SQL" יהיו שייכות כולן לקטגוריה גדולה יותר בשם "פיתוח Full Stack", ואגב גם זו יכולה להיות שייכת לקטגוריה גדולה יותר כמו "פיתוח", שתהיה שייכת לקטגוריית "כל הפוסטים".
אנחנו יודעים שפוסט יכול להיות שייך לקטגוריה בזכות יחס BELONGS_TO שיצרנו, ועכשיו אנחנו צריכים שגם קטגוריה תוכל להיות שייכת לקטגוריה. ל neo4j זה לא משנה בכלל.
ניצור קטגוריה חדשה בשם all:
MERGE (:Category {name: "all"})
ועכשיו נוסיף את שתי הקטגוריות שיש לנו כך שיהיו שייכות ל all:
MATCH (a:Category {name: "all"})
MATCH (c:Category)
WHERE c <> a
MERGE (c)-[:BELONGS_TO]-(a)
וניצור קורא חדש שמנוי לקטגוריית all:
MATCH (a:Category {name: "all"})
MERGE (r:Reader {email: "me@demomail.com"})-[:SUBSCRIBED_TO]-(a)
וכן אפשר ליצור קורא ולחבר אותו לקטגוריה בפקודת MERGE אחת. זה מדליק.
בשביל להציג את כל הפוסטים שהקורא החדש צריך לקבל אני צריך רק עדכון קטן לשאילתה שכבר כתבתי:
MATCH (r:Reader {email: "me@demomail.com"})
MATCH (r)-[:SUBSCRIBED_TO]-(c:Category)
MATCH (p:Post)-[:BELONGS_TO*]-(c)
RETURN p
שימו לב לסימן הכוכבית אחרי המילה BELONGS_TO. הכוכבית הופכת את החיפוש לרקורסיבי ומבקשת מ neo4j להחזיר את כל הפוסטים שיש מהם מסלול שמורכב מכמה חיבורי BELONGS_TO לקטגוריה אליה הוא מנוי.
## לאן ממשיכים
השאילתה האחרונה צריכה להצית את הדמיון, כי חיפושים רקורסיביים ב SQL היו נראים הרבה יותר ארוכים וקשים לכתיבה, ובדרך כלל יעלו לנו בביצועים איומים. בסיסי נתונים גרפיים חיים בשביל שאילתות כאלה, וכאן הכח שלהם.
ננסה לעצום עיניים ולעשות תרגיל דומה בבסיס נתונים רלציוני - זה יתחיל בטבלה של פוסטים, טבלה של קטגוריות וטבלת חיבור שמכילה מזהה פוסט בעמודה אחת ומזהה קטגוריה בעמודה שניה. ואז מגיעה הדרישה לחבר כמה קטגוריות לקטגוריה גדולה יותר. אז עכשיו צריך לעדכן את טבלת החיבור ולהפוך אותה לטבלה פולימורפית, כלומר להוסיף עוד עמודה שתגיד אם עמודת ה"פוסט" מכילה בעצם פוסט או קטגוריה, וכמובן לעדכן את השאילתה כדי לשלב את החיפוש הרקורסיבי.
ואם הדוגמאות כאן עשו לכם חשק ללמוד יותר על neo4j אני ממליץ על אתר הלימוד הרשמי שלהם בכתובת:
https://graphacademy.neo4j.com/1 421
MERGE (:Reader {email: "foo@demomail.com"})
MERGE (:Reader {email: "bar@demomail.com"})
MERGE (:Reader {email: "buz@demomail.com"})
## כתיבת שאילתות על צמתים
הפקודה MATCH מספקת דרך בסיסית לקרוא מידע ב neo4j. המידע שלנו בינתיים מאוחסן רק בצמתים והפקודה MATCH יכולה לקבל קריטריון חיפוש ולהחזיר את כל הצמתים שמתאימים לקריטריון. שימו לב שביצירת כל צומת העברנו ל MERGE מילה שמתארת את הצומת - המילה שמתחילה באות גדולה מיד אחרי הנקודותיים. מילה זו נקראת Label והיא נקודת ההתחלה לכל חיפוש. לכן בשביל למצוא את כל קוראי הבלוג אני אכתוב:
MATCH (r:Reader) return r
בתוך ממשק הניהול של בסיס הנתונים אני מקבל את התוצאה בתור גרף ויזואלי, ויכול גם לבחור להציג את התוצאה בתור Table או Raw. התשובה בתור טבלה נראית כך:
{
identity: 5,
labels: ["Reader"],
properties: {
email: "foo@demomail.com"
},
elementId: "4:afd409dc-b534-4d20-bc87-dbccb412845c:5"
}
{
identity: 6,
labels: ["Reader"],
properties: {
email: "bar@demomail.com"
},
elementId: "4:afd409dc-b534-4d20-bc87-dbccb412845c:6"
}
{
identity: 7,
labels: ["Reader"],
properties: {
email: "buz@demomail.com"
},
elementId: "4:afd409dc-b534-4d20-bc87-dbccb412845c:7"
}
אנחנו יכולים לראות שכל צומת במערכת מכיל את ה label שבחרתי (במקרה של "קורא" זו המילה Reader), וכן שמתם לב נכון ש labels הוא מערך ואפשר להגדיר מספר תוויות לצומת. הצומת מכיל גם שדה בשם identity שזה מספר קצר שמזהה אותו וגם elementId שזה כבר מספר יותר מורכב. בגדול מומלץ לא לשמור ערכים אלה כי neo4j ממחזר אותם כשצמתים נמחקים. וכמובן כל המאפיינים שהגדרנו נשמרים בתוך הטבלה properties.
הנה שאילתה דומה עבור פוסטים:
MATCH (p:Post) return p
{
identity: 2,
labels: ["Post"],
properties: {
publishedAt: 2023-01-24T06:00:00Z,
title: "first post",
slug: "first"
},
elementId: "4:afd409dc-b534-4d20-bc87-dbccb412845c:2"
}
{
identity: 3,
labels: ["Post"],
properties: {
publishedAt: 2023-01-25T06:00:00Z,
title: "second post",
slug: "second"
},
elementId: "4:afd409dc-b534-4d20-bc87-dbccb412845c:3"
}
{
identity: 4,
labels: ["Post"],
properties: {
publishedAt: 2023-04-02T06:00:00Z,
title: "Hello World in Neo4j",
slug: "hello-neo4j"
},
elementId: "4:afd409dc-b534-4d20-bc87-dbccb412845c:4"
}
פקודת MATCH יודעת גם לסנן את התוצאות לפי ערכי ה properties. לדוגמה אני יכול לשלוף רק את הקטגוריה שנקראת neo4j:
MATCH (c:Category) WHERE c.name = "neo4j" return c
או את כל הפוסטים שפורסמו באפריל 2023:
MATCH (p:Post)
WHERE datetime({year: 2023, month: 4, day: 1}) <= p.publishedAt <= datetime({ year: 2023, month: 5, day: 1})
RETURN p
## הוספת קשרים בין הצמתים
עד לפה neo4j לא נראה שונה מכל בסיס נתונים מבוסס מסמכים. מה שהופך אותו לבסיס נתונים גרפי הוא החיבור בין הצמתים באמצעות קשתות. אנחנו יוצרים חיבור בין שני צמתים בעזרת פקודת MATCH ... MERGE. החלק של ה MATCH ישמש כדי למצוא צמתים ואז ה MERGE ייצור את החיבור ביניהם.
הפקודה הבאה משייכת את שני הפוסטים הראשונים לקטגוריית spam:
MATCH (p: Post)
WHERE p.slug IN ["first", "second"]
MATCH (c: Category {name: "spam"})
MERGE (p)-[:BELONGS_TO]->(c)
פקודת MERGE בשורה האחרונה היא החשובה: בצד שמאל יש לנו את הפוסט, בצד ימין את הקטגוריה ובאמצע שם היחס בין שניהם, במקרה שלנו BELONGS_TO, כלומר פוסט שייך לקטגוריה. בגלל שיש שני פוסטים שמתאימים ל WHERE אז שניהם יחוברו לאותה קטגוריה.
נריץ גם את הפקודה הבאה כדי לשייך את הפוסט האחרון לקטגוריית neo4j:
MATCH (p: Post)
WHERE p.slug = "hello-neo4j"
MATCH (c: Category {name: "neo4j"})
MERGE (p)-[:BELONGS_TO]->(c)
כמו כן קוראים יכולים להירשם לקטגוריות מסוימות בבלוג, אז בואו נשייך קורא אחד לקטגוריית spam, קורא שני לקטגוריית neo4j וקורא שלישי לשתי הקטגוריות גם יחד:
MATCH (foo:Reader {email: "foo@demomail.com"})
MATCH (bar:Reader {email: "bar@demomail.com"})
MATCH (buz:Reader {email: "buz@demomail.com"})
MATCH (spam:Category {name: "spam"})1 421
# צעדים ראשונים עם neo4j
בסיסי נתונים גרפיים נכנסו לחיינו כבר לפני מספר שנים במחשבה שאם המידע שלנו לא "מתאים" לטבלה אז במקום לאלץ את המידע לתוך התבנית שאנחנו מכירים אפשר לשנות את מודל האחסון. neo4j הוא אחד מבסיסי הנתונים הגרפיים הפופולריים ביותר גם היום ב 2023. יש לו קהילה חזקה, כלי פיתוח נוחים והתממשקות לכל שפת תכנות שאנחנו מכירים.
## מהו Neo4j
לפני שנשאל מהו neo4j נרצה לדבר על מהו בסיס נתונים גרפי (ולא, אין שום קשר לתמונות). גרף הוא מבנה נתונים שבנוי מצמתים (Nodes) ומקשתות המחברות אותם (Edges). בסיס נתונים גרפי הוא בסיס נתונים שנותן לנו שפה כדי להגדיר ולתשאל מידע בצורה של גרף. הצרכים שלנו מבסיס נתונים גרפי יהיו:
1. יכולת ליצור צמתים בגרף.
2. יכולת לחבר צמתים בקשתות.
3. יכולת להוסיף מידע בצמתים.
4. יכולת להוסיף מידע על הקשתות.
5. יכולת לחפש מידע, כולל חיפוש המבוסס על קשר בין צמתים.
וכמו שיש עולמות תוכן שמאוד קל לייצג בטבלאות, כך יש עולמות תוכן שלמים שייצוג באמצעות גרף פשוט מתאים להם כמו כפפה ליד. חישבו לדוגמה על רשת חברתית, בה כל צומת מייצג חבר ברשת והקשתות מייצגות את ה"חברות" בין החברים ברשת. שאילתות ברשת חברתית יתחילו מצומת מסוים וינסו לברר מי החברים שלו, מי החברים של החברים שלו, איזה אנשים הוא כנראה יכיר (כי אם הרבה חברים שלי הם גם חברים של מישהו מסוים אז בטח גם אני מכיר אותו).
בסיס הנתונים neo4j יאפשר לנו לייצג בקלות תרחישים כמו רשת חברתית ותרחישים רבים נוספים בהם הדגש הוא על הקשרים בין פריטי מידע מסוימים, ואנחנו לא יודעים מראש את כל פריטי המידע והמאפיינים שלהם.
## איך מקבלים בסיס נתונים
בשביל להתחיל לעבוד עם neo4j אפשר להריץ אותו אצלנו על המחשב בהתקנה מקומית או דוקר (הכל קוד פתוח), אבל הדרך הכי קלה היא לפתוח בסיס נתונים בענן שלהם שנקרא auro db. הכניסה בקישור:
https://neo4j.com/cloud/platform/aura-graph-database/
אחרי רישום קצר תוכלו ליצור בסיס נתונים חדש וריק, שימו לב רק שבמסך יצירת בסיס הנתונים הם מציגים סיסמת חיבור ואחרי זה לא תוכלו לראות שוב את הסיסמה הזו ובלעדיה לא תוכלו להתחבר לבסיס הנתונים אז צריך להעתיק אותה הצידה לפני שממשיכים.
אחרי שתיצרו את בסיס הנתונים ב Aura תוכלו להיכנס לממשק גרפי לעבודה איתו באמצעות לחיצה על כפתור Open במסך רשימת בסיסי הנתונים. מדביקים את הסיסמה, לוחצים Connect ואתם יכולים להתחיל להריץ שאילתות.
## יצירת מודל נתונים עבור מנויים לבלוג
בשביל לשחק עם בסיס הנתונים אני אצור מודל נתונים לייצוג משתמשים שנרשמים לקבלת פוסטים במייל מהבלוג. זה אותו מודל שהשתמשתי בו בפוסט שכתבתי על Datomic אז אפשר גם לחזור לקרוא שם כדי להיזכר ולהשוות.
בשביל לאפשר לאנשים להירשם לקבלת פוסטים מהבלוג למייל אני צריך לשמור את פרטי המייל של מי שנרשם, אני צריך לשמור פוסטים ובגלל שאולי לא כל הפוסטים מעניינים את כולם אני רוצה לסדר את הפוסטים לקטגוריות ולאפשר לאנשים להירשם רק לקטגוריות מסוימות.
במודל גרפי נוכל להגיד שהצמתים בגרף יהיו:
1. מנוי - שיחזיק מזהה משתמש, כתובת מייל עדכנית ואולי מידע נוסף שמעניין אותי לגבי המנוי.
2. קטגוריה - שתחזיק את שם הקטגוריה.
3. פוסט - שיחזיק את הפרטים לגבי הפוסט כמו הכותרת, התוכן ומתי פורסם.
4. דיוור - כדי לשלוח מייל למנויים אני ארצה לזכור מה שלחתי למי ואם היו בעיות בשליחה.
מבחינת הקשרים (הקשתות) בין הצמתים נוכל לדבר על:
1. מנוי יכול להיות "רשום ל" קטגוריה, כדי לקבל למייל פוסטים מקטגוריה מסוימת.
2. פוסט יכול להיות "שייך ל" קטגוריה.
3. דיוור "נשלח" למנוי באימייל.
4. דיוור "כולל" פוסט או אוסף של פוסטים.
הפקודה
MERGE ב neo4j יוצרת פריטים חדשים. בואו ניצור כמה פריטים לסכימה שתיארתי. תחילה אני יוצר פוסט עם מאפיין slug (זה השם שמופיע ב url), title עבור שורת הכותרת ו publishedAt עם תאריך ושעת הפירסום:
MERGE (:Post {slug: "first", title: "first post", publishedAt: datetime('2023-01-24T06:00:00')})
ועוד כמה פוסטים בשביל שיהיה מעניין:
MERGE (:Post {slug: "second", title: "second post", publishedAt: datetime('2023-01-25T06:00:00')})
MERGE (:Post {slug: "hello-neo4j", title: "Hello World in Neo4j", publishedAt: datetime('2023-04-02T06:00:00')})
עכשיו אפשר ליצור קטגוריות:
MERGE (:Category {name: "spam"})
MERGE (:Category {name: "neo4j"})
וכמה מנויים:1 421
# הזמן הכי טוב לקחת קורס
מתי הזמן הכי טוב לקחת קורס על נושא שאתם צריכים לפרויקט?
האם זה יהיה לפני תחילת העבודה, כדי להבין על מה מדובר ולהחליט את ההחלטות הנכונות כבר מהצעד הראשון?
האם תוך כדי הפרויקט, כדי לא לבזבז זמן?
או שאולי בכלל אחרי, כשיש לכם כבר הבנה מעשית של עולם התוכן ועכשיו צריך את התיאוריה כדי לשים את החלקים במקום?
בפיתוח תוכנה - כל התשובות נכונות.
לפני תחילת העבודה אנחנו צריכים מושג כללי של עולם התוכן, והוראות בסיסיות איך להתחיל. אבל אחרי שקיבלנו את הכלים הבסיסיים נרצה להתקדם ולחקור את הבעיה שלנו, לנסות לכתוב קוד, להיכשל ולראות מה עובד ומה לא. נכון שהרבה פעמים אפשר להסתדר לבד בשלב הזה עם Tutorials ו Stack Overflow, כשלא מבינים כלום עלולים לעשות טעויות שאחרי זה יהיה קשה לראות ולתקן.
אחרי שנצליח להפעיל כמה דברים ונישאר אבודים לגבי כמה דברים אחרים, נוכל להמשיך ללמוד בצורה מסודרת תוך כדי המשך עבודה. ההבנה הבסיסית שקיבלנו תקבל חיזוק מעבודה על פרויקט שמעניין אותנו, ובמהלך הצפיה בשיעורים נוכל לחשוב איך דברים מתחברים - גם בתרגילים וגם בפרויקט האישי שאנחנו בונים.
וכשהפרויקט באוויר? זה הזמן הכי מהנה לקחת קורס, כי כבר לא צריך לדאוג אם נצליח ליישם את השיעור על הפרויקט שלנו, כי אנחנו כבר יודעים את הסוף וכי זה יהיה הזמן להתעמק בכל פרק, לקרוא את תכני ההעשרה ולהבין יותר טוב מה עשינו, מה עבד ומה עוד אפשר לשפר. ממילא פרויקט תוכנה אף פעם לא "גמור", וידע טוב יותר יעזור לבנות גירסה 2 יעילה יותר.
אי אפשר (ואנחנו גם לא רוצים) לברוח מבניית פרויקטים בטכנולוגיות שאנחנו לא מכירים ומשילוב לימוד תוך כדי עבודה. וכשהקורס מלווה אותנו בכל שלבי העבודה הוא מפסיק להיות "מטלה" והופך למשהו שכיף לחזור אליו.
1 421
# תחליטי עכשיו!
פותח פרויקט ריילס חדש, ומיד מיליון שאלות: רוצה import maps או webpack (או אולי בכלל parcel)? איך לנהל משתמשים? באיזה ספריית בדיקות להשתמש? מה ה Database? האם זו מערכת מלאה שכוללת HTML או שמספיק רק API? איזה כללי Style לשים ב Rubocop? רוצה להשתמש ב tailwind או לכתוב CSS רגיל?
ואני לא מאשים את ריילס פה. המצב דומה בכל סטאק טכנולוגי שתבחרו, ואפילו במקום שיש ברירות מחדל סבירות ככל שמתקדמים בפרויקט תמיד יהיה יותר קשה לשנות החלטות שקיבלת בהתחלה. ואין דבר יותר ממכר מלהתלבט על בחירות טכנולוגיות בתחילת הדרך.
וכמובן כל השאלות חשובות וכל תשובה לא נכונה תחזור לנקום בנו בעתיד. בדוגמה של CSS עבודה ללא תכנון ובלי לבחור סטנדרט מההתחלה עלולה להביא אתכם למצב שה CSS יצא משליטה ותצטרכו לשכתב את כל קבצי ה CSS למבנה חדש ומסודר. ואותו דבר לגבי כל החלטה טכנולוגית אחרת במוצר. ברור שעדיף בהרבה להחליט את ההחלטות הטובות ביותר בתחילת הדרך. טוב יותר אבל לא ממש ריאלי, כי אנחנו עוד לא יודעים מספיק על המוצר.
לכן הפיתרון הוא בדיוק הפוך. במקום "תחליטי עכשיו" נגיד "תתחילי עכשיו". כן זה יהיה מבאס לשנות את כל ה CSS בעוד שנה כשיהיה לך מוצר בפרודקשן עם משתמשים אמיתיים, אבל לפחות יהיה לך מוצר בפרודקשן עם משתמשים אמיתיים.
(ולא, אני לא חושב שצריך להתחיל לעבוד בלי לדעת מה אנחנו עושים. חשוב לבדוק דברים ולהתאים בחירות טכניות למוצר, אבל זה תהליך שחייבים לתחום אותו בזמן, ולא משנה כמה זמן יש לכם בסוף תצטרכו לחיות עם זה שחלק מהבחירות הטכנולוגיות יהיו שגויות).
