1 417
订阅者
-224 小时
-37 天
-630 天
帖子存档
1 417
למה אנחנו לא מצליחים ללמוד תוך כדי עבודה
תסתכלו על עצמכם היום ועל עצמכם של לפני חמש שנים ותענו בכנות, האם החמש שנים האחרונות היו באמת חמש שנים ניסיון, או שנה אחת שחזרה על עצמה חמש פעמים? הנה כמה סיבות בגללן התשובה השניה עשויה להיות נכונה:
1. הסטאק הטכנולוגי במקום עבודה צריך להתאים לפרויקטים שכבר יש שם. נכון מדי פעם אפשר להכניס כלי חדש, אבל ארכיטקטורה חדשה לגמרי מאפס שלא בטוח תעבוד? פחות קורה (מיקרו סרביסס קצת איפשרו לנו לרוץ קדימה עם טכנולוגיה גם בחברות וותיקות, אבל גם שם הדברים מתכנסים. אף אחד לא רוצה שכל מיקרו סרביס יהיה כתוב בשפה אחרת).
2. מי שעושה לך Code Review בעבודה לא ממש כותב קוד - מנהלים החל מדרגה מסוימת מפסיקים לכתוב קוד, ולכן קשה להם להביא Insights לגבי שיטות עבודה חדשות שאולי יכולות לעזור.
3. חלוקה לצוותים ולמשימות - מקום עבודה צריך לייצר קוד ולכן טבעי שאנשים שטובים במשהו ימצאו את עצמם יותר במשבצת בה הם טובים. יודעת לכתוב שאילתות? בואי תבני קוד צד שרת; יודעת לבנות ממשקים? יש מקום בצוות הפרונט. אבל בפרויקט תוכנה כל ההיבטים של הפיתוח קשורים אחד לשני ולפעמים ההתקדמות הנחוצה בניסיון תבוא דווקא מעבודה על הדבר שאת עדיין לא מספיק טובה בו.
4. חלוקה לצוותים ולמשימות 2 - ניסיון עשיר אומר שראית הרבה מקרים ואתה מצליח לזהות תבניות ולהסיק מסקנות מכל המגוון, מהדומה ומהשונה. במקום עבודה יש לנו פרויקט או כמה פרויקטים מרכזיים שנכתבו במתודולוגיה דומה. אפילו AI לא מצליח ללמוד כשאין מספיק קלטים.
כן מתכנתים לומדים דרך פיתוח פרויקטים, כן המדד של שנות ניסיון הוא חשוב אבל הרבה פעמים לא מספיק. בשביל ללמוד אנחנו רוצים לגלות דברים חדשים, להתמודד עם אתגרים וכן לטלטל את הסירה כדי להבין איזה חלקים יציבים ואיזה חלקים דורשים חיזוק.
היום סיימנו את השבוע הראשון בקורס ליווי פרויקטים. העבודה על הקורס ועם המפתחים המוכשרים שנכנסו למחזור הראשון מלמדת אותי המון על פרויקטים, פיתוח ואיך באמת לגדול כמפתחים. המחזור הבא ייפתח ביוני והרישום אליו ייפתח באפריל. שווה לשמור את התאריך.
1 417
ref={measuredRef}
className="h-screen bg-amber-600"
>
I am {Math.round(height)}px tall
</div>
)
}
שימו לב לשתי תופעות מעניינות:
1. בעליית העמוד אני מקבל את ההודעה Creating the Observer, אחריה Disconnect ואחריה שוב Creating the Observer.
2. אחרי כל שינוי גודל אני מקבל הודעת Disconnect ואז שוב Creating the Observer.
הסיפור הראשון פחות מטריד - במצב פיתוח ריאקט "בועט" קצת בקוד שלנו כדי ששגיאות קטנות יהפכו בולטות יותר. מבחינת ריאקט בסוף Callback Ref חייב להופיע קוד ניקוי ובשביל לבדוק את זה הוא מפעיל את ה Callback Ref ואז מנקה ואז מפעיל עוד פעם רק בשביל לראות שהכל עובד ולא שכחנו כלום.
אבל למה החיבור נוצר מחדש אחרי כל שינוי גודל?
שימוש ב useCallback כדי לא ליצור את האפקט מחדש אחרי כל שינוי
שימו לב ל JSX הבא:
<div
ref={measuredRef}
className="h-screen bg-amber-600"
>
אנחנו יוצרים div ומעבירים לו שני דברים, את ה ref callback ומחרוזת בתור className. עכשיו צריך לספר משהו על ריאקט, אלמנטים ב DOM ומאפיין ref: אם ה Callback Ref משתנה ב Render, כלומר ריאקט מפעיל את פונקציית הקומפוננטה, מקבל JSX עם Callback Ref שונה ממה שהיה פעם קודמת שהוא הפעיל את פונקציית הקומפוננטה, אז ריאקט ינתק את האלמנט מהמסך, יפעיל את קוד הניקוי של ה Callback Ref ששמור לו ויריץ את ה Callback Ref החדש.
אבל רגע, מה פתאום "משנים את ה Callback Ref" אתם שואלים, הרי אנחנו רואים שזה תמיד אותה פונקציה, measureRef. נכון?
לא מדויק. כשריאקט מפעיל את פונקציית הקומפוננטה MeasureExample הוא מבצע את כל הפקודות בתוך הפונקציה. הפקודה:
const measureRef = ( ... ) => { ... };
יוצרת פונקציה אנונימית חדשה ומגדירה את המשתנה measureRef שיצביע על אותה פונקציה. בפעם הבאה שיהיה render, תיווצר שוב פונקציה אנונימית חדשה שתישמר לתוך המשתנה measureRef ותעבור להיות ה ref החדש של ה div. נכון, הפונקציה החדשה תהיה ממש זהה לפונקציה הקודמת, אבל זה לא הופך אותה לפחות חדשה. אתם יכולים לעשות את הניסוי הבא בשביל לראות את התופעה:
const a = () => 2;
const b = () => 2;
console.log(a == b)
ותגלו שהמשתנים לא שווים - בכל משתנה יש פונקציה אנונימית אחרת שבמקרה שתי הפונקציות עושות בדיוק אותו דבר ומחזירות 2.
בשביל שריאקט לא יחשוב שאנחנו "מחליפים" את ה Callback Ref עלינו לשמור את הפונקציה באיזשהו מקום מחבוא כך שכל פעם שיהיה Render נוסף ניקח אותה מהמחבוא ונשתמש בה ולא נייצר אחת חדשה. לריאקט יש מנגנון מובנה לשמור פונקציות בצד בדיוק למצבים האלה וזה נקרא useCallback.
פונקציית useCallback של ריאקט מקבלת פונקציה אחרת ורשימה של דברים שאם הם משתנים צריך להחליף את הפונקציה. כל עוד הדברים ברשימה שהעברנו בפרמטר השני לא השתנו useCallback יחזיר בדיוק את אותה פונקציה שהוא קיבל ב render הראשון. הקוד המתוקן עם useCallback נראה ככה:
'use client';
import { useState, useCallback } from "react";
export default function MeasureExample() {
const [height, setHeight] = useState(0)
const measuredRef = useCallback((node: HTMLElement|null) => {
if (!node) { throw new Error("Should not be here")}
console.log(\Creating the Observer\);
// only one entry - because we only "observe" one node
const observer = new ResizeObserver(([entry]) => {
setHeight(entry.contentRect.height)
})
observer.observe(node)
return () => {
console.log(\Disconnect\);
observer.disconnect()
}
}, [setHeight]);
return (
<div
ref={measuredRef}
className="h-screen bg-amber-600"
>
I am {Math.round(height)}px tall
</div>
)
}
שימו לב שרשמתי את setHeight ברשימת התלויות של useCallback. הפונקציה הפנימית משתמשת ב setHeight ולכן אם setHeight תשתנה יש ליצור מחדש את הפונקציה הפנימית. אבל אתם יכולים להיות רגועים, פונקציית setter שחוזרת מ useState תמיד תישאר זהה.
וניסוי למתקדמים
עדכנו את הקוד והפכו את ה Ref Callback לכתיב useEffect. מה ההבדל בין השניים? מתי תעדיפו להשתמש בכל כתיב?1 417
ניקוי אחרי Ref Callback בריאקט
פונקציות Ref Callback יכולות להיות תחליף טוב ויעיל ל useEffect, וכמו useEffect גם להן יש כמה פינות חדות. בואו נראה דוגמה ומה יכול להשתבש.
תזכורת: ResizeObserver
ה API בדוגמה זו נקרא ResizeObserver. זה מנגנון שמאפשר לנו לקבל אירועים כל פעם שגודל של אלמנט ב DOM משתנה, אם כתוצאה משינוי גודל חלון או בגלל שינוי ב CSS או כל סיבה אחרת. ה ResizeObserver מקבל את האלמנט ופונקציית טיפול ויפעיל את הפונקציה כל פעם שהגודל משתנה.
אנחנו משתמשים ב ResizeObserver באופן הבא:
1. יוצרים ResizeObserver חדש עם הפקודה:
const observer = new ResizeObserver(...)
2. בתור פרמטר ל ResizeObserver אנחנו מעבירים פונקציית טיפול בשינוי גודל. הפונקציה מקבלת כקלט מערך של פרטי מידע על אלמנטים שה Observer הסתכל עליהם וגודלם השתנה. לכל פריט מידע יש שדה בשם contentRect שמכיל את הגודל שלו:
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log(\Element resized to: ${width} × ${height}\);
}
});
3. בשביל להתחיל להקשיב לשינויי גודל אנחנו קוראים לפונקציה observe ומעבירים לה אלמנט ב DOM אליו אנחנו רוצים להאזין:
observer.observe(targetElement);
4. אחרי שאנחנו מסיימים להאזין עלינו להפעיל disconnect כדי לנקות את הזיכרון ולהפסיק לקרוא לפונקציה כשגודל האלמנט משתנה:
observer.disconnect();
חיבור ResizeObserver לריאקט
בשביל לחבר ResizeObserver לקומפוננטה בריאקט נוכל להשתמש ב Ref Callback. בעת יצירת הקומפוננטה נבנה את ה ResizeObserver, נגדיר פונקציית טיפול בשינוי גודל שתכתוב את גודל האלמנט לתוך משתנה סטייט ונציג את תוכן משתנה הסטייט. זה הקוד:
'use client';
import { useState } from "react";
export default function MeasureExample() {
const [height, setHeight] = useState(0)
const measuredRef = (node: HTMLElement|null) => {
if (!node) { throw new Error("Should not be here")}
// only one entry - because we only "observe" one node
const observer = new ResizeObserver(([entry]) => {
setHeight(entry.contentRect.height)
})
observer.observe(node)
return () => {
observer.disconnect()
}
}
return (
<div
ref={measuredRef}
className="h-screen bg-amber-600"
>
I am {Math.round(height)}px tall
</div>
)
}
שימו לב שפונקציית ה Ref Callback מחזירה פונקציה. מבנה זה של פונקציית Ref Callback שמחזירה פונקציה אומר לריאקט שאנחנו צריכים להריץ קוד ניקוי אם במקרה הקומפוננטה תעזוב את המסך או אם ה div שעליו ה Ref Callback הזה מחובר יצא מה DOM. בכל מקרה של Ref Callback שיוצר חיבור ארוך טווח עם אוביקט חיצוני עלינו לדאוג לנקות אחרינו.
למה האלמנט יכול להיות null?
החתימה של הפונקציה מבלבלת - הפונקציה יכולה לקבל אלמנט DOM או null, אבל בתחילת הפונקציה אני מוודא שלא קיבלתי null. הסיבה היא שהחתימה של Callback Ref השתנתה בגירסה 19 של ריאקט. עד גירסה 19 לא היתה אפשרות להחזיר פונקציית ניקוי והיינו צריכים בקוד ה Callback Ref לבדוק אם קיבלנו null ואם כן לנקות את החיבור. בריאקט 19 נכנסה האפשרות להחזיר פונקציית ניקוי, וכשמחזירים פונקציית ניקוי ריאקט לא יפעיל את ה Callback Ref עם null ובמקום זה יקרא לפונקציית הניקוי. כתיב פונקציית הניקוי הרבה יותר נוח כי הוא מאפשר להשתמש באותו משתנה observer שכבר הגדרתי בפונקציה כדי לבטל את החיבור.
כמה פעמים הפונקציה מופעלת? מתי מופעל הניקוי?
הקוד עובד אבל כולל רמאות. בואו נוסיף הודעות הדפסה כדי לראות אותה:
'use client';
import { useState } from "react";
export default function MeasureExample() {
const [height, setHeight] = useState(0)
const measuredRef = (node: HTMLElement|null) => {
if (!node) { throw new Error("Should not be here")}
console.log(\Creating the Observer\);
// only one entry - because we only "observe" one node
const observer = new ResizeObserver(([entry]) => {
setHeight(entry.contentRect.height)
})
observer.observe(node)
return () => {
console.log(\Disconnect\);
observer.disconnect()
}
}
return (
<div1 417
אינפלציה של סוכני AI. או בעצם ...
שנים חיינו עם מנוע חיפוש אחד, וחשבנו שאם רק היו יותר מנועי חיפוש היה אפשר לקבל מגוון יותר גדול של דעות ברשת, ואולי לראות אתרים שדוד גוגל החליט לשים בתחתית הרשימה.
עם AI לכאורה אין לנו את הבעיה הזאת. בזריקה מהירה מהראש כשיש לי בעיה היום אני יכול להתייעץ עם Claude, grok, gemini, ChatGPT, perplexity, Copilot, DeepSeek וזה לפני שמחפשים סוכני AI מותאמים אישית לבעיות. כל AI מיוצר על ידי חברה אחרת, מאומן בדרך שונה ועל מידע שונה, ולכן אמור לתת תוצאות שונות לשאלות.
המציאות כמו שאתם מכירים קצת יותר מתסכלת. יש שאלות שאחד מהם מצליח יותר מהאחרים, אבל רוב הזמן התשובות מאוד דומות. ניסיתי לתת תרגיל ריאקט ל 6 סוכני AI שונים וכולם פתרו אותו עם אותו באג. מה אני לומד מהסיפור?
1. כדאי להחליף חברי AI מדי פעם, אבל לא בטוח שיש טעם "לחקור" בעיה באמצעות שליחתה לעוד ועוד מנועים.
2. רצוי לתת למנועים לעשות Code Review על קוד שלהם או של חבריהם. לפעמים הם מצליחים לזהות את הבאגים של עצמם ככה. לפעמים הם רק יוצרים באגים חדשים.
והשאלה הכי קשה היא כמה אני רוצה להשקיע בללמוד נושא מסוים בהינתן שהבוטים של ה AI מסוגלים לפתור בו בעיות פשוטות בצורה סבירה. אם אני יכול לתת לגרוק לכתוב לי קוד ששומר קובץ ל S3, אפילו אם זה לא יוצא הקוד הכי יעיל, האם שווה עדיין לקרוא את כל דף התיעוד של אמזון על S3? אין לי תשובה אבל ברור שמודל AI נוסף או חדש לא יעזור להחליט פה.
1 417
דריזל נותן ביצועים מצוינים, שליטה טובה על ה SQL שמבוצע וחווית פיתוח ממש טובה למפתחים. למי שמכיר SQL עקומת הלמידה לא מסובכת וחברינו הרובוטים רוב הזמן מצליחים לתרגם מדריזל ל SQL בלי בעיה.
קוד הדוגמה המלא שהופיע בפוסט זמין בגיטהאב בקישור:
https://github.com/ynonp/drizzle-intro
אלה מכם שאוהבים השוואות מוזמנים לקרוא גם פוסט שכתבתי על knex בקישור הזה:
https://www.tocode.co.il/blog/2021-02-knex-14-examples
ופוסט על Sequelize כאן:
https://www.tocode.co.il/blog/2019-07-sequelize
1 417
export async function saveCard(user: SelectUser, front: string, back: string) {
return db.insert(vocabularyCardsTable).values({
userId: user.id,
front,
back,
nextReviewDate: sql\current_timestamp\
})
}
export async function findUserById(userId: number): Promise<SelectUser> {
const user = await db.query.usersTable.findFirst({where: eq(usersTable.id, userId)});
if (!user) {
throw new Error(\User not found: ${userId}\);
}
return user;
}
export async function findUserByName(userName: string): Promise<SelectUser> {
const user = await db.query.usersTable.findFirst({where: eq(usersTable.name, userName)});
if (!user) {
throw new Error(\User not found: ${userName}\);
}
return user;
}
אני מוחק את בסיס הנתונים ומפעיל את קובץ ה seed מחדש הפעם עם הנתונים של שתי הטבלאות:
$ rm local.db
$ npx drizzle-kit push
$ npx tsx --env-file=.env src/db/seed.ts
הצגת כל המילים והפירושים של משתמש
נמשיך לעוד דוגמה לשאילתה ודרכה נדבר על קשר בין טבלאות בדריזל - בדוגמה הבאה אני רוצה למצוא משתמשים עם המילים שלהם, כלומר לבצע JOIN בין הטבלאות. שימוש ב select ב drizzle יעבוד והשאילתה הבאה תחזיר לי את כל המילים של המשתמש user1:
db
.select({
userId: users.id,
front: vocabularyCards.front,
back: vocabularyCards.back,
})
.from(users)
.where(eq(users.name, 'user1'))
.innerJoin(vocabularyCards, eq(users.id, vocabularyCards.userId))
וקל לראות שאם נוריד את ה where נוכל לקבל את כל המשתמשים וכל המילים שלהם. ויש עוד דרך. דריזל מאפשר לי להגדיר בסכימה יחס בין שתי הטבלאות, כלומר להגדיר ששדה user_id בטבלת המילים מתאים לשדה id בטבלת המשתמשים.
אבל גם ל Join יש קיצור דרך בעזרת db.query. בשביל להשתמש בו עלינו להרחיב את הסכימה ולעדכן את דריזל על הקשר בין שתי הטבלאות. אני מוסיף את שתי הקריאות הבאות לקובץ הסכימה:
export const usersRelations = relations(usersTable, ({ many }) => ({
words: many(vocabularyCardsTable)
}));
export const vocabularyCardsRelations = relations(vocabularyCardsTable, ({ one }) => ({
user: one(usersTable, {
fields: [vocabularyCardsTable.userId],
references: [usersTable.id]
})
}))
התחביר הוא שוב דריזלי אבל יחסית קל לקריאה - לאוסף המשתמשים אני יוצר "קשר" שנקרא words והוא מתחבר לטבלת אוצר המילים, בטבלת אוצר המילים אני יוצר את הצד השני של הקשר, ומגדיר שלכל מילה יש "משתמש" שמתאים את השדה userId בטבלת המילים לשדה id בטבלת המשתמשים.
אחרי העדכון אני יכול לכתוב את הקוד:
const usersWithWords = await db.query.usersTable.findFirst({
with: {
words: true
}
})
console.log(usersWithWords);
ולקבל את התוצאה:
{
id: 1,
name: 'user1',
words: [
{
id: 1,
userId: 1,
front: 'cat',
back: 'חתול',
nextReviewDate: '2025-03-03 18:04:28',
createdAt: '2025-03-03 18:04:28',
updatedAt: '2025-03-03 18:04:28'
},
{
id: 2,
userId: 1,
front: 'dog',
back: 'כלב',
nextReviewDate: '2025-03-03 18:04:28',
createdAt: '2025-03-03 18:04:28',
updatedAt: '2025-03-03 18:04:28'
},
{
id: 3,
userId: 1,
front: 'why',
back: 'למה',
nextReviewDate: '2025-03-03 18:04:28',
createdAt: '2025-03-03 18:04:28',
updatedAt: '2025-03-03 18:04:28'
}
]
}
הצגת משתמשים שאין להם מילים
שאילתה אחרונה לסיפור שלנו היום היא שליפת כל המשתמשים שלא יצרו עדיין מילים. דרך קלה לעשות את זה ב SQL היא להשתמש ב JOIN שמאלי בין שתי הטבלאות ואז לחפש את השורות בהן vocabulary_words.id הוא NULL. ובדריזל? בדיוק אותו דבר:
const usersWithoutCards = await db
.select()
.from(usersTable)
.leftJoin(vocabularyCardsTable, eq(usersTable.id, vocabularyCardsTable.userId))
.where(isNull(vocabularyCardsTable.id))
.then(rows => rows.map(row => row.users));
console.log(usersWithoutCards);
סיכום1 417
פרקטיקה טובה בעבודה עם בסיסי נתונים היא ליצור קובץ או קבצים עם פונקציות עזר שדרכן ניגש לבסיס הנתונים לפי כללים פשוטים של האפליקציה. בדוגמה שלנו אפשר לדמיין שמשתמשים ירצו לשמור מילים לאוצר המילים שלהם ולכן ארצה פונקציה ששומרת מילה. אולי בהמשך ארצה לבדוק שמשתמש לא שומר את אותה מילה כמה פעמים או לא לאפשר בכלל שמירה של מילים מסוימות, בכל מקרה אני מתחיל ביצירת קובץ של פונקציות גישה לבסיס הנתונים בשם
src/actions/vocabulary.ts, ובו אני כותב את הפונקציה הראשונה ששומרת מילה ופירוש בבסיס הנתונים:
export async function saveCard(userId: number, front: string, back: string) {
return db.insert(vocabularyCardsTable).values({
userId: userId,
front,
back,
nextReviewDate: sql\current_timestamp\
})
}
הפונקציה מקבלת מזהה משתמש, את הטקסט והפירוש שלו וגם מתי בפעם הבאה צריך לעבור על המילה. למילים חדשות אני נותן את הזמן הנוכחי בתור "הפעם הבאה לעבור על המילה", אבל יום אחד כשתהיה מערכת משתמש יוכל לראות מילה ולסמן אם הוא זכר אותה או לא, ואז כשזוכרים מילה נוכל לדחות את הפעם הבאה שצריך לראות אותה לשבוע קדימה (והמתקדמים יוכלו לשמור בבסיס הנתונים כמה פעמים משתמש זכר מילה וכך נוכל לקבוע את מועד הריענון הבא כל פעם למועד רחוק יותר).
לפני שנתקדם בואו נעצור רגע לדבר על החתימה של הפונקציה: הפרמטר הראשון שהיא מקבלת אומנם נקרא userId אבל הוא בסך הכל מספר. טייפסקריפט לא באמת יכול לבדוק שזה מזהה של משתמש וכך יש פוטנציאל לטעויות בקוד שישתמש בפונקציה. דריזל מספק לכל סכימה גם טיפוס נתונים שמתאים לשורה שנשלפה מתוך הטבלה, ועוד טיפוס שמתאים למידע שאפשר להכניס לטבלה. מאחר ואנחנו יודעים ש saveCard אמורה לקבל משתמש, אפשר להחליף את הפרמטר הראשון באוביקט "משתמש" שהגיע מטבלת המשתמשים בעזרת אותו טיפוס של דריזל. בצורה כזאת מי שיפעיל את הפונקציה יהיה חייב להעביר אוביקט שמייצג משתמש, וכך נהיה יותר בטוחים שלא יהיו טעויות בטיפוסים.
את הטיפוסים האוטומטיים אפשר לייצא מקובץ הסכימה באמצעות הוספת השורות הבאות לקובץ schema.ts:
export type SelectUser = typeof usersTable.$inferSelect;
export type InsertUser = typeof usersTable.$inferInsert;
וכן אפשר לייצא אותם לכל טבלה. אחרי הייצוא אני מעדכן את קוד הפונקציה saveCard לגירסה הבאה:
export async function saveCard(user: SelectUser, front: string, back: string) {
return db.insert(vocabularyCardsTable).values({
userId: user.id,
front,
back,
nextReviewDate: sql\current_timestamp\
})
}
שליפות ב Drizzle
ואם כבר אנחנו מייצרים פונקציות עזר בואו נכתוב עוד שתיים כדי למצוא משתמשים לפי מזהה או לפי שם. לדריזל יש שני מנגנונים לשליפת מידע: האחד, בסיסי יותר, הוא פונקציית select. הפונקציה מקבלת שם של טבלה ומאפשרת בנייה של שאילתה באמצעות שרשור פקודות, לדוגמה בשביל למשוך שורה אחת מטבלת המשתמשים לפי שם משתמש אני יכול להשתמש ב:
db
.select()
.from(users)
.where(eq(users.name, 'user1'))
.limit(1);
המבנה השני נקרא query ואני מגיע אליו דרך אוביקט בשם db.query. מבנה זה הוא יותר High Level וכולל קיצורי דרך לפעולות נפוצות של השאילתה כמו חיבור בין טבלאות או משיכת רק תוצאה אחת. אותה שאילתה עם query תהיה:
db
.query
.users
.findFirst({where: eq(users.name, 'user1')})
נשתמש בכתיב השני כדי לכתוב שתי פונקציות עזר למשיכה של משתמשים לפי שם משתמש או מזהה:
export async function findUserById(userId: number): Promise<SelectUser> {
const user = await db.query.usersTable.findFirst({where: eq(usersTable.id, userId)});
if (!user) {
throw new Error(\User not found: ${userId}\);
}
return user;
}
export async function findUserByName(userName: string): Promise<SelectUser> {
const user = await db.query.usersTable.findFirst({where: eq(usersTable.name, userName)});
if (!user) {
throw new Error(\User not found: ${userName}\);
}
return user;
}
וממשיך להשתמש בפונקציה כדי למלא כרטיסים למשתמשים בקובץ seed.ts:
import {db} from '@/db/drizzle';
import { SelectUser, vocabularyCardsTable, usersTable } from '@/db/schema';
import { sql, eq } from 'drizzle-orm';1 417
import { integer, sqliteTable, text} from "drizzle-orm/sqlite-core";
import { sql } from 'drizzle-orm';
export const usersTable = sqliteTable("users", {
id: integer().primaryKey({ autoIncrement: true }),
name: text().notNull(),
});
// Define the vocabulary cards table
export const vocabularyCardsTable = sqliteTable('vocabulary_cards', {
id: integer().primaryKey({ autoIncrement: true }),
userId: integer().notNull().references(() => usersTable.id),
front: text().notNull(),
back: text().notNull(),
nextReviewDate: text().notNull(),
createdAt: text().notNull().default(sql\(current_timestamp)\),
updatedAt: text().notNull().default(sql\(current_timestamp)\),
});
ולסיום בתוך אותה תיקיית db אני יוצר קובץ בשם drizzle.ts שמחבר את הסכימה לדרייבר של SQLite ומייצא אוביקט חיבור לבסיס הנתונים דרכו אוכל להפעיל שאילתות מכל היישום:
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from './schema';
export const db = drizzle(process.env.DB_FILE_NAME!, {schema: schema});
מכאן והלאה בכל מקום שאני רוצה לגשת לבסיס הנתונים אני צריך רק לייבא את אותו db ודרכו אני יכול להפעיל כל פקודת SQL וגם להיעזר באוסף השאילתות והקיצורים של דריזל כדי לכתוב SQL מהר יותר.
קצת דריזל קיט
דריזל קיט הוא החלק מספריית דריזל שאחראי על יצירה ועדכון בסיס הנתונים מתוך הסכימה - כלומר הוא יעזור לנו ליצור את בסיס הנתונים הראשוני ולעדכן את הגדרות הטבלאות כל פעם שאנחנו משנים את הגדרות הטבלאות בסכימה. בשביל להשתמש בדריזל קיט אני צריך ליצור קובץ קונפיגורציה בשם drizzle.config.ts בתיקייה הראשית של הפרויקט. אצלי זה התוכן של הקובץ:
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './drizzle',
schema: './src/db/schema.ts',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DB_FILE_NAME!,
},
});
הקובץ מגדיר איך מתחברים לבסיס הנתונים ואיפה יושבת הסכימה. אחרי שיצרתי אותו אני יכול להפעיל:
$ npx drizzle-kit push
ודריזל קיט יתחבר לבסיס הנתונים, יסתכל על הסכימה שלי ויעדכן את בסיס הנתונים כדי שיתאים לסכימה. אם הוא צריך למחוק טבלאות או מידע הוא יזהיר אתכם לפני ביצוע הפעולה ויבקש אישור מיוחד להמשיך.
לאחר הפעלת הפקודה אני יכול לראות שנוצר לי בסיס הנתונים בקובץ local.db. אלה הטבלאות שם:
sqlite3 local.db
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite> .tables
users vocabulary_cards
אגב אחרי יצירת הטבלאות אפשר להפעיל משורת הפקודה:
$ npx drizzle-kit studio
כדי לקבל סביבת עבודה מלאה לתשאול בסיס הנתונים ועדכון הנתונים בו על בסיס הסכימה שלנו. בתוך הסטודיו אפשר לנסות להריץ כל שאילתת דריזל ולראות את התוצאה בשביל לחקור איך דברים עובדים.
יצירת נתונים
בסיס הנתונים שלנו מתחיל ריק אז בואו נמלא אותו. אני יוצר קובץ בשם src/db/seed.ts וכותב בו את הפקודות:
import {db} from './drizzle';
import { usersTable } from './schema';
async function main() {
['user1', 'user2', 'user3', 'user4'].forEach(async name => {
await db.insert(usersTable).values({ name })
})
}
main();
השורה המעניינת היא זו שבתוך גוף הלולאה:
await db.insert(usersTable).values({ name })
פקודת insert מוסיפה שורות לטבלה ואנחנו יכולים לראות את הדמיון ל INSERT של SQL. האוביקט שאני מעביר ל values מתאר את השורה ובמקרה שלנו אני מעביר ערך רק לעמודה name כי ה id מסומן בסכימה כ autoIncrement.
נפעיל את הקובץ עם הפקודה:
$ npx tsx --env-file=.env src/db/seed.ts
וראו זה פלא - 4 שורות התווספו לטבלה. אם יש לכם עדיין את הדריזל קיט סטודיו פתוח תוכלו לראות את השורות החדשות.
שמירת מילים חדשות1 417
היכרות עם Drizzle
דריזל היא ספריית העבודה עם בסיסי נתונים האהובה עליי בתקופה האחרונה. היא מספקת ביצועים מצוינים, כוללת המון פינוקים למפתחים וכמובן Type Safety ותיעוד מעולה. נכון, מספר הגירסה 0.40 לא משרה תחושת ביטחון אבל בואו נזכור שהספריה בפיתוח כבר שלוש שנים והיא אחת הפופולריות בתעשייה אז אולי אפשר לבלוע את הצפרדע הזאת.
בפוסט זה אספר לכם מספיק על דריזל כדי שתוכלו לכתוב פרויקט ראשון באמצעותה ואני מקווה שגם אתן לכם את החשק לעשות זאת.
מה אנחנו בונים
בשביל המשחק היום אנחנו נבנה מערכת לשינון אוצר מילים. נו, מערכת זו מילה גדולה, אבל נתחיל לבנות את הטבלאות למערכת דמיונית עתידית שתנהל אוצר מילים של משתמשים ותעזור להם להיזכר במילים שמנסים לשנן. המערכת מורכבת משתי טבלאות: טבלת משתמשים וטבלת אוצר מילים. בטבלת המשתמשים אני שומר שם משתמש ומזהה ובטבלת אוצר המילים את המילים של כל המשתמשים ולכל מילה נשמור מתי פעם הבאה כדאי להיזכר בה.
הדבר הראשון שכדאי לדעת על דריזל הוא שזו ספריה שעובדת מאוד קרוב לשכבת הנתונים, כלומר בכל פרויקט דריזל יהיה קובץ סכמה שיתאים לבסיס הנתונים הספציפי שבחרתם ויגדיר את הטבלאות בפורמט של אותו בסיס נתונים. לדוגמה את הטבלאות שתיארתי אני יכול לתאר בסכימה של דריזל עם הקוד הבא:
import { integer, sqliteTable, text} from "drizzle-orm/sqlite-core";
import { sql } from 'drizzle-orm';
export const usersTable = sqliteTable("users", {
id: integer().primaryKey({ autoIncrement: true }),
name: text().notNull(),
});
// Define the vocabulary cards table
export const vocabularyCardsTable = sqliteTable('vocabulary_cards', {
id: integer().primaryKey({ autoIncrement: true }),
userId: integer().notNull().references(() => usersTable.id),
front: text().notNull(),
back: text().notNull(),
nextReviewDate: text().notNull(),
createdAt: text().notNull().default(sql\(current_timestamp)\),
updatedAt: text().notNull().default(sql\(current_timestamp)\),
});
הכתיב הוא בעצם DSL לשמירת נתונים. הוא עוטף כתיב SQL אבל בשכבה מאוד דקה, וקל לנו לדמיין את ה SQL שמתאים להגדרה שכתבנו. מי שממש רוצה יכול להדביק את הסכימה בכל AI ולקבל את ה SQL שמתאים לה, בדוגמה שלנו זה:
-- Create the users table
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
-- Create the vocabulary_cards table
CREATE TABLE vocabulary_cards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL,
front TEXT NOT NULL,
back TEXT NOT NULL,
nextReviewDate TEXT NOT NULL,
createdAt TEXT NOT NULL DEFAULT (current_timestamp),
updatedAt TEXT NOT NULL DEFAULT (current_timestamp),
FOREIGN KEY (userId) REFERENCES users(id)
);
בהינתן קובץ סכימה אנחנו כבר יכולים לגלות הרבה מאוד על מבנה בסיס הנתונים. כשקוראים את הקוד של פרויקט דריזל מאוד נוח להתחיל מקובץ זה. אגב דריזל יודע גם ליצור קובץ סכימה מבסיס נתונים קיים, אבל זה סיפור ליום אחר. אגב נוסף, אם אתם כבר יודעים SQL תמיד אפשר ללכת לכל חבר AI ולבקש שיהפוך רשימת טבלאות ב SQL לסכימת דריזל.
התקנה וחיבור לבסיס נתונים SQLite
נחזור לפרויקט שלנו או יותר נכון נתחיל את הפרויקט. אני יוצר פרויקט next.js כדי להוסיף לו ממשק משתמש בעתיד, למרות שגם זה מחוץ לסקופ של פוסט זה. בשביל ליצור פרויקט next חדש אני משתמש בתבנית שכבר יצרתי מראש באופן הבא:
$ npx create-next-app@latest . -e https://github.com/ynonp/next-15-starter
הקוד יוצר תבנית לפרויקט ריק עם App Router בגירסת נקסט 15.2. עכשיו אפשר להמשיך לדף התיעוד של דריזל כדי לחבר את הפרויקט לבסיס הנתונים. אני משתמש בבסיס נתונים SQLite בשביל הדוגמה, ואליו גם מתאימה הסכימה שכתבתי. מדריך ההתקנה נמצא בקישור:
https://orm.drizzle.team/docs/get-started
ואלה עיקרי הדברים עבור SQLite. תחילה מתקינים את הספריות:
npm i drizzle-orm @libsql/client dotenv
npm i -D drizzle-kit tsx
לאחר מכן יוצרים קובץ .env שמגדיר את הנתיב לבסיס הנתונים (זכרו ש SQLite הוא בסך הכל קובץ יחיד), למשל אני משתמש ב:
DB_FILE_NAME=file:local.db
לאחר מכן אני יוצר תיקייה בשם db בתוך תיקיית src בפרויקט, ובתוכה אני יוצר קובץ בשם schema.ts עם הסכימה שלי:1 417
בניתי לבד משהו טוב יותר
מדי פעם יש פוסטים כאלה ברשת. והם צודקים - הם באמת עזבו פריימוורק שלא עבד להם טוב, בנו משהו מאפס במקום שהיה תפור בדיוק לצרכים שלהם והגיעו לתוצאות טובות יותר. בהתחשב בניסיון שלהם בפיתוח ובהיכרות שלהם את הבעיה הם גם הצליחו לעשות את זה יחסית מהר ומאז לא מסתכלים אחורה. שאפו.
האם זה אומר שגם לי כדאי לבנות לבד את התשתית? פה התשובה קצת יותר מורכבת:
1. לבנות לבד אומר שצריך גם לתחזק לבד. אולי זאת לא בעיה עבורכם ובאמת אני מכיר מערכות שמשתמשות בגירסאות ישנות של ספריות לאורך שנים והכל עובד טוב. אבל אולי אתם כן מעדיפים להשתמש בתיעוד ובדברים הכי חדשים מהרשת ואז לאורך זמן עלות התחזוקה הופכת מורגשת.
2. לבנות לבד אומר שצריך להכשיר לבד את העובדים החדשים. ברור שעכשיו זה לא בעיה כי העובדים הקיימים הם שבנו את התשתית, אבל מה יקרה עוד שנה או שנתיים? כמה קל יהיה לאנשים חדשים להיכנס לקוד כדי לתחזק אותו? וכמה יותר קשה יהיה לכם לגייס אנשים למערכת שאין בשום מקום אחר?
3. הרבה פעמים אנחנו בוחרים ללמוד לבד כשאנחנו לא מבינים מספיק את הפריימוורק. בפוסט בדוגמה החברים מ Northflank מתארים את next בתור קופסה שחורה, וזה נכון נקסט באמת יכול להרגיש כך. ברור שאי אפשר להגיע לביצועים טובים כשהפריימוורק מרגיש כמו קופסה שחורה או קסם. ביצועים דורשים התעמקות והבנה, לא רק של עולם התוכן אלא גם של איך הפריימוורק פותר את הבעיות.
4. המוצר של Northflake הוא תשתית לפיתוח אפליקציות. הם בחרו ב next לצורך אתר שיווקי מתוך דגש על SEO, מה שאומר שהם משתמשים בחלק קטן מהיכולות של המוצר. באופן כללי כשבוחרים פריימוורק ומשתמשים רק בחלק קטן מהיכולות שלו סיכוי טוב שתהיו יותר מרוצים אם תעברו לפיתרון שבניתם לבד או לפריימוורק פשוט יותר.
כשיש פריימוורק עם הרבה Hype יש נטיה טבעית של מפתחים לבחור בו בלי להבין לעומק את היתרונות והחסרונות ובלי לבחון אם זה באמת הפיתרון הכי טוב ל Use Case שלהם. יש הרבה סיבות לבחור או לא לבחור ב Next וכך לגבי כל פריימוורק לפיתוח. כמו כן יש הרבה סיבות בעד או נגד בנייה של פיתרון לבד מאפס. בכל מקרה מומלץ לפני שבוחרים פריימוורק להבין שנצטרך ללמוד אותו לעומק כדי להגיע לתוצאות טובות, ולבחור פריימוורק לפי מה שמתאים ל Use Case שלנו במקום בזה שכולם מדברים עליו.
1 417
תשובות ל-3 התנגדויות ל next
אלן פייק כותב על נקסט ופריימוורקים של ריאקט באופן כללי ומעלה 3 התנגדויות חשובות לפריימוורקים אלה:
1. הפריימוורקים בנויים על אינטגרציה עמוקה עם ריאקט, אבל מפותחים על ידי חברות אחרות.
2. ריאקט = פגיעה בביצועים.
3. למרות התמיכה ב SSR, הפריימוורקים לא כוללים (או כוללות? פריימוורק זה זכר או נקבה?) את הכלים הבסיסיים לכתיבת קוד צד שרת למערכות אמיתיות.
שלושת ההתנגדויות מבוססות ויש בהן יותר משמץ של אמת, ובכל זאת אני רוצה לחדד ולהוסיף:
1. נכון, נקסט ו React Router מפותחים על ידי חברות שונות, אבל ריאקט עצמה היא פרויקט קוד פתוח. בעמוד פגוש את הצוות אנחנו פוגשים את מפתחי הליבה של ריאקט. מתוך 21 מפתחי ליבה ברשימה 5 עובדים ב Vercel, עוד 2 עצמאיים ו 14 עובדי פייסבוק. זה אומר שהקשר בין נקסט לריאקט הוא די מבוסס.
2. נכון, ביצועים של אתרי ריאקט באופן כללי ונקסט באופן ספציפי נוטים להיות פחות טובים מביצועים של אתרי HTML בלבד או אפילו אתרי jQuery או XHTML. אתרי ריאקט מסובכים יותר ובנוסף בריאקט ו next יש הרבה מאוד מלכודות ביצועים, אם תשאלו אותי הרבה יותר ממה שצריך בפריימוורק מסוג זה. אבל האם היינו אומרים שיש לריילס בעיית ביצועים בגלל שמפתחים צעירים ישתמשו לא נכון ב ORM ויצרו יותר מדי שאילתות? כן יש בורות, צריך להכיר אותם, צריך לפעמים לעבוד קשה בשביל להימנע מהם. ועדיין כשמכירים את הבעיות ועובדים נכון אפשר לבנות אתרים שיעבדו מהר גם בריאקט. וכן יש בעיות וזה כנראה דברים שקשה לסדר וריאקט קומפיילר הוא לא צעד בכיוון הנכון, אבל עדיין עם עבודה נכונה ואופטימיזציה במקומות שצריך אפשר להגיע לתוצאות טובות.
3. נקסט הוא בסך הכל node.js. האם היו צריכים להוסיף ORM? ספריה לביצוע משימות ברקע? מערכת לוגים טובה יותר? אולי. אבל Node זה לא רובי ולא אותה אקוסיסטם. בעולם של JavaScript אנשים מעדיפים להרכיב הרבה ספריות קטנות וכל פרויקט הוא שונה, בעוד שבעולם של רובי סינטרה או הנמי לא הצליחו להתרומם ואנשים מעדיפים את הפריימוורק הגדול שפותר את כל הבעיות. זה לא שלא היו ניסיונות אבל איכשהו הם לא היו מספיק מוצלחים או מתוחזקים לאורך זמן. נכון אין לנו פיתרון מובנה מהקופסה לכל הבעיות אבל כל עוד אפשר למצוא שילוב טוב של ספריות או לפתח Serverless על מערכת ענן זה כנראה לא כל כך נורא.
למה בכל זאת צריך לזכור לפני שנכנסים לפרויקט next / react? הבעיה הכי גדולה שלהם היום היא ה Learning Curve, וזה לצערי רק מחמיר. ריאקט נראה פשוט במבט ראשון בגלל שהוא בכל מקום אבל כתיבה נכונה ומסודרת שלו היא עדיין מבלבלת והרבה מפתחי ריאקט מתיחסים לחלקים גדולים מהפריימוורק כמו קסם. אתגר נוסף הוא השינויים התכופים בסגנון העבודה, מה שמייצר צורך לשכתב קוד קיים רק בשביל שהמערכת לא תרגיש מיושנת ושיהיה קל יותר להשתלב באקוסיסטם. רק בשנים שאני עובד בריאקט עברתי מכתיב האוביקטים לכתיב הקלאסים, מכתיב הקלאסים לכתיב ה Hooks ועכשיו עדיין בעולם ה Hooks אנחנו רואים מעבר מעולם של אפליקציות צד-לקוח לאפליקציות Full Stack. כל שינוי כזה הוא רעידת אדמה וזה קשה במערכות גדולות להתבסס על פריימוורק שמשתנה בצורה כל כך דרסטית כל כך מהר.
1 417
ניסוי Next - טעינת מידע אחרי עליית העמוד עם use
קשה מאוד להבין את ריאקט היום מחוץ לקונטקסט של next. כתבתי כאן על הסירבול בפונקציה use בגלל הצורך להעביר את ה Promise מבחוץ והבעיה ביצירת Promise מתוך קומפוננטות. עכשיו בואו נראה את הסיפור במשקפיים הנכונות של next ונראה למה בשימוש מלא של הפיצ'ר אין שום בעיה.
הבעיה עם קומפוננטות אסינכרוניות
נתבונן בקומפוננטה הבאה:
import { slowlyGetCwd } from "../actions/demo"
export default async function Main() {
const cwd = await slowlyGetCwd();
return (
<div>
<p>Page content</p>
<div>Server runs from: <pre>{cwd}</pre></div>
</div>
)
}
ובפונקציית צד השרת שמתאימה לה:
'use server';
import {setTimeout} from 'node:timers/promises';
export async function slowlyGetCwd() {
await setTimeout(3000);
return process.cwd();
}
מה שקורה כשהעמוד עולה השרת מפעיל את הפונקציה, מחכה 3 שניות ורק אז מסיים לייצר את ה HTML ושולח אותו לדפדפן. ב-3 שניות האלה הגולש פשוט מחכה לתשובה שרת ולא רואה כלום על העמוד. לפעמים זאת התנהגות הגיונית, אולי באמת אנחנו טוענים מידע מאוד בסיסי מבסיס הנתונים ואין מה להראות עד שהמידע הזה מגיע, אבל לפעמים זו המתנה מיותרת - אולי אני מחכה לטעון תמונת פרופיל של המשתמש מאיזה אתר צד שלישי ורק בגלל ההמתנה לתמונה הזאת משתמש צריך לחכות ולא לקבל את המידע שכן יש לי מוכן בשבילו.
איך use פותר את הבעיה
פונקציית use החדשה בריאקט 19 מאפשרת גם גישה ל Context API וגם המתנה ל Promises. אותנו מעניין לנסות את השימוש השני בה: אם פונקציית use מקבלת Promise אז הקומפוננטה לא תתרנדר, יוצג במקומה "תחליף" מתוך בלוק ה Suspense הקרוב ביותר, ורק כשה Promise תהיה מוכנה אז הקומפוננטה תופיע.
ב Next אנחנו יכולים להעביר Promise בתור Prop מקומפוננטת צד שרת לקומפוננטת צד לקוח, ולכן נוכל להשתמש במנגנון כדי לשלוח מידע לדפדפן אחרי שהעמוד נטען. זה יעבוד כך:
1. קומפוננטת צד שרת מפעילה פעולה אסינכרונית שלוקחת זמן.
2. הקומפוננטה מעבירה את ה Promise לסיום התהליך בתור Prop לקומפוננטת צד לקוח.
3. קומפוננטת צד לקוח מפעילה use על ה Promise.
4. כשה Promise מסתיים בצד השרת, השרת שולח לדפדפן את המשך העמוד ב Streaming. זה גורם ל Promise בצד הלקוח להסתיים וכך ריאקט יכול לרנדר את קומפוננטת צד הלקוח עם המידע שהגיע מהשרת.
דוגמה: קבלת מידע אחרי שהעמוד נטען
בואו נחליף את קומפוננטת Main שכתבנו בקומפוננטה עם use. תחילה גירסת ה client component של Main:
'use client';
import {use} from 'react';
export default function Main({
cwdPromise
}: {
cwdPromise: Promise<string>,
}) {
const cwd = use(cwdPromise)
return (
<div>
<p>Page content</p>
<div>Server runs from: <pre>{cwd}</pre></div>
</div>
)
}
ועדכון page.tsx כדי לייצר את ה Promise, להעביר אותו כ prop ולהגדיר את התוכן שיוצג בזמן הטעינה:
import Head from "./components/head"
import Main from "./components/main"
import { slowlyGetCwd } from "./actions/demo"
import { Suspense } from "react";
export default function Home() {
const getCwd = slowlyGetCwd();
return (
<div>
<Head />
<Suspense fallback={<p>Loading Main</p>}>
<Main cwdPromise={getCwd} />
</Suspense>
</div>
)
}
התוצאה: העמוד נטען מיד אבל במקום Main מוצג התוכן Loading Main. אחרי 3 שניות תוכן ה Fallback מוחלף בתוכן האמיתי של Main עם התוצאה של הפונקציה האיטית בצד השרת.
现已上线!2025 年 Telegram 研究 — 年度关键洞察 
