コンテンツへスキップ

データベースからコードへ (ORM)

ここでは、SQLModelがデータベースとどのように相互作用するか、なぜそれを使用したいのか(または同様のツールを使用するのか)、そしてそれがSQLとどのように関連しているかについて説明します。

コード内にSQLを直接記述

heroテーブルからすべてのデータを取得するための簡単なSQLクエリの例を見てみましょう。

SELECT *
FROM hero;

そして、そのSQLクエリはテーブルを返します。

idnamesecret_nameageteam_id
1DeadpondDive Wilsonnull2
2Spider-BoyPedro Parqueadornull1
3Rusty-ManTommy Sharp481

このSQL言語には小さな注意点があります。Pythonのようなプログラミング言語の通常のコードと混在するように設計されていません。🚨

したがって、Pythonを使用している場合、最も簡単な方法はSQLコードを文字列内に入れ、その文字列を直接データベースに送信することです。

statement = "SELECT * FROM hero;"

results = database.execute(statement)

しかし、その場合、エディターのサポート、インラインエラー、オートコンプリートなどは利用できません。エディターにとって、SQLステートメントは単なるテキストの文字列だからです。エラーが発生した場合、エディターは助けることができません。😔

さらに重要なことに、ほとんどの場合、SQL文字列は修正やパラメーター付きで送信します。たとえば、特定のアイテムID日付範囲などのデータを取得するためです。

ほとんどの場合、データベース内のデータをクエリまたは変更するためにコードで使用するパラメーターは、何らかの形で外部ユーザーから提供されます。

たとえば、このSQLクエリを確認してください

SELECT *
FROM hero
WHERE id = 2;

IDパラメーター2を使用しています。その番号2は、おそらく何らかの形でユーザー入力から得られます。

ユーザーはおそらく、何らかの方法でアプリケーションに伝えています。

「IDを持つヒーローを取得したい」

2

そして、結果はこのテーブルになります(1行のみ)。

idnamesecret_nameageteam_id
2Spider-BoyPedro Parqueadornull1

SQLインジェクション

しかし、コードが外部ユーザーが提供するものをすべて受け取り、データベースに送信する前にSQL文字列に入れるとしましょう。このようなものです。

# Never do this! 🚨 Continue reading.

user_id = input("Type the user ID: ")

statement = f"SELECT * FROM hero WHERE id = {user_id};"

results = database.execute(statement)

外部ユーザーが実際には攻撃者である場合、すべてのレコードを削除するなど、ひどいことを行う悪意のあるSQL文字列を送信する可能性があります。これは「SQLインジェクション」と呼ばれます。

たとえば、この新しい攻撃者のユーザーが言うと想像してください。

「IDを持つヒーローを取得したい」

2; DROP TABLE hero

次に、ユーザー入力を受け取り、それをSQLに入れる上記のコードは、実際にはこれをデータベースに送信します。

SELECT * FROM hero WHERE id = 2; DROP TABLE hero;

最後に追加されたセクションを確認してください。これは別のSQLステートメント全体です。

DROP TABLE hero;

これが、SQLでデータベースにテーブルhero全体を削除するように指示する方法です。

うわあああ! heroテーブルのすべてのデータを失いました!💥😱

SQLサニタイズ

外部ユーザーが送信したものがSQL文字列で使用しても安全であることを確認するプロセスは、サニタイズと呼ばれます。

これは、SQLModel(SQLAlchemyのおかげ)にデフォルトで付属しています。そして、他の多くの同様のツールも、他の多くの機能の中でもその機能を提供します。

これで、xkcdからのジョークの準備ができました。

Exploits of a Mom

SQLをSQLModelで扱う

SQLModelでは、SQLステートメントを直接記述する代わりに、Pythonクラスとオブジェクトを使用してデータベースと対話します。

たとえば、次のコードでID2を持つ同じヒーローをデータベースに要求できます。

user_id = input("Type the user ID: ")

session.exec(
    select(Hero).where(Hero.id == user_id)
).all()

ユーザーがこのIDを提供した場合

2

...結果はこのテーブルになります(1行のみ)。

idnamesecret_nameageteam_id
2Spider-BoyPedro Parqueadornull1

SQLインジェクションの防止

ユーザーが攻撃者であり、「ID」としてこれを送信しようとすると

2; DROP TABLE hero

次に、SQLModelはそれをリテラル文字列"2; DROP TABLE hero"に変換します。

そして、攻撃を注入する代わりに、正確にそのIDを持つレコードを見つけようとするようにSQLデータベースに指示します。

最終的なSQLステートメントの違いはわずかですが、意味が完全に変わります。

SELECT * FROM hero WHERE id = "2; DROP TABLE hero;";

