Skip to content

many_to_many

ManyToManyField

Bases: ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationProtocol

Actual class returned from ManyToMany function call and stored in model_fields.

Source code in ormar/fields/many_to_many.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationProtocol):
    """
    Actual class returned from ManyToMany function call and stored in model_fields.
    """

    def __init__(self, **kwargs: Any) -> None:
        if TYPE_CHECKING:  # pragma: no cover
            self.__type__: type
            self.to: Type["Model"]
            self.through: Type["Model"]
        super().__init__(**kwargs)

    def get_source_related_name(self) -> str:
        """
        Returns name to use for source relation name.
        For FK it's the same, differs for m2m fields.
        It's either set as `related_name` or by default it's field name.
        :return: name of the related_name or default related name.
        :rtype: str
        """
        return (
            self.through.Meta.model_fields[
                self.default_source_field_name()
            ].related_name
            or self.name
        )

    def has_unresolved_forward_refs(self) -> bool:
        """
        Verifies if the filed has any ForwardRefs that require updating before the
        model can be used.

        :return: result of the check
        :rtype: bool
        """
        return self.to.__class__ == ForwardRef or self.through.__class__ == ForwardRef

    def evaluate_forward_ref(self, globalns: Any, localns: Any) -> None:
        """
        Evaluates the ForwardRef to actual Field based on global and local namespaces

        :param globalns: global namespace
        :type globalns: Any
        :param localns: local namespace
        :type localns: Any
        :return: None
        :rtype: None
        """
        if self.to.__class__ == ForwardRef:
            self.to = evaluate_forwardref(
                self.to, globalns, localns or None  # type: ignore
            )

            (self.__type__, self.column_type) = populate_m2m_params_based_on_to_model(
                to=self.to, nullable=self.nullable
            )

        if self.through.__class__ == ForwardRef:
            self.through = evaluate_forwardref(
                self.through, globalns, localns or None  # type: ignore
            )
            forbid_through_relations(self.through)

    def get_relation_name(self) -> str:
        """
        Returns name of the relation, which can be a own name or through model
        names for m2m models

        :return: result of the check
        :rtype: bool
        """
        if self.self_reference and self.name == self.self_reference_primary:
            return self.default_source_field_name()
        return self.default_target_field_name()

    def get_source_model(self) -> Type["Model"]:
        """
        Returns model from which the relation comes -> either owner or through model

        :return: source model
        :rtype: Type["Model"]
        """
        return self.through

    def create_default_through_model(self) -> None:
        """
        Creates default empty through model if no additional fields are required.
        """
        owner_name = self.owner.get_name(lower=False)
        to_name = self.to.get_name(lower=False)
        class_name = f"{owner_name}{to_name}"
        table_name = f"{owner_name.lower()}s_{to_name.lower()}s"
        new_meta_namespace = {
            "tablename": table_name,
            "database": self.owner.Meta.database,
            "metadata": self.owner.Meta.metadata,
        }
        new_meta = type("Meta", (), new_meta_namespace)
        through_model = type(
            class_name,
            (ormar.Model,),
            {"Meta": new_meta, "id": ormar.Integer(name="id", primary_key=True)},
        )
        self.through = cast(Type["Model"], through_model)

create_default_through_model()

Creates default empty through model if no additional fields are required.

Source code in ormar/fields/many_to_many.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def create_default_through_model(self) -> None:
    """
    Creates default empty through model if no additional fields are required.
    """
    owner_name = self.owner.get_name(lower=False)
    to_name = self.to.get_name(lower=False)
    class_name = f"{owner_name}{to_name}"
    table_name = f"{owner_name.lower()}s_{to_name.lower()}s"
    new_meta_namespace = {
        "tablename": table_name,
        "database": self.owner.Meta.database,
        "metadata": self.owner.Meta.metadata,
    }
    new_meta = type("Meta", (), new_meta_namespace)
    through_model = type(
        class_name,
        (ormar.Model,),
        {"Meta": new_meta, "id": ormar.Integer(name="id", primary_key=True)},
    )
    self.through = cast(Type["Model"], through_model)

evaluate_forward_ref(globalns, localns)

Evaluates the ForwardRef to actual Field based on global and local namespaces

Parameters:

Name Type Description Default
globalns Any

global namespace

required
localns Any

local namespace

required

Returns:

