allow customization of through model relation names
This commit is contained in:
@ -162,6 +162,71 @@ The default naming convention is:
|
||||
* for table name it similar but with underscore in between and s in the end of class
|
||||
lowercase name, in example above would be `posts_categorys`
|
||||
|
||||
### Customizing Through relation names
|
||||
|
||||
By default `Through` model relation names default to related model name in lowercase.
|
||||
|
||||
So in example like this:
|
||||
```python
|
||||
... # course declaration ommited
|
||||
class Student(ormar.Model):
|
||||
class Meta:
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100)
|
||||
courses = ormar.ManyToMany(Course)
|
||||
|
||||
# will produce default Through model like follows (example simplified)
|
||||
class StudentCourse(ormar.Model):
|
||||
class Meta:
|
||||
database = database
|
||||
metadata = metadata
|
||||
tablename = "students_courses"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
student = ormar.ForeignKey(Student) # default name
|
||||
course = ormar.ForeignKey(Course) # default name
|
||||
```
|
||||
|
||||
To customize the names of fields/relation in Through model now you can use new parameters to `ManyToMany`:
|
||||
|
||||
* `through_relation_name` - name of the field leading to the model in which `ManyToMany` is declared
|
||||
* `through_reverse_relation_name` - name of the field leading to the model to which `ManyToMany` leads to
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
... # course declaration ommited
|
||||
class Student(ormar.Model):
|
||||
class Meta:
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100)
|
||||
courses = ormar.ManyToMany(Course,
|
||||
through_relation_name="student_id",
|
||||
through_reverse_relation_name="course_id")
|
||||
|
||||
# will produce Through model like follows (example simplified)
|
||||
class StudentCourse(ormar.Model):
|
||||
class Meta:
|
||||
database = database
|
||||
metadata = metadata
|
||||
tablename = "students_courses"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
student_id = ormar.ForeignKey(Student) # set by through_relation_name
|
||||
course_id = ormar.ForeignKey(Course) # set by through_reverse_relation_name
|
||||
```
|
||||
|
||||
!!!note
|
||||
Note that explicitly declaring relations in Through model is forbidden, so even if you
|
||||
provide your own custom Through model you cannot change the names there and you need to use
|
||||
same `through_relation_name` and `through_reverse_relation_name` parameters.
|
||||
|
||||
## Through Fields
|
||||
|
||||
The through field is auto added to the reverse side of the relation.
|
||||
|
||||
@ -24,6 +24,60 @@
|
||||
in those cases you don't have to split save into two calls (`save()` and `save_related()`)
|
||||
* it supports also `ManyToMany` relations
|
||||
* it supports also optional `Through` model values for m2m relations
|
||||
* Add possibility to customize `Through` model relation field names.
|
||||
* By default `Through` model relation names default to related model name in lowercase.
|
||||
So in example like this:
|
||||
```python
|
||||
... # course declaration ommited
|
||||
class Student(ormar.Model):
|
||||
class Meta:
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100)
|
||||
courses = ormar.ManyToMany(Course)
|
||||
|
||||
# will produce default Through model like follows (example simplified)
|
||||
class StudentCourse(ormar.Model):
|
||||
class Meta:
|
||||
database = database
|
||||
metadata = metadata
|
||||
tablename = "students_courses"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
student = ormar.ForeignKey(Student) # default name
|
||||
course = ormar.ForeignKey(Course) # default name
|
||||
```
|
||||
* To customize the names of fields/relation in Through model now you can use new parameters to `ManyToMany`:
|
||||
* `through_relation_name` - name of the field leading to the model in which `ManyToMany` is declared
|
||||
* `through_reverse_relation_name` - name of the field leading to the model to which `ManyToMany` leads to
|
||||
|
||||
Example:
|
||||
```python
|
||||
... # course declaration ommited
|
||||
class Student(ormar.Model):
|
||||
class Meta:
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100)
|
||||
courses = ormar.ManyToMany(Course,
|
||||
through_relation_name="student_id",
|
||||
through_reverse_relation_name="course_id")
|
||||
|
||||
# will produce default Through model like follows (example simplified)
|
||||
class StudentCourse(ormar.Model):
|
||||
class Meta:
|
||||
database = database
|
||||
metadata = metadata
|
||||
tablename = "students_courses"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
student_id = ormar.ForeignKey(Student) # set by through_relation_name
|
||||
course_id = ormar.ForeignKey(Course) # set by through_reverse_relation_name
|
||||
```
|
||||
|
||||
## 🐛 Fixes
|
||||
|
||||
|
||||
@ -53,6 +53,12 @@ class BaseField(FieldInfo):
|
||||
"is_relation", None
|
||||
) # ForeignKeyField + subclasses
|
||||
self.is_through: bool = kwargs.pop("is_through", False) # ThroughFields
|
||||
|
||||
self.through_relation_name = kwargs.pop("through_relation_name", None)
|
||||
self.through_reverse_relation_name = kwargs.pop(
|
||||
"through_reverse_relation_name", None
|
||||
)
|
||||
|
||||
self.skip_reverse: bool = kwargs.pop("skip_reverse", False)
|
||||
self.skip_field: bool = kwargs.pop("skip_field", False)
|
||||
|
||||
|
||||
@ -318,29 +318,23 @@ class ForeignKeyField(BaseField):
|
||||
"""
|
||||
return self.related_name or self.owner.get_name() + "s"
|
||||
|
||||
def default_target_field_name(self, reverse: bool = False) -> str:
|
||||
def default_target_field_name(self) -> str:
|
||||
"""
|
||||
Returns default target model name on through model.
|
||||
:param reverse: flag to grab name without accessing related field
|
||||
:type reverse: bool
|
||||
:return: name of the field
|
||||
:rtype: str
|
||||
"""
|
||||
self_rel_prefix = "from_" if not reverse else "to_"
|
||||
prefix = self_rel_prefix if self.self_reference else ""
|
||||
return f"{prefix}{self.to.get_name()}"
|
||||
prefix = "from_" if self.self_reference else ""
|
||||
return self.through_reverse_relation_name or f"{prefix}{self.to.get_name()}"
|
||||
|
||||
def default_source_field_name(self, reverse: bool = False) -> str:
|
||||
def default_source_field_name(self) -> str:
|
||||
"""
|
||||
Returns default target model name on through model.
|
||||
:param reverse: flag to grab name without accessing related field
|
||||
:type reverse: bool
|
||||
:return: name of the field
|
||||
:rtype: str
|
||||
"""
|
||||
self_rel_prefix = "to_" if not reverse else "from_"
|
||||
prefix = self_rel_prefix if self.self_reference else ""
|
||||
return f"{prefix}{self.owner.get_name()}"
|
||||
prefix = "to_" if self.self_reference else ""
|
||||
return self.through_relation_name or f"{prefix}{self.owner.get_name()}"
|
||||
|
||||
def evaluate_forward_ref(self, globalns: Any, localns: Any) -> None:
|
||||
"""
|
||||
|
||||
@ -122,6 +122,9 @@ def ManyToMany(
|
||||
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))
|
||||
|
||||
@ -158,6 +161,8 @@ def ManyToMany(
|
||||
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), {})
|
||||
|
||||
@ -112,6 +112,8 @@ def register_reverse_model_fields(model_field: "ForeignKeyField") -> None:
|
||||
self_reference_primary=model_field.self_reference_primary,
|
||||
orders_by=model_field.related_orders_by,
|
||||
skip_field=model_field.skip_reverse,
|
||||
through_relation_name=model_field.through_reverse_relation_name,
|
||||
through_reverse_relation_name=model_field.through_relation_name,
|
||||
)
|
||||
# register foreign keys on through model
|
||||
model_field = cast("ManyToManyField", model_field)
|
||||
|
||||
@ -0,0 +1,84 @@
|
||||
import databases
|
||||
import pytest
|
||||
import sqlalchemy
|
||||
|
||||
import ormar
|
||||
from tests.settings import DATABASE_URL
|
||||
|
||||
metadata = sqlalchemy.MetaData()
|
||||
database = databases.Database(DATABASE_URL, force_rollback=True)
|
||||
|
||||
|
||||
class Course(ormar.Model):
|
||||
class Meta:
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
course_name: str = ormar.String(max_length=100)
|
||||
|
||||
|
||||
class Student(ormar.Model):
|
||||
class Meta:
|
||||
database = database
|
||||
metadata = metadata
|
||||
|
||||
id: int = ormar.Integer(primary_key=True)
|
||||
name: str = ormar.String(max_length=100)
|
||||
courses = ormar.ManyToMany(
|
||||
Course,
|
||||
through_relation_name="student_id",
|
||||
through_reverse_relation_name="course_id",
|
||||
)
|
||||
|
||||
|
||||
# create db and tables
|
||||
@pytest.fixture(autouse=True, scope="module")
|
||||
def create_test_database():
|
||||
engine = sqlalchemy.create_engine(DATABASE_URL)
|
||||
metadata.create_all(engine)
|
||||
yield
|
||||
metadata.drop_all(engine)
|
||||
|
||||
|
||||
def test_tables_columns():
|
||||
through_meta = Student.Meta.model_fields["courses"].through.Meta
|
||||
assert "course_id" in through_meta.table.c
|
||||
assert "student_id" in through_meta.table.c
|
||||
assert "course_id" in through_meta.model_fields
|
||||
assert "student_id" in through_meta.model_fields
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_working_with_changed_through_names():
|
||||
async with database:
|
||||
async with database.transaction(force_rollback=True):
|
||||
to_save = {
|
||||
"course_name": "basic1",
|
||||
"students": [{"name": "Jack"}, {"name": "Abi"}],
|
||||
}
|
||||
await Course(**to_save).save_related(follow=True, save_all=True)
|
||||
course_check = await Course.objects.select_related("students").get()
|
||||
|
||||
assert course_check.course_name == "basic1"
|
||||
assert course_check.students[0].name == "Jack"
|
||||
assert course_check.students[1].name == "Abi"
|
||||
|
||||
students = await course_check.students.all()
|
||||
assert len(students) == 2
|
||||
|
||||
student = await course_check.students.get(name="Jack")
|
||||
assert student.name == "Jack"
|
||||
|
||||
students = await Student.objects.select_related("courses").all(
|
||||
courses__course_name="basic1"
|
||||
)
|
||||
assert len(students) == 2
|
||||
|
||||
course_check = (
|
||||
await Course.objects.select_related("students")
|
||||
.order_by("students__name")
|
||||
.get()
|
||||
)
|
||||
assert course_check.students[0].name == "Abi"
|
||||
assert course_check.students[1].name == "Jack"
|
||||
Reference in New Issue
Block a user