הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

מה למדתי מ Rocket על ניהול תלויות

25/08/2023

רוקט היא ספריית פיתוח ווב ל Rust. למפתח המרכזי שלה קוראים סרחיו בניטז והוא מפתח את רוקט מאז 2016. אם אתם כמוני, כלומר אנשים שנלחצים מפרויקטי צד של בן אדם אחד שעדיין נמצאים בגירסה 0.5 אז אולי לא תרצו לבחור את רוקט בתור תשתית לפרויקט הבא שלכם, וזה בסדר. אבל אפילו אם זה המצב הספריה עדיין מעניינת ומציגה מבט רענן על פיתוח ווב בתוך שפה עם הגדרת טיפוסים סטטית. דוגמת ה Hello World שלהם נראית כך:

#[macro_use] extern crate rocket;

#[get("/hello/<name>/<age>")]
fn hello(name: &str, age: u8) -> String {
    format!("Hello, {} year old named {}!", age, name)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![hello])
}

והיא מייצרת נתיב עם שני משתנים, name ו age שבגלישה אליו יחזיר הודעת טקסט עם הערכים.

זה לבד נראה אחלה ועושה חשק להמשיך ללמוד, אבל הנושא של הפוסט היום הוא דווקא לא רוקט אלא פיצ'ר ספציפי של רוקט שנקרא Request Guards. הרעיון מאוד מזכיר את ה Middlewares של אקספרס ואומר שכשיש לי בקשה אז לפני שאני מגיע לקוד הטיפול בנתיב קודם צריך לעבור דרך ה"שומרים" שיקחו מידע מהבקשה ויאתחלו משתנים מסוימים, שאחרי זה עוברים כפרמטרים לנתיב.

הדוגמה הבאה ממחישה את הקונספט לא רע:

#[get("/sensitive")]
fn sensitive(key: ApiKey) { /* .. */ }

בהנחה שיש לנו טיפוס בשם ApiKey והטיפוס מממש התנהגות בשם FromRequest, אז לפני שרוקט תעביר את הטיפול לנתיב שהגדרנו היא תנסה "ליצור" ApiKey מהבקשה.

הדוגמה הבאה מציגה פונקציה יותר מפורטת שתיקרא רק אם משתמש במערכת מנסה לגשת לנתיב:

/// This route is chosen if the request guard for User passes (e.g. logged in).
#[get("/")]
fn home(user: User, _cookies: Cookies) -> Markup {
    html! {
        head {
            title {"Welcome | Auth0 Rocket Example"}
            link rel="stylesheet" href="static/css/style.css";
        }
        body{
            h1 {"Guarded Route"}
            div {p {
                "You logged in successfully."
            }}
            div {p {
                "Email: " (user.email)
            }}
            div {p {
                a class="login" href="/profile" {"Another private route"}
            }}
        }
    }
}

כשהקוד של User הוא:

#[derive(Debug, Serialize, Deserialize)]
struct User {
    user_id: String,
    email: String,
}

impl<'a, 'r> FromRequest<'a, 'r> for User {
    type Error = ();
    fn from_request(request: &'a Request<'r>) -> Outcome<User, ()> {
        let session_id: Option<String> = request
            .cookies()
            .get("session")
            .and_then(|cookie| cookie.value().parse().ok());
        match session_id {
            None => {
                println!("no session id");
                rocket::Outcome::Forward(())
            }
            Some(session_id) => {
                println!("session id: {}", session_id);
                let db = State::<DB>::from_request(request).unwrap();
                let session_key = make_key!("sessions/", session_id);
                match db.get(&session_key.0) {
                    Ok(Some(sess)) => {
                        let sess: Session =
                            deserialize(&sess).expect("could not deserialize session");
                        if sess.expired() {
                            return rocket::Outcome::Forward(());
                        }
                        let user_key = make_key!("users/", sess.user_id);
                        match db.get(&user_key.0) {
                            Ok(Some(user)) => {
                                let user: User =
                                    deserialize(&user).expect("could not deserialize user");
                                rocket::Outcome::Success(user)
                            }
                            _ => rocket::Outcome::Forward(()),
                        }
                    }
                    _ => rocket::Outcome::Forward(()),
                }
            }
        }
    }
}

