コンテンツにスキップ

多対多

ManyTomany(to、through)には、必要な引数toとオプションのthroughがあり、ターゲットモデルクラスとリレーショナルモデルクラスを使用します。

Sqlalchemyカラムと型は、ターゲットモデルから自動的に取得されます。

  • Sqlalchemyカラム:ターゲットモデルの主キーカラムのクラス
  • 型(pydantic用):ターゲットモデルの型

モデルの定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from typing import List, Optional

import databases
import ormar
import sqlalchemy

DATABASE_URL = "sqlite:///test.db"

ormar_base_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData()
)


class Author(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="authors")

    id: int = ormar.Integer(primary_key=True)
    first_name: str = ormar.String(max_length=80)
    last_name: str = ormar.String(max_length=80)


class Category(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="categories")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=40)


class Post(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="posts")

    id: int = ormar.Integer(primary_key=True)
    title: str = ormar.String(max_length=200)
    categories: Optional[List[Category]] = ormar.ManyToMany(Category)
    author: Optional[Author] = ormar.ForeignKey(Author)

サンプルデータの作成:

1
2
3
guido=await Author. object.create(first_name="guido",last_name="Van Rossum")
post=await Post. object.create(title="こんにちは、M 2 M",著者=ガイド)
news=await Category. object.create(name="ニュース")

逆の関係

外部キーフィールドは、リレーションシップの裏面を自動的に登録します。

デフォルトでは、以下のコードスニペットの投稿のように、サブ(ソース)モデル名+sです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Category(ormar. Model):
    or_config=base_or_config. copy(tablename="カテゴリ")

    id: int=ormar.Integer(プライマリキー=True)
    name: str=ormar.String(max_length=40)


class Post(ormar. Model):
    base_or_config. copy()

    id: int=ormar.Integer(プライマリキー=True)
    title: str=ormar.String(max_length=200)
    カテゴリ:オプション[リスト[カテゴリ]]=ormar. ManyTomany(カテゴリ)

#サンプルデータを作成する
post=await Post. object.create(title="Hello,M 2 M")
news=await Category. object.create(name="ニュース")
await post. category.add(ニュース)

#今、あなたは両方の側からクエリとアクセスができます:
post_check=Post. object.select_関連("カテゴリ").get()
assert post_check. category[0]==ニュース

#自動登録裏側からのクエリ
カテゴリーチェック=カテゴリー。オブジェクト。関連する選択("投稿")。取得()
assert Category_check. post[0]==post

リバースリレーションシップ公開APIは、親から関連するオブジェクトを管理します。

関連する名前

デフォルトでは、相関名は外部キーリレーションシップ(class.name. lower()+'s')と同じように生成されますが、次のように相関名パラメーターを指定することで、この名前を上書きすることもできます。

1
2
3
カテゴリ:オプション[Union[Category, List[Category]]]=ormar.ManyTomany(
        カテゴリー、through=PostCategory、関連する名前="new_カテゴリー"
    )

!!!は、同じモデルに複数のリレーションシップを提供しないと、関連する_nameを自動的に生成できなくなることを警告します。 したがって、この場合、1つのフィールド(1つはデフォルトで生成できます)を除くすべてのフィールド、または関連するすべてのフィールドに関連する_nameを指定する必要があります。

逆関係をスキップする

逆の関係を望まないと確信している場合は、ManyTomanyのskip_verse=Trueフラグを使用できます。

内部的にskip_verseフラグを設定している場合は、リレーションシップの反対側にフィールドが登録されたままなので、次のことができます。

  • リバースモデルの関連するモデルフィールドによるフィルタリング
  • order_byリバースモデルの関連するモデルフィールド

しかし、あなたはできません:

  • 関連_nameを使用して、リバースモデルから関連フィールドにアクセスします
  • モデルの反対側から相関を選択しても、返されたモデルは逆インスタンスに入力されません(接続をブロックしないため、関係をフィルタリングしてソートできます)
  • この関係はmodel_dump()とjson()には入力されません
  • がディクショナリまたはjsonから(fastapiを介しても)入力されている場合、ネストされた関連オブジェクトを渡すことはできません。pydantic構成の追加設定によっては、無視されるか、エラーが発生します。

例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Category(ormar. Model):
    or_config=base_or_config. copy(tablename="カテゴリ")

    id: int=ormar.Integer(プライマリキー=True)
    name: str=ormar.String(max_length=40)


class Post(ormar. Model):
    base_or_config. copy()

    id: int=ormar.Integer(プライマリキー=True)
    title: str=ormar.String(max_length=200)
    カテゴリ:オプション[List[Category]]=ormar. ManyTomany(Category,skip_verse=True)

#サンプルデータを作成する
post=await Post. object.create(title="Hello,M 2 M")
news=await Category. object.create(name="ニュース")
await post. category.add(ニュース)

assert post. category[0]==news#ok
assert news. post#属性エラー!

#order_byでも使えます
カテゴリー=(
    awaitカテゴリー.オブジェクト. select_関連("投稿").order_by("投稿_タイトル").all()
)
assertカテゴリー[0]. first_name=="テスト"