ヒント

二重引用符(")が、より生のSQLではなく、文字列にしていることに注意してください。

データベースはそのIDを持つレコードを見つけられません。

"2; DROP TABLE hero;"

次に、データベースはそのIDを持つレコードを見つけられなかったため、結果として空のテーブルを送信します。

次に、コードは実行を続け、何も見つからなかったことをユーザーに冷静に伝えます。

しかし、heroテーブルは削除しませんでした。🎉

情報

もちろん、SQLModelのようなツールを使用せずにSQLデータサニタイズを行う他の方法もありますが、それでもデフォルトで得られる優れた機能です。

エディターサポート

上記のPythonスニペットをもう一度確認してください。

標準のPythonクラスとオブジェクトを使用しているため、エディターはオートコンプリート、インラインエラーなどを提供できます。

たとえば、秘密のIDに基づいてヒーローを見つけるためにデータベースをクエリしたいとしましょう。

列にどのように名前を付けたか覚えていないかもしれません。もしかしたら

  • secret_identity?

...または

  • secretidentity?

...または

  • private_name?
  • secret_name?
  • secretname?

コード内のSQL文字列でそれを入力した場合、エディターはあなたを助けることができません

statement = "SELECT * FROM hero WHERE secret_identity = 'Dive Wilson';"

results = database.execute(statement)

...エディターはそれを中にテキストがある長い文字列と見なし、secret_identityオートコンプリートまたはエラーを検出することはできません

しかし、一般的なPythonクラスとオブジェクトを使用すると、エディターがあなたを助けることができます。

database.execute(
    select(Hero).where(Hero.secret_name == "Dive Wilson")
).all()

ORMとSQL

SQLModel(そしてもちろんSQLAlchemy)のような、SQLとクラスおよびオブジェクトを使用したコードの間で変換するタイプのライブラリは、ORMと呼ばれます。

ORMオブジェクト-リレーショナルマッパーを意味します。

これは非常に一般的な用語ですが、非常に技術的で学術的な概念にも由来します 👩‍🎓

  • オブジェクト: クラスとインスタンスを使用したコードを指し、通常は「オブジェクト指向プログラミング」と呼ばれます。これが「オブジェクト」の部分です。

たとえば、このクラスはそのオブジェクト指向プログラミングの一部です。

class Hero(SQLModel):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: Optional[int] = None
  • リレーショナルSQLデータベースを指します。それらのテーブルのそれぞれが「リレーション」とも呼ばれているため、それらはリレーショナルデータベースとも呼ばれていることを覚えていますか?そこから「リレーショナル」が来ています。

たとえば、このリレーションまたはテーブル

idnamesecret_nameageteam_id
1DeadpondDive Wilsonnull2
2Spider-BoyPedro Parqueadornull1
3Rusty-ManTommy Sharp481
  • マッパー:これは数学に由来しており、いくつかのものの集合から別のものに変換できるものがある場合、「マッピング関数」と呼ばれます。そこからマッパーが来ています。

Squares to Triangles Mapper

また、Pythonで、小文字のセットから大文字のセットに変換するマッピング関数をこのように記述することもできます。

def map_lower_to_upper(value: str):
    return value.upper()

実際には、非常に学術的で数学的な名前を持つ単純なアイデアです。😅

したがって、ORMはSQLからコードへ、およびコードからSQLへ変換するライブラリです。すべてクラスとオブジェクトを使用しています。

SQLModel以外にも多くのORMが利用可能です。代替、インスピレーション、比較でそれらの一部について詳しく読むことができます。

SQLテーブル名

技術的な背景

これはSQL原理主義者にとっては少し退屈な背景です。このセクションはスキップしてもかまいません。😉

純粋なSQLを扱う場合、テーブルに複数形で名前を付けるのが一般的です。したがって、テーブルにはheroではなくheroesという名前が付けられます。これは、複数の行を含めることができ、それぞれに1人のヒーローがいる可能性があるためです。

それにもかかわらず、SQLModelおよび他の多くの同様のツールは、チュートリアルで後で説明するように、コードからテーブル名を自動的に生成できます。

ただし、この名前はクラス名から派生します。また、クラスには単数名(例:class Heroclass Heroesの代わりに)を使用するのが一般的な慣例です。class Heroのようなクラスに単数名を使用すると、コードがより直感的になります。

内部テーブル名よりも自分のコードを頻繁に見ることになるため、SQLの規則よりもコード/クラスの規則を維持する方がおそらく良いでしょう。

したがって、一貫性を保つために、SQLModelが生成したのと同じテーブル名を引き続き使用します。

ヒント

テーブル名を上書きすることもできます。詳細については、高度なユーザーガイドを参照してください。