המבנה טוב כיוון שהוא מחבר בין משהו שהפונקציה צריכה - המשתמש - לבין שורת ההגדרה של הפונקציה. אי אפשר היה להיכנס לקוד הטיפול בנתיב בלי שעברנו בקוד שקודם מפענח את ה User מתוך הבקשה.

נשווה את זה לריילס שם כל נתיב אחראי על טעינת המידע ובדיקת המשתמש:

def show
    @post = Post.find(params[:id])
    authorize! :read, @post
    render plain: "Welcome #{current_user.name}"
end

או ל Express שם אפשר להשתמש ב Middlewares בשורת הגדרת הפונקציה:

app.get('/post/:id', authorize_user, (req, res) => {
    const user = res.locals.user;
    res.send(`Welcome! ${user.name}`);
});

אז בריילס המצב הכי גרוע כי הכי קל לשכוח לקרוא ל authorize או לקרוא לו עם פרמטרים לא נכונים (קרה לי יותר מדי פעמים), אבל גם ב Express המצב בעייתי כי אנחנו מתבססים על תיאום בין המידלוור authorize_user שתכתוב את המידע לשדה ב req.locals באותו שם בו הנתיב מצפה למצוא אותו. בין שלושת האפשרויות הפיתרון של רוקט הוא הטוב ביותר מאחר ואי אפשר "לשכוח" לבקש את ה User (אם תשכחו פשוט לא תקבלו את המידע על המשתמש), ולא צריך לתאם שמות בין מספר מקומות שונים בקוד.

הרצת משימות ברקע עם Qt בפייתון

24/08/2023

בכתיבת תוכניות גרפיות עם Qt ב Python נוכל לשים לב שפעולות ארוכות ״תוקעות״ את התוכנית. לדוגמה נדמיין תוכנית המציגה תיבת חיפוש לקבצים, כפתור ורשימה ובלחיצה על הכפתור התוכנית מחפשת קבצים שמתאימים לשם הקובץ שבתיבה וממלאת את הרשימה בשמות הקבצים שנמצאו.

אם נריץ את התוכנית תוך שימוש ב Thread אחד, בלחיצה על הכפתור פייתון יתחיל לחפש קבצים ולא יהיה זמין לטפל באירועים אחרים הקשורים לממשק המשתמש - למשל הגדלה או הקטנה של החלון, או אפילו שינויי עיצוב קטנים כשסמן העכבר עובר על הכפתור. התנהגות כזאת תיתן למשתמשים הרגשה שהתוכנית ״תקועה״, למרות שההיפך הוא הנכון והתוכנית עובדת מאוד קשה. יותר מזה, רק אחרי שיימצאו כל הקבצים ולולאת החיפוש תסתיים נראה שינוי בממשק המשתמש ולכן ברגע אחד יתווספו לרשימה כל הקבצים שמצאנו.

חווית משתמש טובה יותר תאפשר למשתמשים להמשיך לעבוד עם הממשק ותוסיף את התוצאות שהיא מוצאת אחת-אחת בזמן בו התוכנית מוצאת אותן. בארכיטקטורה של Qt יש שתי דרכים מרכזיות להגיע לתוצאה זו:

  1. אפשר לכתוב את לולאת החיפוש עם ״הפסקות״, כך שכל פעם שמוצאים קובץ שמתאים למילת החיפוש נעצור לנשום ונבקש מ Qt לטפל באירועים גרפיים שאולי הצטברו. זה ייתן חווית משתמש טובה יותר ויפתור את הבעיה בתוכנית חיפוש הקבצים, אבל לא תמיד אפשרי במקרה הכללי כי לא תמיד יש מקום טוב לעצירות כאלה.

  2. פיתרון יותר כללי יהיה לפתוח Thread נוסף ולבצע את הלולאה (במקרה שלנו לולאת חיפוש) מתוך ה Thread הנוסף.

התקשורת בין תהליכון ראשי ב Qt לבין תהליכון נוסף שרץ ברקע מתבצעת במנגנון הסיגנלים הרגיל של Qt - התהליכון הראשי שולח סיגנל ותהליכון הרקע קורא את הסיגנל הזה ומתחיל לעבוד. כל פעם שתהליכון החיפוש מזהה תוצאה חדשה הוא ישלח סיגנל והתהליכון הראשי יוסיף פריט נוסף לתיבת התוצאות.

קוד? ברור, ומספיק קובץ אחד:

from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
from pathlib import Path
from time import sleep

