# SQLAlchemy 2.0(02)建置資料表
###### tags: `python` `sqlalchemy`
[官方文件](https://docs.sqlalchemy.org/en/20/index.html)
透過[SQLAlchemy 2.0(01)概觀](https://hackmd.io/@shaoeChen/B1CJAsHJ3)的簡單說明大致對SQLAlchemy有初步的觀念,接下來就要進入下一個議題,也就是Database Metadata。
:::info
The foundation for these queries are Python objects that represent database concepts like tables and columns. These objects are known collectively as database metadata.
:::
官方文件說明來看,大致就是我們所查詢像是資料庫的資料表、欄位這種Python物件就統稱為database metadata。
## 建置資料表(Core)
Core的記錄是保存在由很多欄位所組成的資料表中,資料表則是放置在集合中,也就是`MetaData`:
```python=
from sqlalchemy import MetaData
metadata_obj = MetaData()
```
有了這個`MetaData`,我們就可以開始建置資料表:
```python=
from sqlalchemy import Table, Column, Integer, String
user_table = Table(
"user_account",
metadata_obj,
Column("id", Integer, primary_key=True),
Column("name", String(30)),
Column("fullname", String),
)
```
上面的範例中我們定義一個`user_table`,它的資料表名稱為`user_account`,然後有三個欄位,`id, name, fullname`,其中以`id`做為pk值,也可以看的到剛剛所宣告的`metadata_obj`也做為定義這個`Table`的其中一個參數。
我們可以利用`Table.c`來觀察我們所設置的相關欄位的資訊:
```python=
>>> user_table.c.name
Column('name', String(length=30), table=<user_account>)
>>> user_table.c.keys()
['id', 'name', 'fullname']
```
這可以讓我們觀察到所設置的資料表的欄位類型跟長度,回頭看上面的資料表定義,確實我們就是把`name`的長度設置為30。並且你也可以利用`metadata_obj`來取得該集名中的所有資料表資訊:
```python=
>>> metadata_obj.sorted_tables
[Table('user_account', MetaData(), Column('id', Integer(), table=<user_account>, primary_key=True, nullable=False), Column('name', String(length=30), table=<user_account>), Column('fullname', String(), table=<user_account>), schema=None)]
```
關聯式資料庫的資料表中也可以定義外來鍵,也就是Foregin Key:
```python=
from sqlalchemy import ForeignKey
address_table = Table(
"address",
metadata_obj,
Column("id", Integer, primary_key=True),
Column("user_id", ForeignKey("user_account.id"), nullable=False),
Column("email_address", String, nullable=False),
)
```
上面我們再定義一個資料表,設置`user_id`為fk,並且不允許為null。
定義好資料表之後就要實際的生成資料表,所有的資料表資訊都是保存在`MetaData`,也就是我們所宣告的資料集集合`metadata_obj`裡面,因此我們是可以直接利用這個物件來生成資料表的:
```python=
metadata_obj.create_all(engine)
```
## 建置資料表(ORM)
如果是ORM在定義資料表的話就有些許的不同,主要是利用`DeclarativeBase`,但這並不代表它就不存在`MetaData`:
```python=
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
```
事實上,`DeclarativeBase`的屬性中就存在著`metadata`,這就是`MetaData`。
:::info
可參考[官方文件](https://docs.sqlalchemy.org/en/20/orm/mapping_api.html#sqlalchemy.orm.DeclarativeBase.metadata)說明
:::
現在就利用剛剛所設置的類別`Base`來為剛剛所設置的兩張資料表定義兩個ORM mapped classed:
```python=
from typing import List
from typing import Optional
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(30))
fullname: Mapped[Optional[str]]
addresses: Mapped[List["Address"]] = relationship(back_populates="user")
def __repr__(self) -> str:
return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"
class Address(Base):
__tablename__ = "address"
id: Mapped[int] = mapped_column(primary_key=True)
email_address: Mapped[str]
user_id = mapped_column(ForeignKey("user_account.id"))
user: Mapped[User] = relationship(back_populates="addresses")
def __repr__(self) -> str:
return f"Address(id={self.id!r}, email_address={self.email_address!r})"
```
* 8、17行的`__tablename__`是建置過程中生成`Table`的依據,總之就是資料表名稱了
* 為了能夠指向`Table`內的欄位,利用`mapped_column`來建構,結合型別注釋的`Mapped`來生成`Column`
* 可以利用python型別來對應資料庫型別,所以看的到`Mapped[str]`對應資料庫的string
* 21行則是設置兩者之間的關聯,`Mapped[User]`指向另一個類別
* 13、22行就單純設置一個給人看的訊息
這兩個類別現在是ORM Mapped Classes,這樣的生成方式又稱為Declarative Table Configuration。可以看的出來整個設置的表達式跟1.4以前的版本有很大的差異。當然如果你習慣過去的寫法的話還是可以這麼做的:
```python=
class User(Base):
__tablename__ = "user_account"
id = mapped_column(Integer, primary_key=True)
name = mapped_column(String(30), nullable=False)
fullname = mapped_column(String)
addresses = relationship("Address", back_populates="user")
# ... definition continues
```
值得注意的是,不是`Column`,而是`mapped_column`。
當我們整個架構都設置好要真正的生成資料表的時候就可以執行:
```python=
Base.metadata.create_all(engine)
```
不過因為我們剛剛的練習中已經有生成資料表了,所以這次的執行並不會再次的生成資料表。
## Table Reflection
剛剛大致的操作都是一路的建置,如果有個什麼需求需要你拉出目前某個`Table`的狀況的話,那就可以利用這個table reflection的操作來處理:
```python=
some_table = Table("some_table", metadata_obj, autoload_with=engine)
```
`some_table`是最一開始的範例所設置的資料表,裡面有`x, y`兩個欄位。這邊做的是,我們希望從`metadata_obj`這個集合中取得資料表為`some_table`的資料,結果如下:
```
Table('some_table', MetaData(),
Column('x', INTEGER(), table=<some_table>),
Column('y', INTEGER(), table=<some_table>),
schema=None)
```