טיפ SQL: החברים של בוב
אחד הדברים שלא נכנסו לקורס SQL החדש שכתבתי היה שאילתות רקורסיביות. וכן מאוד רציתי להקליט עליהן שיעור אבל הרגשתי שזה יותר מדי בשביל קורס SQL בסיסי, אז בואו נראה אותן בפוסט במקום.
טיפים קצרים וחדשות למתכנתים
אחד הדברים שלא נכנסו לקורס SQL החדש שכתבתי היה שאילתות רקורסיביות. וכן מאוד רציתי להקליט עליהן שיעור אבל הרגשתי שזה יותר מדי בשביל קורס SQL בסיסי, אז בואו נראה אותן בפוסט במקום.
הי חברים.
מה לא הייתי עושה בשביל לא ללמוד SQL ... למדתי ריילס ואת ה ORM שלהם ושכנעתי את עצמי ש ORM זה הדבר וממילא אף אחד כבר לא כותב SQL; למדתי לעבוד עם מונגו ואז עם סייפר וגרמלין ואפילו דטהלוג ושכנעתי את עצמי ש NoSQL זה הדבר ותכף אף אחד לא יצטרך יותר SQL; ואפילו שמחתי כש ChatGPT התחיל לכתוב שאילתות וראיתי איך ממש תוך רגע נוכל לקבל שאילתות מהירות וטובות יותר ממה שאפשר לכתוב לבד.
וכמו איזה גירוד מציק שמסרב להיעלם ה SQL תמיד חוזר ומזכיר לך שיש דרך קלה להוציא מידע מבסיסי נתונים. שבעצם זו רק עוד שפה עם כללי הדיקדוק שלה והעקרונות שלה וששליטה בה יכולה לשפר משמעותית את איכות הקוד שאנחנו כותבים - בין אם אנחנו משתמשים ב ORM, בין אם אנחנו נעזרים ב ChatGPT ואפילו כשאנחנו עובדים עם בסיסי נתונים שלא משתמשים ב SQL.
ועם ההבנה הזאת ישבתי לכתוב ולהקליט קורס SQL מיוחד למתחילים ומתחילות שמתאים גם לאנשים בלי שום רקע קודם בתכנות. בקורס תלמדו מהצעד הראשון למה חשוב לשמור את המידע בצורה מאורגנת, מהי טבלה ואיך היא עוזרת לשמור מידע והכי חשוב תלמדו את התחביר של שפת SQL ואיתו תדברו עם הנתונים שלכם. וכן על הדרך אנחנו מדברים על ההבדלים בין בסיסי הנתונים השונים, ואיך SQL מאפשרת את השוני. הקורס הוא המשך והרחבה של סידרת פוסטים שכתבתי בנושא, ואם אהבתם את הסידרה אני בטוח שתאהבו גם את הקורס.
הפרק הראשון חופשי להתרשמות, המשך הקורס במסגרת תוכנית המנויים (רק 117 ש"ח לחודש לגישה חופשית לכל תכני האתר). פרטים והרשמה בקישור:
תמיד היו אנשים שהיו יותר טובים בחיפוש בגוגל מאחרים. תמיד אם ידעת מה לחפש קיבלת תוצאות טובות יותר ומהר יותר. תמיד היו שטויות באינטרנט ואנשים שלא מספיק מבינים האמינו להן.
ובכל זאת העבודה עם AI רק מחדדת את החשיבות של אותו "ידע מינימלי". בדיוק בגלל שהשטות נראית בדיוק כמו הדבר הנכון, ובדיוק בגלל שלפחות בתחום שלנו - השטות הרבה פעמים עובדת. זה לא סתם ש AI מצליח לכתוב דוגמאות קוד קטנות אבל לא מצליח לעשות עבודה אמיתית על פרויקט גדול (וכן Cursor AI מרשים ביותר ובדיוק ממחיש את הפער בין הרצוי למצוי). אנחנו לא שם.
ואם AI לא מבין (לא יכול להבין) את ההשלכות של הדברים שהוא כותב, ולא כולל תפיסת עומק של הבעיה, הקונטקסט וכל האילוצים סביבה, אז מישהו אחר (אנחנו) יהיה חייב להשלים את החסר. ידע מינימלי הכרחי הוא הידע המינימלי שאנחנו צריכים על אודות נושא מסוים כדי לתפעל מנוע AI ולהגיע לתוצאות טובות.
דוגמה פשוטה בריאקט היא הקוד הזה ש Chat GPT הציע לי כדי להציג את ימות השבוע ותיבת חיפוש על הנתונים:
import React, { useState } from 'react';
const WeekDaysList = () => {
const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const [searchTerm, setSearchTerm] = useState('');
const handleSearchChange = (event) => {
setSearchTerm(event.target.value);
};
const filteredDays = daysOfWeek.filter(day =>
day.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div>
<h2>Days of the Week</h2>
<input
type="text"
placeholder="Search days..."
value={searchTerm}
onChange={handleSearchChange}
/>
<ul>
{filteredDays.map((day, index) => (
<li key={index}>{day}</li>
))}
</ul>
</div>
);
};
export default WeekDaysList;
אם אני לא אשאל אותו במפורש על ה keys ועל הבעיות בשימוש באינדקס בתור מפתח שם הוא לא יחשוב להציף את זה - פשוט בגלל שהוא לא חושב. זה בסדר בשביל מנוע AI, אבל לא בסדר בשביל קוד אמיתי. בהיבט של ה HTML אם אני לא יודע לבקש טיפים על נגישות ה AI לא יחשוב להשתמש ב label בשביל להציג את הכותרת.
במיוחד עכשיו שאפשר לקבל כל כך הרבה עבור פרומפט הרבה יותר חשוב לדעת מה לבקש.
הרבה זמן חשבתי שבגלל ש File יורש מ IO, אין שום בעיה לקרוא ל File.read או ל IO.read ותמיד מקבלים אותה תוצאה. מבחן הניסיון גם הראה לי תמיד שאני צודק לדוגמה:
3.1.1 :003 > File.read('/etc/shells')
=> "# List of acceptable shells for chpass(1).\n# Ftpd will not allow users to connect who are not using\n# one of these shells.\n\n/bin/bash\n/bin/csh\n/bin/dash\n/bin/ksh\n/bin/sh\n/bin/tcsh\n/bin/zsh\n/usr/local/bin/pwsh\n/usr/local/bin/bash\n/usr/local/bin/zsh\n"
3.1.1 :004 > IO.read('/etc/shells')
=> "# List of acceptable shells for chpass(1).\n# Ftpd will not allow users to connect who are not using\n# one of these shells.\n\n/bin/bash\n/bin/csh\n/bin/dash\n/bin/ksh\n/bin/sh\n/bin/tcsh\n/bin/zsh\n/usr/local/bin/pwsh\n/usr/local/bin/bash\n/usr/local/bin/zsh\n"
אז למה בכל זאת רובוקופ התרגז עליי כשכתבתי IO.read? מסתבר של IO.read יש פונקציונאליות נסתרת - אם נפעיל אותו עם קו אנכי ואחריו שם של פקודה, הוא יפעיל את הפקודה וייתן לנו גישה לפלט שלה, לדוגמה:
3.1.1 :007 > IO.read('|whoami')
=> "ynonp\n"
3.1.1 :008 > File.read('|whoami')
(irb):8:in `read': No such file or directory @ rb_sysopen - |whoami (Errno::ENOENT)
from (irb):8:in `<main>'
from /Users/ynonp/.rvm/rubies/ruby-3.1.1/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
from /Users/ynonp/.rvm/rubies/ruby-3.1.1/bin/irb:25:in `load'
from /Users/ynonp/.rvm/rubies/ruby-3.1.1/bin/irb:25:in `<main>'
בעולם האמיתי אף פעם אי אפשר לדעת מאיפה הגיע שם הקובץ אותו רוצים לפתוח, ולכן כשהמטרה היא לפתוח קובץ תמיד נעדיף להשתמש בפונקציה שלא תוכל להריץ תוכנית בטעות.
כשאנשים רואים בפעם הראשונה איך להשתמש ב fetch כלומר את הקוד הזה:
const res = await fetch(url);
const data = await res.json();
חלק קטן מהם ישאל "למה צריך await פעמיים". חלק מאלה ששאלו יסתפקו בתשובה "כי json מחזיר Promise", ואולי אפילו יגלו שאפשר לוותר על await אחד אם יכתבו:
const res = await fetch(url).then(r => r.json());
אבל רק מעטים ישאלו את השאלה החשובה - למה json צריך להחזיר Promise, אם JSON.parse
הוא סינכרוני?
מרגע ששאלתם את השאלה מאוד קל לגלות את התשובה. היא גם בתיעוד וגם Chat GPT יודע לתת הסבר די טוב. אבל לא התיעוד ולא ChatGPT יעזרו אם לא נשאל אותם. כן אפילו שהכלים נהיים יותר חכמים כל יום, ואולי במיוחד לאור עובדה זו, אנחנו צריכים להמשיך ולהיות יותר סקרנים ויותר מקצועיים ממה שאי פעם היינו.
נ.ב. קרדיט לטום באינטרנט על ההשראה.
ג'אסט הוא כלי להרצת משימות לפרויקט. הוא עובד עם כל שפה ויש לו תחביר שקצת דומה ל make, רק שבעוד ש make נועד ספציפית לבנות מודולים לפרויקט, ג'אסט הוא יותר כללי במטרה שלו ומכוון להרצת משימות וניהול התלויות ביניהן.
בואו נראה דוגמה קצרה איך just יכול לעזור לנו בפרויקט node כש npm מתחיל להסתבך. נניח שיש לי קובץ package.json כזה:
{
"name": "demo-project",
"version": "1.0.0",
"description": "A simple demo project",
"main": "index.js",
"scripts": {
"start": "npm run build && NODE_ENV=production node index.js",
"build": "echo building the app",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^2.0.22",
"babel-cli": "^6.26.0"
}
}
שימו לב לבלוק הסקריפטים ובמיוחד לסקריפט start:
"start": "npm run build && NODE_ENV=production node index.js",
הסקריפט מריץ שני דברים, קודם את build ואחרי זה מפעיל את התוכנית. הוא יפעיל את התוכנית רק אם build הצליח ובהפעלת התוכנית הוא יגדיר משתנה סביבה בשם NODE_ENV
. זה סקריפט שנועד להפעלה רק על מערכות יוניקס ואין בו באמת ניהול תלויות למרות שזה קצת נראה כמו. נדמיין שמישהו יוסיף עוד סקריפט לקובץ:
publish: "npm run build && npm run start"
אז במצב כזה ה build ירוץ פעמיים, כי publish יקרא לו ואז start גם יפעיל אותו.
כשה npm מתחיל להסתבך, just יכול להציל את המצב. ג'אסט הוא כלי שרץ משורת הפקודה, אבל אנחנו יכולים להוסיף אותו לקובץ ה package.json בתור תלות נוספת לפיתוח כדי שאנשים לא יצטרכו להתקין אותו בנפרד:
"devDependencies": {
"just-install": "2.0.2",
}
אחרי שהוספנו והתקנו אותו אנחנו יוצרים קובץ בשם justfile
בתיקייה הראשית של הפרויקט עם תוכן שנראה כמו Makefile:
default:
just --list
start: build
NODE_ENV=production
node index.js
build:
echo building app
ועכשיו אפשר למחוק את כל בלוק הסקריפטים מ package.json ולעבור לעבוד עם just. אני מריץ:
$ npx just
just --list
Available recipes:
build
default
start
כדי לראות את רשימת האפשרויות, ואז בשביל המשחק אני מריץ:
$ npx just start
echo building app
building app
NODE_ENV=production
node index.js
starting app
ואני רואה ש start הריץ גם את build וגם את פקודת ה node. בשביל להוסיף סקריפט publish ל justfile אני כותב:
publish: build start
echo publish
ובהפעלה ברור ש build רץ רק פעם אחת:
$ npx just publish
echo building app
building app
NODE_ENV=production
node index.js
starting app
echo publish
publish
התיעוד של just מצוין ויש לו הרבה פחות פינות חדות בהשוואה ל make וגם ChatGPT מכיר אותו די טוב אז לא תהיה לכם בעיה להיעזר ב AI כשצריך. אפשר לקרוא יותר על הכלי בגיטהאב שלהם בקישור: https://github.com/casey/just/
בדף התיעוד של דינו יש רשימה של Web Frameworks שדינו תומך בהם ואני מודה שהפריט הראשון ברשימה בלבל אותי. כלומר שמעתי את אנשי דינו מדברים על תמיכה ב next.js אבל לא הבנתי שהכל כבר מוכן, ואם זה מוכן לא ברור למה האנשים ב vercel לא נותנים יותר פירסום לסיפור. אני מפרסם פה כמה ניסויים שעשיתי עם השילוב, לצערי בלי מסקנות מלהיבות.
האתגר הכי גדול עבורי עם קלוז'ר הוא חוסר העקביות של השפה, או שאולי יש לה איזשהו הגיון פנימי שאני לא מבין. הקיטור היום הוא על הפונקציות contains?
ו keys
.
הפונקציה keys
בקלוז'ר מקבלת מפה ומחזירה את המפתחות שלה. זה קל:
user=> (keys {:a 10 :b 20 })
(:a :b)
ברור שאי אפשר להשתמש ב keys על וקטור, כי אין לו מפתחות. קלוז'ר היא הרי לא JavaScript והיא לא חושבת שמפתחות של מערך זה האינדקסים שלו. זה מה שקורה אם מנסים:
user=> (keys ["a" "b" "c"])
Error printing return value (ClassCastException) at clojure.lang.APersistentMap$KeySeq/first (APersistentMap.java:168).
class java.lang.String cannot be cast to class java.util.Map$Entry (java.lang.String and java.util.Map$Entry are in module java.base of loader 'bootstrap')
בהתבסס על ההגיון הזה הלכתי לשחק עם הפונקציה contains?
בעבודה על מפה, הפונקציה contains?
בודקת אם ערך מסוים הוא מפתח במפה. עד פה הכל גם הגיוני:
user=> (contains? {:a 10 :b 20} :a)
true
בגלל ש keys לא עובדת על מערכים, חשבתי שגם contains?
לא אמורה לעבוד על מערכים, וקצת הופתעתי לגלות שהיא דווקא עובדת ואפילו לפעמים צודקת:
user=> (contains? [1 2 3] 2)
true
אבל זה היה רק מוקש. למעשה contains?
על מערכים בקלוז'ר בודקת אם הפרמטר שהיא קיבלה הוא אינדקס במערך - ולא ערך. הדוגמה הקודמת עבדה רק במקרה. הנה כמה דוגמאות יותר טובות:
user=> (contains? [1 2 3] 0)
true
user=> (contains? [1 2 3] 3)
false
user=> (contains? ['a' 'b' 'c'] 0)
true
user=> (contains? ['a' 'b' 'c'] 'a')
false
user=> (contains? ['a' 'b' 'c'] 9)
false
ונשארתי עם השאלה - אם האינדקסים של מערך הם המפתחות שלו, למה keys
לא מחזירה את רשימת האינדקסים? ואם הם לא מפתחות, למה contains?
בודקת אם ערך שהיא קיבלה הוא אינדקס חוקי במערך?
על הרעיון הבא חשבתי אחרי שבזבזתי שוב יותר מדי זמן על להבין למה התוכנית לא פועלת כמו שצריך. אני בטוח שהוא יהיה להיט אבל לא כל כך בטוח איך לממש אותו, אז זורק אותו פה בתקווה שאחד או אחת מקוראיי האמיצים ירימו את הכפפה. המטרה של התוסף היא להפעיל Debugger, לשים נקודת עצירה ואז להתחיל לדבר עם Chat GPT ולשאול שאלות לגבי מצב העניינים בתוכנית (כמו "למה יש null ב name?")
ב VS Code אפשר לכתוב תוסף שמתממשק עם ה Debugger, למשל דרך https://code.visualstudio.com/api/references/vscode-api#DebugAdapterTracker
מתוך ה Callbacks אפשר לגלות מה הקוד שבו עצרנו, מה ערכי המשתנים ומה ה Stack Trace ברגע העצירה.
בעזרת היוריסטיקות מתוחכמות מחליטים איזה שורות וקבצים שולחים ל Chat GPT. הכי טוב היה לשלוח את כל הפרויקט אבל לא נראה לי שזה אפשרי או רצוי מבחינת ניצול טוקנים. בנוסף לקוד נשלח גם את ערכי המשתנים, ה Stack Trace וכל מידע אחר שאפשר יהיה להוציא.
מוסיפים תיבת Chat לחלון הדיבאג ששולחת שאלות ל Chat GPT ומציגה את התשובות בחלונית.
נ.ב. יש כבר כלי כזה שכולם משתמשים בו ורק אני לא הכרתי? שתפו בתגובות. כתבתם כלי כזה ואתם רוצים לפרסם אותו? השאירו הודעה ותקבלו פירסום בחינם ותהילת עולם.
נ.ב.ב. באותו נושא cursor ai לוקח כיוון אחר אבל סופר מעניין. אם לא ניסיתם אותו מאוד מומלץ. זה נראה כמו VS Code משולב עם חלון שיחה עם AI, וה AI ממש רץ על כל הקבצים ומעדכן את הקוד לפי הבקשות שלכם. אולי יום אחד הם יוסיפו תמיכה ב Debugger. בכל מקרה מאחר והם לא קוד פתוח זה לא עוזר בינתיים.
מצאתי השבוע באג מעניין פה באתר בפונקציה שתמיד נראתה לי כמו קסם. הסיפור הוא פשוט, לפעמים אנשים קונים מנוי לאתר עבור עובדים שלהם ואז העובד מקבל קישור מיוחד להרשמה. כשהעובד נרשם דרך הקישור הוא מחובר לאותו חשבון חברה שכבר נקנה עבורו במרוכז ויכול לגשת לקורסים. הקוד שטיפל ביצירת בחיבור העובד לקוד המיוחד הוא בסך הכל השורות:
create_user(!!params[:quickjoin]) do |user|
user.after_confirmation
end
נו, למעשה המימוש נמצא בתוך הבלוק after_confirmation
אבל זה לא חשוב לשיחה שלנו כרגע. ועכשיו שאני קורא את זה אני גם לא בטוח מה תפקיד הפרמטר quickjoin, אבל גם זה לא חשוב לשיחה שלנו כרגע.
מה שכן חשוב וקצת קשה לראות מהמבנה זה ש create_user
יכולה להיכשל. למשל אם מישהו בחר אימייל שכבר קיים במערכת, לא בחר סיסמה או לא בחר שם משתמש. במצב כזה הבלוק הפנימי לא נקרא וזה ברור כי לא נוצר משתמש. הקוד שמופעל אחרי כישלון ביצירת משתמש מתחבא בתוך הפונקציה create_user
והוא השורות:
clean_up_passwords resource
set_minimum_password_length
return redirect_to quickjoin_path, alert: resource.errors.full_messages.join(', ') if params[:quickjoin] == '1'
respond_with resource
בפרט מה שמעניין אותנו זו השורה האחרונה - respond_with
. היא מעניינת כי היא עובדת ממש בסדר לכל המשתמשים שנרשמים רגיל לאתר. כשיש כישלון ברישום היא מחזירה את הנרשם לדף הרישום הראשי שם הוא יכול לתקן את השגיאה ברישום ולנסות שוב. אבל אם מראש הקישור לרישום היה שונה כי הבן אדם הגיע מתוך קישור מיוחד כדי להשתמש בחשבון מתנה, הפונקציה respond_with
לא רואה את זה ומחזירה את הנרשם לדף הרישום הראשי, שם הוא מנסה שוב להירשם אבל החשבון כבר לא מחובר לחשבון שקנו עבורו.
ב Rails הפונקציה respond_with
היא קיצור לזה:
def create
@user = User.new(params[:user])
respond_to do |format|
if @user.save
flash[:notice] = 'User was successfully created.'
format.html { redirect_to(@user) }
format.xml { render xml: @user }
else
format.html { render action: "new" }
format.xml { render xml: @user }
end
end
end
בכתיב המלא הכל ברור - כשיצירת המשתמש נכשלה מציגים את הטופס מ new, ולא מהקישור המיוחד דרכו הוא ניסה להירשם. ברישום הבא המשתמש כבר ייכנס לאתר כמשתמש רגיל ולא מחובר לחשבון שהוכן עבורו.
תיקונים? קל. קודם כל ב create_user
עדיף להחליף את respond_with
בגירסה הארוכה שלה, ואז בכישלון גם להפעיל redirect לקישור ממנו הוא ניסה להירשם. בנוסף להוסיף בדיקות צד-לקוח בטופס הרישום. בדיקות אלה אומנם לא יתפסו את כל הבעיות אבל ישפרו את החוויה למי ששכח למלא תוכן באחד השדות.