class Finder(QObject):
    path_found = Signal(str)

    def __init__(self, parent=None):
        super().__init__(parent)

    def search(self, text):
        results = Path('.').rglob(text)

        for result in results:
            self.path_found.emit(str(result.absolute()))
            sleep(0.01)



class Ui(QWidget):
    start_search = Signal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.results = QListWidget()
        self.searchbox = QLineEdit()
        self.search_button = QPushButton("Search")
        self.finder = Finder()
        self.finder_thread = QThread(self)
        self.finder.moveToThread(self.finder_thread)
        self.finder_thread.start()

        self.main_layout = QVBoxLayout(self)
        self.top_layout = QHBoxLayout()
        self.main_layout.addLayout(self.top_layout)

        self.top_layout.addWidget(self.searchbox)
        self.top_layout.addWidget(self.search_button)
        self.main_layout.addWidget(self.results)

        self.search_button.clicked.connect(self.search)

        self.start_search.connect(self.finder.search)
        self.finder.path_found.connect(self.results.addItem)

    def search(self):
        self.results.clear()
        self.start_search.emit(self.searchbox.text())

    def closeEvent(self, event):
        self.finder_thread.exit(0)



app = QApplication()
w = Ui()
w.show()
app.exec()

אז מה היה לנו כאן?

  1. האוביקט Finder אחראי על חיפוש הקבצים. הוא יורש מ QObject כי ככה Qt דורש. הפונקציה moveToThread שלו שולחת אותו לעבוד ב Thread אחר.

  2. ה Thread אגב הוא Thread של Qt ולא של פייתון. נוצר באמצעות QThread().

  3. ה Widget הראשי מגדיר סיגנל כדי לאותת ל Finder שצריך להתחיל את החיפוש, וה Finder מגדיר סיגנל כדי לאותת ל Widget הראשי שהוא מצא קובץ.

  4. השורות שמחברות את הסיגנלים לקודי הטיפול מופעלות ב init של ה Widget והן:

self.start_search.connect(self.finder.search)
self.finder.path_found.connect(self.results.addItem)

כל השאר זה קסם אוטומטי של Qt - כשהסיגנל start_search נשלח ה Finder מתחיל לחפש ברקע, וכל תוצאה שהוא מצא נשלחת דרך סיגנל לפונקציה addItem של תיבת התוצאות.

בסגירה אנחנו צריכים לסגור את תהליכון הרקע ובשביל זה הגדרתי פונקציית טיפול באירוע סגירה:

def closeEvent(self, event):
    self.finder_thread.exit(0)

פיתרון כזה יכול לתת מענה טוב לכל תוכנית גרפית שצריכה לבצע עבודה ברקע. נכון אין להם עדיין תמיכה טובה ב async/await, אבל אני מקווה שבעתיד גם לזה יימצא פיתרון.

הפרויקט הראשון

23/08/2023

מה עושים כשאין עדיין תיק עבודות? מה עושים כשרוצים להיכנס להייטק, אבל אף אחד לא מדבר איתך (ובצדק) כי הם לא מזהים את הערך עבורם, כי הם (עדיין) לא רואים?

האתגר כאן הוא כפול כי צריך גם לבנות תיק עבודות, גם שהפרויקטים יהיו מספיק טובים וגם להספיק לעשות את הכל לפני שהכסף ייגמר - ואת כל זה צריך לעשות עוד לפני שנכנסת לעבודה ולפני שיש לך את הכלים והידע להצליח.

מאתגר, אבל לא בלתי אפשרי.

כיוון אחד שעובד הוא לבחור פרויקט אחד ולבנות אותו בצורה פתוחה תוך כדי חיפוש עבודה, ותוך הדגשת המסגרת. כלומר:

  1. בונים קודם דף נחיתה שמספר על הפרויקט. תניחו שהכל כבר כתוב ועובד ואפילו תוכלו לשים Screenshots מהפרויקט הדמיוני (מנועי בינה מלאכותית ישמחו לייצר לכם כמה).

  2. בונים פרויקט בגיטהאב עם קובץ readme מושקע שמספר על הפרויקט.

  3. את הפיתוח מתחילים עם תשתיות של בדיקות ו Deployment אוטומטי דרך Github Actions.

  4. משקיעים בהודעות קומיט (מישהו אמר gitpoet?)

