צעדים ראשונים עם SQL Alchemy

07/05/2022

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

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

1. התקנה וחיבור לבסיס הנתונים

צעד ראשון בעבודה עם SQL Alchemy הוא להתקין את הספריה. בפייתון זה פשוט אומר להפעיל:

$ pip install sqlalchemy

שווה לציין שלי זה לא הצליח מהפעם הראשונה בגלל שידרוגי גירסאות. אם אתם מקבלים הודעות שגיאה מוזרות שווה להפעיל שידרוג לגירסה החדשה ביותר של pip ושל setuptools עם:

pip install -U pip setuptools

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

Python 3.10.0 (default, Nov  3 2021, 23:29:09) [Clang 13.0.0 (clang-1300.0.29.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sqlalchemy
>>> print(sqlalchemy.__version__)
1.4.36
>>>

כל העבודה מול בסיס הנתונים ב SQL Alchemy מבוצעת דרך אוביקט שנקרא Engine. הדבר המרכזי שנעשה איתו זה ליצור חיבור, ולכן כשיוצרים את ה Engine צריך להגיד לו לאיזה בסיס נתונים אנחנו רוצים להתחבר. אני מפעיל את הדוגמה עם SQLite שנשמר בקובץ ולכן מפעיל:

from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, ForeignKey, insert, select, bindparam

engine = create_engine("sqlite+pysqlite:///db.sql", echo=True, future=True)

שם הקובץ יהיה db.sql וזה נוח לשמור בסיס נתונים שלם בקובץ כשמשחקים עם ספריה חדשה כי לא צריך להתקין כלום וקל להסתכל בקובץ (באמצעות SQLite CLI או כל כלי גרפי אחר) ולראות שדברים נוצרו כמו שחשבנו שהם צריכים להיות.

2. יצירת טבלאות

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

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

metadata_obj = MetaData()

authors_table = Table(
        "authors",
        metadata_obj,
        Column('id', Integer, primary_key=True),
        Column('name', String()))

books_table = Table(
        "books",
        metadata_obj,
        Column('ISBN', String(13), primary_key=True),
        Column('name', String()),
        Column('author_id', ForeignKey('authors.id'), nullable=False))

האוביקט metadata מרכז את כל נתוני הטבלאות, אוביקט Table מייצג טבלה ו Column מייצג עמודה. ביצירה של עמודות אפשר להעביר את סוגי הנתונים ומאפיינים של העמודה כמו primary_key או nullable.

אחר כך מגיעה השורה האופציונאלית:

metadata_obj.create_all(engine)

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

3. הכנסת מידע

בעבודה עם SQL Alchemy יש לי ביד מנוע (engine) ובעזרתו אני מתחבר לבסיס הנתונים. החיבור תמיד כולל טרנזאקציה, מבוצע בתוך בלוק with ובסוף הבלוק, אם הכל עבד כמו שצריך, יהיה commit. אני משתמש בפקודה engine.begin כדי להתחיל את החיבור ולבצע אוטומטית את ה commit בסוף הבלוק.

בתוך הבלוק שיטת העבודה היא לפי תבנית שנקראת Command Pattern. זה אומר שיש פונקציות של SQL Alchemy שמייצגות פקודות שיכולות להישלח לבסיס הנתונים, ואוביקט החיבור יודע להפעיל אותן עם הפונקציה execute שלו. אז בשביל להפעיל הכנסה לטבלת המחברים אני אפעיל:

# Part 2 - Insert some data
with engine.begin() as conn:
    conn.execute(insert(authors_table), [
        { "name": "Jay Shetty" },
        { "name": "David Foster Wallace" },
        { "name": "Marcus Aurelius" },
        ])

הפונקציה insert יוצרת פקודת הכנסה, והפונקציה conn.execute מקבלת את פקודת ההכנסה ואוסף של פרמטרים לפקודה ומפעילה אותה כדי להכניס את שלושת השורות.

הכנסה יותר מורכבת תהיה לטבלת הספרים: שם אני צריך להשתמש בתת-שאילתה כדי לדעת מה ה id של הסופר שכתב את הספר. אני לא יודע את ה id כי בסיס הנתונים יצר אותו אוטומטית בעת ההכנסה ולכן צריך לתשאל.

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

    author_subquery = (
            select(authors_table.c.id).
            where(authors_table.c.name == bindparam('author_name')).
            scalar_subquery())

ומשלב אותו בפקודת ההכנסה באופן הבא:

    conn.execute(insert(books_table).values(author_id=author_subquery), [
        { "ISBN": "9781982134488", "name": "Think Like a Monk", "author_name": "Jay Shetty" },
        { "ISBN": "9780316066525", "name": "Infinite Jest", "author_name": "David Foster Wallace" },
        { "ISBN": "9781515208563", "name": "Meditations", "author_name": "Marcus Aurelius" },
        ])

4. שליפת מידע ו Join

ואם כבר ראינו את select בואו נרחיב עליו ונכתוב שאילתה שמושכת את כל הספרים והמחברים שלהם. הפקודה select מקבלת רשימה של עמודות ומחזירה "פקודת" שליפה. הפעלת פקודות כמו where על פקודת השליפה מוסיפה מידע לשליפה עד שיש לנו פקודה שלמה ואותה מעבירים ל execute.

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

with engine.begin() as conn:
# Part 3 - Join Select
    books = conn.execute(select(books_table.c.name, books_table.c.ISBN, authors_table.c.name).join_from(books_table, authors_table))
    for book in books:
        book_name, isbn, author = book
        print(f"Book: {book_name}; ISBN: {isbn}; author: {author}")

הפקודה select לוקחת רשימה של עמודות, עליה אני מפעיל את הפקודה join_from כדי להגיד לה מה סדר ה Join-ים והיא כבר מחשבת את פקודת ה SQL אוטומטית.

5. הקוד המלא ולאן ממשיכים

סך הכל הקוד שראינו בכל הפוסט הוא:

from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, ForeignKey, insert, select, bindparam

# Part 1 - Create the tables
engine = create_engine("sqlite+pysqlite:///db.sql", echo=True, future=True)
metadata_obj = MetaData()

authors_table = Table(
        "authors",
        metadata_obj,
        Column('id', Integer, primary_key=True),
        Column('name', String()))

books_table = Table(
        "books",
        metadata_obj,
        Column('ISBN', String(13), primary_key=True),
        Column('name', String()),
        Column('author_id', ForeignKey('authors.id'), nullable=False))


metadata_obj.create_all(engine)

# Part 2 - Insert some data
with engine.begin() as conn:
    conn.execute(insert(authors_table), [
        { "name": "Jay Shetty" },
        { "name": "David Foster Wallace" },
        { "name": "Marcus Aurelius" },
        ])

    author_subquery = (
            select(authors_table.c.id).
            where(authors_table.c.name == bindparam('author_name')).
            scalar_subquery())

    conn.execute(insert(books_table).values(author_id=author_subquery), [
        { "ISBN": "9781982134488", "name": "Think Like a Monk", "author_name": "Jay Shetty" },
        { "ISBN": "9780316066525", "name": "Infinite Jest", "author_name": "David Foster Wallace" },
        { "ISBN": "9781515208563", "name": "Meditations", "author_name": "Marcus Aurelius" },
        ])


with engine.begin() as conn:
# Part 3 - Join Select
    books = conn.execute(select(books_table.c.name, books_table.c.ISBN, authors_table.c.name).join_from(books_table, authors_table))
    for book in books:
        book_name, isbn, author = book
        print(f"Book: {book_name}; ISBN: {isbn}; author: {author}")

אני מקווה שהוא עזר לכם לקבל תמונה מגבוה על SQL Alchemy. אם אהבתם את הממשק ותרצו ללמוד עוד על הספריה אני ממליץ על התיעוד שלהם ובמיוחד על ה Tutorial, שהרבה מהדוגמאות בפוסט מבוססות עליו. זה הקישור:

https://docs.sqlalchemy.org/en/14/tutorial/engine.html