Type Description
None

None

Source code in ormar/fields/many_to_many.py
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
def evaluate_forward_ref(self, globalns: Any, localns: Any) -> None:
    """
    Evaluates the ForwardRef to actual Field based on global and local namespaces

    :param globalns: global namespace
    :type globalns: Any
    :param localns: local namespace
    :type localns: Any
    :return: None
    :rtype: None
    """
    if self.to.__class__ == ForwardRef:
        self.to = evaluate_forwardref(
            self.to, globalns, localns or None  # type: ignore
        )

        (self.__type__, self.column_type) = populate_m2m_params_based_on_to_model(
            to=self.to, nullable=self.nullable
        )

    if self.through.__class__ == ForwardRef:
        self.through = evaluate_forwardref(
            self.through, globalns, localns or None  # type: ignore
        )
        forbid_through_relations(self.through)

get_relation_name()

Returns name of the relation, which can be a own name or through model names for m2m models

Returns:

Type Description
bool

result of the check

Source code in ormar/fields/many_to_many.py
239
240
241
242
243
244
245
246
247
248
249
def get_relation_name(self) -> str:
    """
    Returns name of the relation, which can be a own name or through model
    names for m2m models

    :return: result of the check
    :rtype: bool
    """
    if self.self_reference and self.name == self.self_reference_primary:
        return self.default_source_field_name()
    return self.default_target_field_name()

get_source_model()

Returns model from which the relation comes -> either owner or through model

Returns:

Type Description
Type["Model"]

source model

Source code in ormar/fields/many_to_many.py
251
252
253
254
255
256
257
258
def get_source_model(self) -> Type["Model"]:
    """
    Returns model from which the relation comes -> either owner or through model

    :return: source model
    :rtype: Type["Model"]
    """
    return self.through

Returns name to use for source relation name. For FK it's the same, differs for m2m fields. It's either set as related_name or by default it's field name.

Returns:

Type Description
str

name of the related_name or default related name.

Source code in ormar/fields/many_to_many.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def get_source_related_name(self) -> str:
    """
    Returns name to use for source relation name.
    For FK it's the same, differs for m2m fields.
    It's either set as `related_name` or by default it's field name.
    :return: name of the related_name or default related name.
    :rtype: str
    """
    return (
        self.through.Meta.model_fields[
            self.default_source_field_name()
        ].related_name
        or self.name
    )

has_unresolved_forward_refs()

Verifies if the filed has any ForwardRefs that require updating before the model can be used.

Returns:

Type Description
bool

result of the check

Source code in ormar/fields/many_to_many.py
203
204
205
206
207
208
209
210
211
def has_unresolved_forward_refs(self) -> bool:
    """
    Verifies if the filed has any ForwardRefs that require updating before the
    model can be used.

    :return: result of the check
    :rtype: bool
    """
    return self.to.__class__ == ForwardRef or self.through.__class__ == ForwardRef

ManyToMany(to, through=None, *, name=None, unique=False, virtual=False, **kwargs)

Despite a name it's a function that returns constructed ManyToManyField. This function is actually used in model declaration (as ormar.ManyToMany(ToModel, through=ThroughModel)).

Accepts number of relation setting parameters as well as all BaseField ones.

Parameters:

Name Type Description Default
to 'ToType'

target related ormar Model

required
through Optional['ToType']

through model for m2m relation

None
name str

name of the database field - later called alias

None
unique bool

parameter passed to sqlalchemy.ForeignKey, unique flag

False
virtual bool

marks if relation is virtual. It is for reversed FK and auto generated FK on through model in Many2Many relations.

False
kwargs Any

all other args to be populated by BaseField

required

Returns:

Type Description
ManyToManyField

ormar ManyToManyField with m2m relation to selected model