עכשיו אפשר להתחיל לעבוד - תוך כדי חיפוש העבודה אתם ממשיכים להשקיע לפחות חצי מהזמן שלכם בפיתוח הפרויקט שבחרתם. בקורות חיים מופיע קישור לדף הנחיתה של הפרויקט כדי להרשים את המגייסים, ובזמן הראיונות אתם מספרים על האתגרים איתם אתם מתמודדים במהלך הפיתוח.

נכון הפרויקט עדיין לא עובד ואין שם המון קוד, אבל כשהכיוון ברור והתשתית הבסיסית טובה אפשר לראות את הפוטנציאל גם בפרויקט אחד שעדיין נמצא בהתחלה.

ואולי Icon Fonts לא היו רעיון כזה טוב

22/08/2023

במאי 2012 דייב גאנדי פירסם את ספריית Font Awesome שכתב, שאיפשרה למתכנתים לבנות אייקונים באמצעות אותיות בגופן. בגלל שהיה יותר קל לעצב טקסט מאשר לעצב תמונות, אפשר היה באמצעות CSS לשנות את הצבעים והגדלים של האייקונים וכך להתאים אותם לכל עיצוב.

היום כבר אפשר להשתמש ב SVG כדי להשיג בדיוק את אותו אפקט, אבל האקים שנוצרו ב 2012 לא מתים בקלות.

וכך לא מזמן חברה ביקשה שאעזור לה לפתור בעיה באתר. מסתבר שמשתמשים שנעזרו בקוראי מסך כדי לגלוש באתר שמעו אותיות מוזרות ולא קשורות בין הטקסטים הרגילים. זה נשמע כאילו קורא המסך פתאום ממציא טקסטים שלא מופיעים בטקסט.

מהר מאוד שמנו לב שהאותיות המוזרות מופיעות בדיוק איפה שבאתר מופיעים אייקונים, ושהאתר משתמש ב Icon Font. ברגע שחיברנו את הנקודות גם הפיתרון היה קל - או כמו שכותבים באתר של Font Awesome פשוט צריך להסתיר את האייקונים מקורא המסך עם:

<span class="fa-solid fa-envelope" aria-hidden="true" />

אז כן גם אייקונים בגופן יכולים להיות נגישים. חשוב רק לשים לב ולזכור את הטיפול המיוחד בהם כשמשתמשים בהם באתר.

שימו לב: אין Merge חלקי ב neo4j

21/08/2023

הפקודה MERGE ב neo4j יוצרת צמתים חדשים וקשתות חדשות אבל בצורה מעניינת - במקום שנגיד לה מה אנחנו רוצים ליצור אנחנו מתארים לה את מצב העניינים אחרי היצירה. אם הכל כבר קיים merge לא תעשה כלום, ואם החיבור לא קיים היא תיצור אותו.

איפה הבעיה?

בניגוד לכלים חכמים כמו קוברנטיס שמקבלים מצב סופי ויודעים להבין מה צריך לעשות כדי להגיע מאיפה שאני נמצא למצב שרציתי להגיע, פקודת merge ב neo4j עובדת בשיטת "הכל או כלום". אם החיבור קיים היא לא תעשה כלום, אם החיבור לא קיים היא תיצור אותו, אבל אם חלק מהחיבור קיים אז היא תנסה ליצור חיבור חדש לגמרי שיתאים למה שביקשתי.

ניקח דוגמה מבסיס הנתונים של הסרטים מארגז החול של neo4j. בסיס הנתונים מכיל Person בשם Jack Nicholson ו Person נוסף בשם Al Pacino. אני רוצה לחבר בין שניהם בקשר שנקרא FRIEND אז אני מריץ:

MERGE (n:Person{name:"Jack Nicholson"})-[:FRIENDS]-(m:Person{name:"Al Pacino"}) RETURN n LIMIT 25

אבל במקום ליצור את הקשר אני מקבל שגיאה:

Node(17) already exists with label `Person` and property `name` = 'Jack Nicholson'

מסתבר ש MERGE זיהה שאין קשר של חברות בין ג'ק ניקולסון לאל פאצ'ינו, ולכן ניסה ליצור את כל השלישייה - בן אדם בשם ג'ק ניקולסון, בן אדם נוסף בשם אל פאצ'ינו וקשר של חברות ביניהם. בארגז החול מוגדר אינדקס ייחודי על שמות של אנשים ולכן הניסיון ליצור בן אדם נוסף עם שם קיים נכשל. בלי האינדקס המצב היה עוד יותר רע, והייתי מקבל בסיס נתונים עם שני שחקנים באותו שם - מה שיגרום לבאגים בשאילתות בהמשך.