#明示的に投稿しても、投稿者には入力されないことに注意してください。
#select_関連に含まれる-model_dump()に投稿がないことに注意してください
assert news. model_dump({"id"}を除外)=={"name": "News"}

#関連するモデルのフィールドをフィルタリングすることができます。
カテゴリー=awaitカテゴリー.オブジェクト.フィルター(post__title="こんにちは、M 2 M"). all()
assertカテゴリー[0]. name=="ニュース"
カテゴリーの長さ==1

モデルを通じて

または、他のフィールドを追加したい場合は、モデルクラスを明示的に作成して渡すこともできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import databases
import ormar
import sqlalchemy

DATABASE_URL = "sqlite:///test.db"

ormar_base_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData()
)


class Category(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="categories")

    id = ormar.Integer(primary_key=True)
    name = ormar.String(max_length=40)


class PostCategory(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="posts_x_categories")

    id: int = ormar.Integer(primary_key=True)
    sort_order: int = ormar.Integer(nullable=True)
    param_name: str = ormar.String(default="Name", max_length=200)


class Post(ormar.Model):
    ormar_config = ormar_base_config.copy()

    id: int = ormar.Integer(primary_key=True)
    title: str = ormar.String(max_length=200)
    categories = ormar.ManyToMany(Category, through=PostCategory)

!!!警告モデルを通じて提供しなくても、自動的に作成され、alembicマイグレーションの例に含める必要があることに注意してください。

!!!tip Throughモデル名またはそのモデルのデータベーステーブル名をカスタマイズする場合は、Throughモデルを指定する必要があることに注意してください。

「合格」フィールドを指定しない場合、そのフィールドが生成されます。

デフォルトの命名規則は次のとおりです:

  • クラス名については、2つのクラス名(親+その他)を組み合わせたものなので、上の例ではPostCategoryになります
  • テーブル名の場合も同様ですが、クラスの小文字名の末尾にアンダースコアとsがあります。これは、上記の例では、ポスト_カテゴリです。

リレーショナル名によるカスタマイズ

デフォルトでは、モデルリレーションシップ名は、関連するモデル名を小文字にします。

このような例では

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
。。。 #コース宣言省略
class Student(ormar. Model):
    base_or_config. copy()

    id: int=ormar.Integer(プライマリキー=True)
    name: str=ormar.String(max_length=100)
    コース=ormar. ManyTomany(コース)

#以下のようなデフォルトのスルーモデルを生成します(例を簡略化)。
class StudentCourse(ormar. Model):
    or_config=base_or_config. copy(tablename="学生コース")

    id: int=ormar.Integer(プライマリキー=True)
    student=ormar. ForeignKey(学生)#デフォルトの名前
    コース=ormar. ForeignKey(コース)#デフォルト名

Throughモデルのフィールド/リレーションシップ名をカスタマイズするには、ManyTomanyの新しいパラメーターを使用できます。

  • through_Relations_name-ManyTomanyを宣言するモデルへのフィールド名
  • through_逆_関係_name-ManyTomanyにつながるモデルのフィールド名

例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
。。。 #コース宣言省略
base_ormar_config=ormar. OrmarConfig(ベースの設定
    Database("sqlite:///db. sqlite"),
    metadata=sqlalchemy. MetaData(),
)


class Student(ormar. Model):
    base_or_config. copy()

    id: int=ormar.Integer(プライマリキー=True)
    name: str=ormar.String(max_length=100)
    コース=ormar. ManyTomany(コース、
                               through_Relations_name="student_id",
                               through_逆関係名="コースID")

#次のようなモデルを通じて生成されます(例を簡略化)。
class StudentCourse(ormar. Model):
    or_config=base_or_config. copy(tablename="学生コース")

    id: int=ormar.Integer(プライマリキー=True)
    student_id=ormar. ForeignKey(Student)#through_Relations_nameで設定
    Course_id=ormar. ForeignKey(コース)#through_逆関係名で設定

!!!注Throughモデルでは明示的にリレーションシップを宣言することは禁止されているため、独自のカスタムThroughモデルを提供しても、そこで名前を変更することはできません 。

フィールドを通して

フィールドを渡すと、リレーションシップの反対側に自動的に追加されます。

公開されたフィールドは、クラス名で小文字と名付けられます。

公開されているフィールドは、ManyTomanyフィールドに埋め込まれているため、明示的にリレーションシップをロードしません。これは、Throughモデルで他のフィールドが提供されている場合にのみ役立ちます。

サンプルモデルの設定では、次のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import databases
import ormar
import sqlalchemy

DATABASE_URL = "sqlite:///test.db"

ormar_base_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData()
)


class Category(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="categories")

    id = ormar.Integer(primary_key=True)
    name = ormar.String(max_length=40)


class PostCategory(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="posts_x_categories")

    id: int = ormar.Integer(primary_key=True)
    sort_order: int = ormar.Integer(nullable=True)
    param_name: str = ormar.String(default="Name", max_length=200)


class Post(ormar.Model):
    ormar_config = ormar_base_config.copy()

    id: int = ormar.Integer(primary_key=True)
    title: str = ormar.String(max_length=200)
    categories = ormar.ManyToMany(Category, through=PostCategory)