Source code in ormar/fields/many_to_many.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def ManyToMany(  # type: ignore
    to: "ToType",
    through: Optional["ToType"] = None,
    *,
    name: str = None,
    unique: bool = False,
    virtual: bool = False,
    **kwargs: Any,
) -> "RelationProxy[T]":
    """
    Despite a name it's a function that returns constructed ManyToManyField.
    This function is actually used in model declaration
    (as ormar.ManyToMany(ToModel, through=ThroughModel)).

    Accepts number of relation setting parameters as well as all BaseField ones.

    :param to: target related ormar Model
    :type to: Model class
    :param through: through model for m2m relation
    :type through: Model class
    :param name: name of the database field - later called alias
    :type name: str
    :param unique: parameter passed to sqlalchemy.ForeignKey, unique flag
    :type unique: bool
    :param virtual: marks if relation is virtual.
    It is for reversed FK and auto generated FK on through model in Many2Many relations.
    :type virtual: bool
    :param kwargs: all other args to be populated by BaseField
    :type kwargs: Any
    :return: ormar ManyToManyField with m2m relation to selected model
    :rtype: ManyToManyField
    """
    related_name = kwargs.pop("related_name", None)
    nullable = kwargs.pop("nullable", True)

    owner = kwargs.pop("owner", None)
    self_reference = kwargs.pop("self_reference", False)

    orders_by = kwargs.pop("orders_by", None)
    related_orders_by = kwargs.pop("related_orders_by", None)

    skip_reverse = kwargs.pop("skip_reverse", False)
    skip_field = kwargs.pop("skip_field", False)

    through_relation_name = kwargs.pop("through_relation_name", None)
    through_reverse_relation_name = kwargs.pop("through_reverse_relation_name", None)

    if through is not None and through.__class__ != ForwardRef:
        forbid_through_relations(cast(Type["Model"], through))

    validate_not_allowed_fields(kwargs)

    if to.__class__ == ForwardRef:
        __type__ = (
            Union[to, List[to]]  # type: ignore
            if not nullable
            else Optional[Union[to, List[to]]]  # type: ignore
        )
        column_type = None
    else:
        __type__, column_type = populate_m2m_params_based_on_to_model(
            to=to, nullable=nullable  # type: ignore
        )
    namespace = dict(
        __type__=__type__,
        to=to,
        through=through,
        alias=name,
        name=name,
        nullable=nullable,
        unique=unique,
        column_type=column_type,
        related_name=related_name,
        virtual=virtual,
        primary_key=False,
        index=False,
        pydantic_only=False,
        default=None,
        server_default=None,
        owner=owner,
        self_reference=self_reference,
        is_relation=True,
        is_multi=True,
        orders_by=orders_by,
        related_orders_by=related_orders_by,
        skip_reverse=skip_reverse,
        skip_field=skip_field,
        through_relation_name=through_relation_name,
        through_reverse_relation_name=through_reverse_relation_name,
    )

    Field = type("ManyToMany", (ManyToManyField, BaseField), {})
    return Field(**namespace)

forbid_through_relations(through)

Verifies if the through model does not have relations.

Parameters:

Name Type Description Default
through Type['Model']

through Model to be checked

required
Source code in ormar/fields/many_to_many.py
32
33
34
35
36
37
38
39
40
41
42
43
44
def forbid_through_relations(through: Type["Model"]) -> None:
    """
    Verifies if the through model does not have relations.

    :param through: through Model to be checked
    :type through: Type['Model]
    """
    if any(field.is_relation for field in through.Meta.model_fields.values()):
        raise ModelDefinitionError(
            f"Through Models cannot have explicit relations "
            f"defined. Remove the relations from Model "
            f"{through.get_name(lower=False)}"
        )

populate_m2m_params_based_on_to_model(to, nullable)

Based on target to model to which relation leads to populates the type of the pydantic field to use and type of the target column field.

Parameters:

Name Type Description Default
to Type['Model']

target related ormar Model

required
nullable bool

marks field as optional/ required

required

Returns:

Type Description
tuple with target pydantic type and target col type

Tuple[List, Any]

Source code in ormar/fields/many_to_many.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def populate_m2m_params_based_on_to_model(
    to: Type["Model"], nullable: bool
) -> Tuple[Any, Any]:
    """
    Based on target to model to which relation leads to populates the type of the
    pydantic field to use and type of the target column field.

    :param to: target related ormar Model
    :type to: Model class
    :param nullable: marks field as optional/ required
    :type nullable: bool
    :return: Tuple[List, Any]
    :rtype: tuple with target pydantic type and target col type
    """
    to_field = to.Meta.model_fields[to.Meta.pkname]
    __type__ = (
        Union[to_field.__type__, to, List[to]]  # type: ignore
        if not nullable
        else Optional[Union[to_field.__type__, to, List[to]]]  # type: ignore
    )
    column_type = to_field.column_type
    return __type__, column_type