הפיתרון, מעבר להבנת החשיבות של אינדקסים, הוא לחלק את ה MERGE לפקודות MATCH כשרוצים להשתמש במידע קיים, כלומר השאילתה תהיה:

MATCH (n:Person{name:"Jack Nicholson"})
MATCH (m:Person{name:"Al Pacino"})
MERGE (n)-[:FRIENDS]-(m) RETURN n LIMIT 25

היום למדתי: אופרטור += בפייתון נראה אטומי על מעבדי אפל

20/08/2023

בזמן ריענון מצגת על פייתון וביצוע תהליכים במקביל הרצתי את התוכנית של ה Threads שמראה למה צריך לסנכרן גישה למשתנים למרות ה GIL:

import threading

i = 0

def test():
    global i
    for x in range(100000):
        i += 1

threads = [threading.Thread(target=test) for t in range(10)]
for t in threads:
    t.start()

for t in threads:
    t.join()

print(i)
assert i == 1000000, i

אבל במקום להתרסק ב assert הכל עבר בשלום.

לא עזר להריץ שוב. ושוב. ושוב.

רק בשביל לראות שלא השתגעתי הרצתי את אותה תוכנית על חלונות ולינוקס וכמובן בשתי המכונות שניסיתי הכל התרסק כצפוי. מה קורה פה?

לצערי לא מצאתי כלום בחיפוש רגיל ברשת, וגם Chat GPT לא ידע על מה אני מדבר. בינג הכיר את התופעה וטען שהמעבד של אפל יודע לעבוד בצורה אטומית על ערכים של 64 ביט ונראה לו שזאת הסיבה, אבל אני לא בטוח שאני מאמין לו או רואה את הקשר. כך שנצטרך להישאר עם פוסט פתוח להיום ואם אתם מכירים את ההסבר לתופעה אשמח לשיתוף.

כן כדאי לזכור שבהיעדר תיעוד רלוונטי ובהינתן שהקוד נשבר על מערכות הפעלה אחרות לא כדאי להתסמך על עובדה זו בעבודה שלכם עם פייתון על מעבדי אפל ותמיד תזכרו להוסיף נעילות כשכותבים קוד בו מספר תהליכונים מעדכנים את אותם משתנים.

הדבר הגדול הבא

19/08/2023

כאילו משום מקום הרעיון הבא החליט לדפוק באיזו דלת פנימית בראש. כמו לראות משהו שאחרים לא רואים ואחר כך אי אפשר להפסיק לראות אותו. כמו שעון מעורר שנתקע על הסנוז הוא ממשיך לתקתק בתוך הראש.

ואת רוצה להגיד לו - עזוב רעיון. לא צריך עכשיו. חכה שאמצא עבודה, או אחרי החתונה, או שאצליח לחסוך קצת, או אחרי הטיול, אחרי חופשת לידה, אחרי שהוא יתחיל ללכת, אחרי הגמילה, אחרי הקידום אחרי התואר.

אבל הרעיון לא יחכה. כשלא פותחים את הדלת לרעיונות הם הולכים למקום אחר, ואנחנו ממשיכים עם זיכרון של מה שהיה יכול להיות.

דווקא היום ודווקא אנחנו (הייטקיסטים והייטקיסטיות, אנשים שבונים מוצרים ביום יום בתור עבודה, אנשים שיודעים לבנות מוצר ויכולים להגיע איתו ללקוחות בכל העולם) נמצאים במקום הכי טוב להתקדם עם רעיון גם בלי סיכון גדול מדי.

ויכול להיות שזה לא רעיון כזה טוב. ויכול להיות שאף אחד לא ירצה לבוא. ויכול להיות שזה לא יצא יפה או אופנתי כמו שרצית. זה בסדר. גם להתאמן על בניית פרויקטים זה חשוב, וככל שנותנים לרעיונות יותר מקום באים יותר מהם.

כשהדבר הגדול הבא דופק על הדלת, אל תשאירו אותו בחוץ.

דיאלוגים מודאליים בדפדפן

18/08/2023

