Referential Actions Enum Class (#735)
* feat: add action enum class to referential actions * feat: write validation func for action name string * test: write test for validation referential action * fix: backend database running for action test * fix: set the string type of value enum class * fix: debuging return statement type for validation * fix: return non empty for empty action * refactor: change in line return if statement * fix: add iterate method in read document md * fix: update foreign key docstring types * docs: write documention of refernal actions * docs: complete referential actions descriptions * refactor: rename and reposition referential action * refactor: change validate referential action func * test: add assert check for really deleted rows * fix: debug error problem in renamed enum class * fix: apply black formatted codes * docs: update the document for referential actions * docs: added note for server default argument Co-authored-by: collerek <collerek@gmail.com>
This commit is contained in:
@ -6,6 +6,7 @@ Following methods allow you to load data from the database.
|
|||||||
* `get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs) -> Tuple[Model, bool]`
|
* `get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs) -> Tuple[Model, bool]`
|
||||||
* `first(*args, **kwargs) -> Model`
|
* `first(*args, **kwargs) -> Model`
|
||||||
* `all(*args, **kwargs) -> List[Optional[Model]]`
|
* `all(*args, **kwargs) -> List[Optional[Model]]`
|
||||||
|
* `iterate(*args, **kwargs) -> AsyncGenerator[Model]`
|
||||||
|
|
||||||
|
|
||||||
* `Model`
|
* `Model`
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
# ForeignKey
|
# ForeignKey
|
||||||
|
|
||||||
`ForeignKey(to, related_name=None)` has required parameters `to` that takes target `Model` class.
|
`ForeignKey(to: Model, *, name: str = None, unique: bool = False, nullable: bool = True,
|
||||||
|
related_name: str = None, virtual: bool = False, onupdate: Union[ReferentialAction, str] = None,
|
||||||
|
ondelete: Union[ReferentialAction, str] = None, **kwargs: Any)`
|
||||||
|
has required parameters `to` that takes target `Model` class.
|
||||||
|
|
||||||
Sqlalchemy column and Type are automatically taken from target `Model`.
|
Sqlalchemy column and Type are automatically taken from target `Model`.
|
||||||
|
|
||||||
@ -182,6 +185,41 @@ But you can overwrite this name by providing `related_name` parameter like below
|
|||||||
the `related_name` for you. Therefore, in that situation you **have to** provide `related_name`
|
the `related_name` for you. Therefore, in that situation you **have to** provide `related_name`
|
||||||
for all but one (one can be default and generated) or all related fields.
|
for all but one (one can be default and generated) or all related fields.
|
||||||
|
|
||||||
|
## Referential Actions
|
||||||
|
|
||||||
|
When an object referenced by a ForeignKey is changed (deleted or updated),
|
||||||
|
ormar will set the SQL constraint specified by the `ondelete` and `onupdate` argument.
|
||||||
|
|
||||||
|
The possible values for `ondelete` and `onupdate` are found in `ormar.ReferentialAction`:
|
||||||
|
|
||||||
|
!!!note
|
||||||
|
Instead of `ormar.ReferentialAction`, you can directly pass string values to these two arguments, but this is not recommended because it will break the integrity.
|
||||||
|
|
||||||
|
### CASCADE
|
||||||
|
|
||||||
|
Whenever rows in the parent (referenced) table are deleted (or updated), the respective rows of the child (referencing) table with a matching foreign key column will be deleted (or updated) as well. This is called a cascade delete (or update).
|
||||||
|
|
||||||
|
### RESTRICT
|
||||||
|
|
||||||
|
A value cannot be updated or deleted when a row exists in a referencing or child table that references the value in the referenced table.
|
||||||
|
|
||||||
|
Similarly, a row cannot be deleted as long as there is a reference to it from a referencing or child table.
|
||||||
|
|
||||||
|
### SET_NULL
|
||||||
|
|
||||||
|
Set the ForeignKey to `None`; this is only possible if `nullable` is True.
|
||||||
|
|
||||||
|
### SET_DEFAULT
|
||||||
|
|
||||||
|
Set the ForeignKey to its default value; a `server_default` for the ForeignKey must be set.
|
||||||
|
|
||||||
|
!!!note
|
||||||
|
Note that the `default` value is not allowed and you must do this through `server_default`, which you can read about in [this section][server_default].
|
||||||
|
|
||||||
|
### DO_NOTHING
|
||||||
|
|
||||||
|
Take `NO ACTION`; NO ACTION and RESTRICT are very much alike. The main difference between NO ACTION and RESTRICT is that with NO ACTION the referential integrity check is done after trying to alter the table. RESTRICT does the check before trying to execute the UPDATE or DELETE statement. Both referential actions act the same if the referential integrity check fails: the UPDATE or DELETE statement will result in an error.
|
||||||
|
|
||||||
## Relation Setup
|
## Relation Setup
|
||||||
|
|
||||||
You have several ways to set-up a relationship connection.
|
You have several ways to set-up a relationship connection.
|
||||||
@ -243,3 +281,4 @@ Finally you can explicitly set it to None (default behavior if no value passed).
|
|||||||
[fields]: ./queries.md#fields
|
[fields]: ./queries.md#fields
|
||||||
[exclude_fields]: ./queries.md#exclude_fields
|
[exclude_fields]: ./queries.md#exclude_fields
|
||||||
[order_by]: ./queries.md#order_by
|
[order_by]: ./queries.md#order_by
|
||||||
|
[server_default]: ../fields/common-parameters.md#server-default
|
||||||
@ -57,7 +57,6 @@ from ormar.fields import (
|
|||||||
Float,
|
Float,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
ForeignKeyField,
|
ForeignKeyField,
|
||||||
IndexColumns,
|
|
||||||
Integer,
|
Integer,
|
||||||
JSON,
|
JSON,
|
||||||
LargeBinary,
|
LargeBinary,
|
||||||
@ -70,7 +69,9 @@ from ormar.fields import (
|
|||||||
Time,
|
Time,
|
||||||
UUID,
|
UUID,
|
||||||
UniqueColumns,
|
UniqueColumns,
|
||||||
|
IndexColumns,
|
||||||
CheckColumns,
|
CheckColumns,
|
||||||
|
ReferentialAction,
|
||||||
) # noqa: I100
|
) # noqa: I100
|
||||||
from ormar.models import ExcludableItems, Extra, Model
|
from ormar.models import ExcludableItems, Extra, Model
|
||||||
from ormar.models.metaclass import ModelMeta
|
from ormar.models.metaclass import ModelMeta
|
||||||
@ -103,6 +104,7 @@ __all__ = [
|
|||||||
"Float",
|
"Float",
|
||||||
"ManyToMany",
|
"ManyToMany",
|
||||||
"Model",
|
"Model",
|
||||||
|
"Action",
|
||||||
"ModelDefinitionError",
|
"ModelDefinitionError",
|
||||||
"MultipleMatches",
|
"MultipleMatches",
|
||||||
"NoMatch",
|
"NoMatch",
|
||||||
@ -114,6 +116,7 @@ __all__ = [
|
|||||||
"UniqueColumns",
|
"UniqueColumns",
|
||||||
"IndexColumns",
|
"IndexColumns",
|
||||||
"CheckColumns",
|
"CheckColumns",
|
||||||
|
"ReferentialAction",
|
||||||
"QuerySetProtocol",
|
"QuerySetProtocol",
|
||||||
"RelationProtocol",
|
"RelationProtocol",
|
||||||
"ModelMeta",
|
"ModelMeta",
|
||||||
|
|||||||
@ -28,6 +28,7 @@ from ormar.fields.model_fields import (
|
|||||||
from ormar.fields.parsers import DECODERS_MAP, ENCODERS_MAP, SQL_ENCODERS_MAP
|
from ormar.fields.parsers import DECODERS_MAP, ENCODERS_MAP, SQL_ENCODERS_MAP
|
||||||
from ormar.fields.sqlalchemy_encrypted import EncryptBackend, EncryptBackends
|
from ormar.fields.sqlalchemy_encrypted import EncryptBackend, EncryptBackends
|
||||||
from ormar.fields.through_field import Through, ThroughField
|
from ormar.fields.through_field import Through, ThroughField
|
||||||
|
from ormar.fields.referential_actions import ReferentialAction
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Decimal",
|
"Decimal",
|
||||||
@ -38,7 +39,6 @@ __all__ = [
|
|||||||
"DateTime",
|
"DateTime",
|
||||||
"String",
|
"String",
|
||||||
"JSON",
|
"JSON",
|
||||||
"IndexColumns",
|
|
||||||
"Integer",
|
"Integer",
|
||||||
"Text",
|
"Text",
|
||||||
"Float",
|
"Float",
|
||||||
@ -59,4 +59,7 @@ __all__ = [
|
|||||||
"SQL_ENCODERS_MAP",
|
"SQL_ENCODERS_MAP",
|
||||||
"LargeBinary",
|
"LargeBinary",
|
||||||
"UniqueColumns",
|
"UniqueColumns",
|
||||||
|
"IndexColumns",
|
||||||
|
"CheckColumns",
|
||||||
|
"ReferentialAction",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -21,6 +21,7 @@ from pydantic.typing import ForwardRef, evaluate_forwardref
|
|||||||
|
|
||||||
import ormar # noqa I101
|
import ormar # noqa I101
|
||||||
from ormar.exceptions import ModelDefinitionError, RelationshipInstanceError
|
from ormar.exceptions import ModelDefinitionError, RelationshipInstanceError
|
||||||
|
from ormar.fields.referential_actions import ReferentialAction
|
||||||
from ormar.fields.base import BaseField
|
from ormar.fields.base import BaseField
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma no cover
|
if TYPE_CHECKING: # pragma no cover
|
||||||
@ -159,6 +160,27 @@ def validate_not_allowed_fields(kwargs: Dict) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_referential_action(
|
||||||
|
action: Optional[Union[ReferentialAction, str]],
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Validation `onupdate` and `ondelete` action cast to a string value
|
||||||
|
|
||||||
|
:raises ModelDefinitionError: if action is a not valid name string value
|
||||||
|
:param action: referential action attribute or name string
|
||||||
|
:type action: Optional[Union[ReferentialAction, str]]
|
||||||
|
:rtype: Optional[str]
|
||||||
|
"""
|
||||||
|
|
||||||
|
if action is not None and not isinstance(action, ReferentialAction):
|
||||||
|
try:
|
||||||
|
action = ReferentialAction(action.upper())
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
raise ModelDefinitionError(f"{action} ReferentialAction not supported.")
|
||||||
|
|
||||||
|
return action.value if action is not None else None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ForeignKeyConstraint:
|
class ForeignKeyConstraint:
|
||||||
"""
|
"""
|
||||||
@ -190,8 +212,8 @@ def ForeignKey( # type: ignore # noqa CFQ002
|
|||||||
nullable: bool = True,
|
nullable: bool = True,
|
||||||
related_name: str = None,
|
related_name: str = None,
|
||||||
virtual: bool = False,
|
virtual: bool = False,
|
||||||
onupdate: str = None,
|
onupdate: Union[ReferentialAction, str] = None,
|
||||||
ondelete: str = None,
|
ondelete: Union[ReferentialAction, str] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> "T":
|
) -> "T":
|
||||||
"""
|
"""
|
||||||
@ -215,16 +237,19 @@ def ForeignKey( # type: ignore # noqa CFQ002
|
|||||||
:type virtual: bool
|
:type virtual: bool
|
||||||
:param onupdate: parameter passed to sqlalchemy.ForeignKey.
|
:param onupdate: parameter passed to sqlalchemy.ForeignKey.
|
||||||
How to treat child rows on update of parent (the one where FK is defined) model.
|
How to treat child rows on update of parent (the one where FK is defined) model.
|
||||||
:type onupdate: str
|
:type onupdate: Union[ReferentialAction, str]
|
||||||
:param ondelete: parameter passed to sqlalchemy.ForeignKey.
|
:param ondelete: parameter passed to sqlalchemy.ForeignKey.
|
||||||
How to treat child rows on delete of parent (the one where FK is defined) model.
|
How to treat child rows on delete of parent (the one where FK is defined) model.
|
||||||
:type ondelete: str
|
:type ondelete: Union[ReferentialAction, str]
|
||||||
:param kwargs: all other args to be populated by BaseField
|
:param kwargs: all other args to be populated by BaseField
|
||||||
:type kwargs: Any
|
:type kwargs: Any
|
||||||
:return: ormar ForeignKeyField with relation to selected model
|
:return: ormar ForeignKeyField with relation to selected model
|
||||||
:rtype: ForeignKeyField
|
:rtype: ForeignKeyField
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
onupdate = validate_referential_action(action=onupdate)
|
||||||
|
ondelete = validate_referential_action(action=ondelete)
|
||||||
|
|
||||||
owner = kwargs.pop("owner", None)
|
owner = kwargs.pop("owner", None)
|
||||||
self_reference = kwargs.pop("self_reference", False)
|
self_reference = kwargs.pop("self_reference", False)
|
||||||
|
|
||||||
|
|||||||
26
ormar/fields/referential_actions.py
Normal file
26
ormar/fields/referential_actions.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"""
|
||||||
|
Gathers all referential actions by ormar.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class ReferentialAction(Enum):
|
||||||
|
"""
|
||||||
|
Because the database management system(DBMS) enforces referential constraints,
|
||||||
|
it must ensure data integrity
|
||||||
|
if rows in a referenced table are to be deleted (or updated).
|
||||||
|
|
||||||
|
If dependent rows in referencing tables still exist,
|
||||||
|
those references have to be considered.
|
||||||
|
|
||||||
|
SQL specifies 5 different referential actions
|
||||||
|
that shall take place in such occurrences.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CASCADE: str = "CASCADE"
|
||||||
|
RESTRICT: str = "RESTRICT"
|
||||||
|
SET_NULL: str = "SET NULL"
|
||||||
|
SET_DEFAULT: str = "SET DEFAULT"
|
||||||
|
DO_NOTHING: str = "NO ACTION"
|
||||||
@ -5,6 +5,7 @@ import pytest
|
|||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
import ormar
|
import ormar
|
||||||
|
from ormar.fields.foreign_key import validate_referential_action
|
||||||
from tests.settings import DATABASE_URL
|
from tests.settings import DATABASE_URL
|
||||||
|
|
||||||
database = databases.Database(DATABASE_URL)
|
database = databases.Database(DATABASE_URL)
|
||||||
@ -33,6 +34,35 @@ class Album(ormar.Model):
|
|||||||
artist: Optional[Artist] = ormar.ForeignKey(Artist, ondelete="CASCADE")
|
artist: Optional[Artist] = ormar.ForeignKey(Artist, ondelete="CASCADE")
|
||||||
|
|
||||||
|
|
||||||
|
class A(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=64, nullalbe=False)
|
||||||
|
|
||||||
|
|
||||||
|
class B(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=64, nullalbe=False)
|
||||||
|
a: A = ormar.ForeignKey(to=A, ondelete=ormar.ReferentialAction.CASCADE)
|
||||||
|
|
||||||
|
|
||||||
|
class C(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=64, nullalbe=False)
|
||||||
|
b: B = ormar.ForeignKey(to=B, ondelete=ormar.ReferentialAction.CASCADE)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True, scope="module")
|
@pytest.fixture(autouse=True, scope="module")
|
||||||
def create_test_database():
|
def create_test_database():
|
||||||
metadata.drop_all(engine)
|
metadata.drop_all(engine)
|
||||||
@ -53,3 +83,28 @@ def test_simple_cascade():
|
|||||||
assert fks[0]["constrained_columns"][0] == "artist"
|
assert fks[0]["constrained_columns"][0] == "artist"
|
||||||
assert fks[0]["referred_columns"][0] == "id"
|
assert fks[0]["referred_columns"][0] == "id"
|
||||||
assert fks[0]["options"].get("ondelete") == "CASCADE"
|
assert fks[0]["options"].get("ondelete") == "CASCADE"
|
||||||
|
|
||||||
|
|
||||||
|
def test_validations_referential_action():
|
||||||
|
CASCADE = ormar.ReferentialAction.CASCADE.value
|
||||||
|
|
||||||
|
assert validate_referential_action(None) == None
|
||||||
|
assert validate_referential_action("cascade") == CASCADE
|
||||||
|
assert validate_referential_action(ormar.ReferentialAction.CASCADE) == CASCADE
|
||||||
|
|
||||||
|
with pytest.raises(ormar.ModelDefinitionError):
|
||||||
|
validate_referential_action("NOT VALID")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cascade_clear():
|
||||||
|
async with database:
|
||||||
|
async with database.transaction(force_rollback=True):
|
||||||
|
a = await A.objects.create(name="a")
|
||||||
|
b = await B.objects.create(name="b", a=a)
|
||||||
|
c = await C.objects.create(name="c", b=b)
|
||||||
|
|
||||||
|
await a.bs.clear(keep_reversed=False)
|
||||||
|
|
||||||
|
assert await B.objects.count() == 0
|
||||||
|
assert await C.objects.count() == 0
|
||||||
|
|||||||
Reference in New Issue
Block a user