throughフィールドは、ほとんどのQuerySet操作で通常のモデルフィールドとして使用できます。

パスフィールドはクエリの関連するエンドにのみ追加されることに注意してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
post=await Post. object.select_関連("カテゴリ").get()
#ソースモデルにはスルーフィールドがありません
assert post. postcategoryはなし
#関連モデルはスルーフィールドを持っています
assert post. category[0].postcategory is not None

#逆クエリにも同じことが適用されます。
カテゴリー=awaitカテゴリー。オブジェクト。関連する選択("投稿")。取得()
assertカテゴリー。postcategoryはなし
assertカテゴリー。ポスト[0]。ポストカテゴリーはなし

スルーフィールドを使用して、データをフィルタリングできます。

1
2
3
4
5
投稿=(
        await Post. object.select_関連("カテゴリ")
        . filter(ポストカテゴリ__ソート_順序__gt=1)
        . get()
        )

!!!tip実際のインスタンスはソースモデルには埋め込まれていませんが、クエリ、order by文などでは、両側からモデルを介してアクセスできることに注意してください。したがって、次のクエリはまったく同じ効果があります(カテゴリを介してアクセスすることに注意してください)

1
2
3
4
5
6
7
```python
post = (
    await Post.objects.select_related("categories")
    .filter(categories__postcategory__sort_order__gt=1)
    .get()
    )
```

モデルを使用すると、クエリを順番に使用できます。

1
2
3
4
5
投稿=(
        await Post. object.select_関連("カテゴリ")
        . order_by("-postcategory__sort_order")
        . get()
    )

通常のQuerySet方式では、fieldとout of_fieldを使用して列のサブセットを選択することもできます。

1
2
3
4
5
ポスト2=(
        await Post. object.select_関連("カテゴリ")
        除外フィールド("Post Category__param_name")
        . get()
        )

!!!警告ManyTomanyフィールドにリレーションシップが埋め込まれているため、through fieldはすべてのリレーションシップフィールドを明示的に無効にするため、データベースからフィールドをリロードする前にsave()やupdate()などの標準モデルメソッドを使用しないでください。

パスフィールドをその場で変更したい場合は、データベースからリロードすることを忘れないでください。そうしないと、リレーションシップを「なし」に設定して、フィールドを効果的に無効にします!

1
2
3
4
#変更前に常にフィールドをリロードする
post2. category[0].postcategory.load()を待ちます。
#その後、フィールドを更新するだけ
post2. category[0].postcategory.updateを待っています(ソート順=3)

モデルを再読み込みすると、実際にはリレーションシップがpk_onlyモデル(プライマリキーのみが設定されている)として再読み込みされるため、完全には満たされませんが、更新時にリレーションシップを保持するには十分です。

!!!は、ie fastapiを使用している場合、フィールドを介して部分的にロードされた関連モデルがpydantic検証エラーを引き起こす可能性があると警告しています(これがデフォルトでそれらが入力されない主な理由です)。したがって、応答で関連フィールドを除外するか、関連モデルを完全にロードする必要があります。 上記の例では、これは次のことを意味します。python await post2.categories[0].postcategory.post.load() await post2.categories[0].postcategory.category.load() または、load_all():python await post2.categories[0].postcategory.load_all() を使用できます

推奨される更新方法は、クエリセットプロキシupdate()メソッドです

1
2
#スルーフィールドで目的の関連モデルをフィルタリングし、スルーフィールドパラメータのみを更新する
post 2. category.filter(name='テストカテゴリ').update(postcategory={"sort_order": 3})を待つ

関係法

追加する

追加(プロジェクト:モデル、**kwargs)

多対多のリレーションシップにモデルを追加できます。

1
2
3
4
#投稿にカテゴリを追加します。
await post. category.add(ニュース)
#または反対側から:
await news. post.add(投稿)

!!!警告None以外のすべての場合、関連するモデルのプライマリキー値はデータベースに存在する必要があります。

1
そうしないと、データベースドライバライブラリによってIntegrityErrorが発生します。

追加のフィールドを持つThroughモデルを使用してモデルを宣言した場合、サブモデルをリレーションシップに追加するときにそれらを埋めることができます。

これを行うには、フィールド名と値を含むキーワード引数をadd()呼び出しに渡します。

これは多対多の関係にのみ適用されることに注意してください。

1
2
3
4
post=await Post(title="テストポスト"). save()
category=await Category(name="テストカテゴリ"). save()
#モデルフィールドを参照するモデルパス引数とは別に
post. category.addを待ちます(カテゴリー、ソート順=1、パラメーター名='テスト')

排除する

関連するモデルを1つずつ削除します。

データベース内のリレーションシップも削除します。

1
await news. post.remove(ポスト)

クリア

関連するすべてのモデルを1回の呼び出しで削除します。

データベース内のリレーションシップも削除します。

news. post.clear()を待つ

クエリセットエージェント

リバースリレーションシップは、通常のクエリと同じように関連するモデルをクエリできるQuerysetProxy APIを公開します。

QuerySetで使用できるメソッドを確認するには、以下のquerysetproxyをお読みください