לפעמים יש לי אמונות והרגלים כל כך עתיקים שאפילו כשיש מסביב רמזים לשינוי אני לא טורח לבדוק אותם. דיאלוגים מודאליים ב HTML? אין דבר כזה. צריך ליצור אלמנט ולהשתמש ב CSS כדי לתת "הרגשה של מודאלי". ככה זה היה תמיד.

כן טוב, מה שהיה הוא לא מה שיהיה. כרום 37, אדג' 79, פיירפוקס 98, אופרה 24, ספארי 15.4 ואפילו דפדפני המובייל - כולם תומכים היום תמיכה מובנית וטבעית באלמנט dialog ויודעים להציג דיאלוג מודאלי בלי שום ספריה ובצורה נגישה.

בואו נראה איך זה עובד.

המשך קריאה

כמה פחדים אמיתיים מ WEI

17/08/2023

משמעות ראשי התיבות WEI היא Web Environment Integrity. זהו API שנתמך כבר בכרום אבל עדיין לא סטנדרטי שמטרתו לוודא בצד השרת שהדפדפן שפונה לקבל את התוכן הוא דפדפן "אותנטי".

אפשר לחשוב על WEI בתור Captcha משודרג - משתמש גולש לאתר, האתר דורש מהדפדפן להוכיח שהוא בן אדם שמשתמש בדפדפן "רגיל", הדפדפן פונה לשרת אימות עם ה URL אותו הוא רוצה למשוך, מקבל טוקן ומשתמש בטוקן כדי לחזור לשרת המקורי ולקבל את התוכן.

ההבדל המרכזי בין מנגנון זה לבין Captcha הוא שבדרך כלל Captcha חוסם רק מביצוע פעולות באתר, ו WEI יחסום בכל פניה לתוכן.

אלה הפחדים המרכזיים של מפתחים והקהילה מ API זה:

  1. חסימת מתחרים - אם חברת דפדפנים אחת (אה לא אמרתי? ה API הוצע ונדחף על ידי גוגל) יכולה להחליט אם דפדפן מסוים הוא "אותנטי" כדי לגשת לתוכן, הם יוכלו לחסום דפדפנים מתחרים או להשתמש בכח שלהם כדי לקבוע איזה יכולות דפדפן חייב לממש.

  2. אוטומציה - הכנסת תקן WEI תהפוך פרויקטי תחביב וגם פרויקטים יותר גדולים שמתבססים על אוטומציה להרבה יותר קשים. זאת בעצם חסימה מוחלטת של כל אפשרות ל Web Scraping.

  3. פרטיות - איך השרת של גוגל ידע אם אתה בן אדם שגולש דרך דפדפן אמיתי? האם נתוני שימוש או נתונים התנהגותיים כלשהם ישודרו מהדפדפן לשרת האימות? האם תהיה לנו שליטה על הנתונים שנשלחים?

  4. תוספים לדפדפן - האם תוספים לדפדפן יוכלו למשוך מידע מאתרים? או לחסום מידע מאתרים? איך כל העסק ישפיע על עולם פיתוח התוספים?

ברמה האישית אוטומציה של עולם הווב היתה תמיד אחת הסיבות שלי לבחירה בעולם זה. היכולת לבנות כלים קטנים שעוזרים לי בלי לבקש רשות מאף אחד היא בבסיס של החופש שלי היה כל כך חשוב בעת גלישה ברשת. יהיה מאוד חבל להיפרד מחופש זה בגלל שגוגל החליטו שהחופש שלי פוגע להם בהכנסות מפירסומות. כרגע מוזילה עדיין מסרבים לממש את התקן. נמשיך לעקוב ונקווה לטוב.

ארבעה חסרונות מורגשים של neo4j (אחרי 4 חודשים של עבודה)

16/08/2023

התחלתי לעבוד עם neo4j באפריל בהתרגשות גדולה וגם כתבתי עליו כמה פוסטים באותה תקופה. ארבעה חודשים לתוך הפרויקט החסרונות מתחילים להיות יותר מורגשים, למרות שעדיין מוקדם ורק כשהפרויקט יעלה לפרודקשן אוכל לענות על שאלת המחץ לגבי הביצועים ואם הוא מצליח לסחוב משתמשים אמיתיים.

בינתיים קבלו ארבעה חסרונות של neo4j שיכולים ממש להרגיז בזמן פיתוח-

המשך קריאה