SQLAlchemy 2.0 で複雑なテーブルをいろいろ作ったときのメモ

書き足していきます。

### __table_args__ が sqlalchemy.exc.ArgumentError のエラーを吐く

テーブル定義のクラスに

```python
    __table_args__ = (
        # データセットをまたがって同じグローバルIDをもつプロセスを扱うことがあるためデータセットIDとグローバルIDのセットでユニーク
        UniqueConstraint("hoge_id", "fuga_id", name="uq_samples_hoge_id_fuga_id")
    )
```
は
```
sqlalchemy.exc.ArgumentError: __table_args__ value must be a tuple, dict, or None
```

というエラーになります。正解は

```diff
    __table_args__ = 
    (
        # データセットをまたがって同じグローバルIDをもつプロセスを扱うことがあるためデータセットIDとグローバルIDのセットでユニーク
-        UniqueConstraint("hoge_id", "fuga_id", name="uq_samples_hoge_id_fuga_id")
+        UniqueConstraint("hoge_id", "fuga_id", name="uq_samples_hoge_id_fuga_id"),
    )
```

カンマが要ります。

### 単一継承テーブル (single table inheritance:STI) の子クラスで __table_args__ が使えない

ちゃんとカンマを入れてもダメです。

Stackoverflow で同じことを聞いてるかたがいました（ https://stackoverflow.com/questions/43832848/cant-define-table-args-on-a-child-class-in-a-single-table-inheritance-setup ）古い質問ではありますが、最新版のSQLAlchemry 2.0 でもおなじワークアラウンドが使えました。

子クラスでは __table_args__ が使えないので、クラスの外に定義を出します。クラスがSampleというクラスだった場合はクラス定義の外で

```python
UniqueConstraint(Sammple.hoge_id, Sample.fuga_id, name="uq_samples_hoge_id_fuga_id")
```

と書くことで子クラスに（このケースでは複合ユニークキー制約）書くことが出来ます。


## 同じテーブルへの外部キーが二つあるときに AmbiguousForeignKeysError になる
こんなエラーが出ます。

```
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship ***.***** - there are multiple foreign key paths linking the tables.  Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table.
```

片方だけで良いように思えますが。親子の relationship に両側とも foreign_keys パラメータがいります。

```Python
class Author(Base):
    __tablename__ = 'authors'
    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False)

    books = relationship('Book', foreign_keys='Book.author_id',     sub_books = relationship('Book', foreign_keys='Book.sub_author_id', back_populates='sub_author')

class Book(Base):
    __tablename__ = 'books'
    id = Column(Integer, primary_key=True)
    title = Column(String, nullable=False)
    author_id = Column(Integer, ForeignKey('authors.id'))
    sub_author_id = Column(Integer, ForeignKey('authors.id'))

    author = relationship('Author', foreign_keys=[author_id], back_populates='books')
    sub_author = relationship('Author', foreign_keys=[sub_author_id], back_populates='sub_books')
```

## 意図せず関係の無いテーブルへUPDATEやDELETEが発生する

どうも非同期利用時に relationship の cascade オプションで all を設定しているリレーションがあるとリレーション先が更新されたものとして扱われたりしてへんなクエリが発行されてしまうようです。公式のこのページの途中

https://docs.sqlalchemy.org/en/20/orm/cascades.html

> The all cascade option implies the refresh-expire cascade setting which may not be desirable when using the Asynchronous I/O (asyncio) extension, as it will expire related objects more aggressively than is typically appropriate in an explicit IO context. See the notes at Preventing Implicit IO when Using AsyncSession for further background.

allでなく個別に指定してくれ、all に含まれる refresh-expire がわるさをするから、ということのようです。all という設定は save-update, merge, refresh-expire, expunge が含まれているとのことなので、save-update, merge, expunge を設定してみました。

リンク先にも記述がありました。

https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#asyncio-orm-avoid-lazyloads

> Avoid using the all cascade option documented at Cascades in favor of listing out the desired cascade features explicitly. The all cascade option implies among others the refresh-expire setting, which means that the AsyncSession.refresh() method will expire the attributes on related objects, but not necessarily refresh those related objects assuming eager loading is not configured within the relationship(), leaving them in an expired state.

詳細はわかりにくいのですが、関連オブジェクトが期限切れの状態になったものを明示的に削除したと勘違いしてしまうってことかな……。


##  Joint Table Inheritanceで作られた継承関係があるモデルを flat=True で結合すると、エラーも出ずに関連テーブルのロードが無視される

これはめちゃくちゃ罠でした。テーブル継承であっても上に書いたような single table inheritanceであれば問題ないのが余計罠です。

Empolyee から Manager, Enginier が派生しているとします。
EmpolyeeにはCompanyがありManagerのみからSubCompanyへのリレーションがあるとしましょう。

```python
p_employee = with-polymorphic(Employee, [Manager, Enginier], flat=True, aliased=True)

select(Company)
  joinedload(Company.employee.of_type(p_employee)
  joinedload(p_employee.Manager.sub_company)
  ：
```

とか書くのと

```python
p_employee = with-polymorphic(Employee, [Manager, Enginier], flat=False, aliased=True)

select(Company)
  joinedload(Company.employee.of_type(p_employee)
  joinedload(p_employee.Manager.sub_company)
  ：
```

とでは sub_company がロードされているかどうかの違い出てきます。Ture にすると「joinedload(p_employee.Manager.sub_company)」がまるごと無視されるような挙動になるのですが、特に渓谷もエラーも出ません。アクセスしようとするとロードされてないよエラーになります。書いてあるのに何でロードされないんだよっ！　って思っちゃいますね。

そもそも、flat=True ってなんだろう……以下のリンク先の辺に書いてあることを読み解けばこの挙動を理解出来るのかも知れませんが、よく分かりませんでした……（Falseにしたほうが複雑なことになるのは分かりましたが、じゃあなんのためにあるの？って感じです）。

https://docs.sqlalchemy.org/en/20/orm/queryguide/inheritance.html#using-aliasing-with-with-polymorphic

上の例では joinedload ですが、そのまま selectinload に変えても同じ結果でした。もしかしたら、selectinload に変えた場合は selectin_polymorphic を使えば良かったのか……すいませんそちらは未確認です。