Skip to content

Commit

Permalink
Merge pull request #4 from collerek/many_to_many
Browse files Browse the repository at this point in the history
Many to many
  • Loading branch information
collerek authored Sep 14, 2020
2 parents 58c3627 + 5a8fd74 commit 1075e84
Show file tree
Hide file tree
Showing 19 changed files with 880 additions and 254 deletions.
Binary file modified .coverage
Binary file not shown.
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,86 @@ tracks = await Track.objects.limit(1).all()
assert len(tracks) == 1
```

Since version >=0.3 Ormar supports also many to many relationships
```python
import databases
import ormar
import sqlalchemy

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()

class Author(ormar.Model):
class Meta:
tablename = "authors"
database = database
metadata = metadata

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


class Category(ormar.Model):
class Meta:
tablename = "categories"
database = database
metadata = metadata

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


class PostCategory(ormar.Model):
class Meta:
tablename = "posts_categories"
database = database
metadata = metadata


class Post(ormar.Model):
class Meta:
tablename = "posts"
database = database
metadata = metadata

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

guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum")
post = await Post.objects.create(title="Hello, M2M", author=guido)
news = await Category.objects.create(name="News")

# Add a category to a post.
await post.categories.add(news)
# or from the other end:
await news.posts.add(post)

# Creating related object from instance:
await post.categories.create(name="Tips")
assert len(await post.categories.all()) == 2

# Many to many relation exposes a list of related models
# and an API of the Queryset:
assert news == await post.categories.get(name="News")

# with all Queryset methods - filtering, selecting related, counting etc.
await news.posts.filter(title__contains="M2M").all()
await Category.objects.filter(posts__author=guido).get()

# related models of many to many relation can be prefetched
news_posts = await news.posts.select_related("author").all()
assert news_posts[0].author == guido

# Removal of the relationship by one
await news.posts.remove(post)
# or all at once
await news.posts.clear()

```

## Data types

The following keyword arguments are supported on all field types.
Expand Down
4 changes: 3 additions & 1 deletion ormar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
ForeignKey,
Integer,
JSON,
ManyToMany,
String,
Text,
Time,
)
from ormar.models import Model

__version__ = "0.2.2"
__version__ = "0.3.0"
__all__ = [
"Integer",
"BigInteger",
Expand All @@ -28,6 +29,7 @@
"Date",
"Decimal",
"Float",
"ManyToMany",
"Model",
"ModelDefinitionError",
"ModelNotSet",
Expand Down
2 changes: 2 additions & 0 deletions ormar/fields/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from ormar.fields.base import BaseField
from ormar.fields.foreign_key import ForeignKey
from ormar.fields.many_to_many import ManyToMany
from ormar.fields.model_fields import (
BigInteger,
Boolean,
Expand Down Expand Up @@ -27,5 +28,6 @@
"Float",
"Time",
"ForeignKey",
"ManyToMany",
"BaseField",
]
4 changes: 2 additions & 2 deletions ormar/fields/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class BaseField:
index: bool
unique: bool
pydantic_only: bool
virtual: bool = False

default: Any
server_default: Any
Expand All @@ -34,8 +35,7 @@ def default_value(cls) -> Optional[Field]:
default = cls.default if cls.default is not None else cls.server_default
if callable(default):
return Field(default_factory=default)
else:
return Field(default=default)
return Field(default=default)
return None

@classmethod
Expand Down
12 changes: 9 additions & 3 deletions ormar/fields/foreign_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,28 @@ def create_dummy_instance(fk: Type["Model"], pk: Any = None) -> "Model":
return fk(**init_dict)


def ForeignKey(
def ForeignKey( # noqa CFQ002
to: Type["Model"],
*,
name: str = None,
unique: bool = False,
nullable: bool = True,
related_name: str = None,
virtual: bool = False,
onupdate: str = None,
ondelete: str = None,
) -> Type[object]:
fk_string = to.Meta.tablename + "." + to.Meta.pkname
to_field = to.__fields__[to.Meta.pkname]
namespace = dict(
to=to,
name=name,
nullable=nullable,
constraints=[sqlalchemy.schema.ForeignKey(fk_string)],
constraints=[
sqlalchemy.schema.ForeignKey(
fk_string, ondelete=ondelete, onupdate=onupdate
)
],
unique=unique,
column_type=to_field.type_.column_type,
related_name=related_name,
Expand Down Expand Up @@ -117,7 +123,7 @@ def expand_relationship(
cls, value: Any, child: "Model", to_register: bool = True
) -> Optional[Union["Model", List["Model"]]]:
if value is None:
return None
return None if not cls.virtual else []

constructors = {
f"{cls.to.__name__}": cls._register_existing_model,
Expand Down
40 changes: 40 additions & 0 deletions ormar/fields/many_to_many.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import TYPE_CHECKING, Type

from ormar.fields import BaseField
from ormar.fields.foreign_key import ForeignKeyField

if TYPE_CHECKING: # pragma no cover
from ormar.models import Model


def ManyToMany(
to: Type["Model"],
through: Type["Model"],
*,
name: str = None,
unique: bool = False,
related_name: str = None,
virtual: bool = False,
) -> Type[object]:
to_field = to.__fields__[to.Meta.pkname]
namespace = dict(
to=to,
through=through,
name=name,
nullable=True,
unique=unique,
column_type=to_field.type_.column_type,
related_name=related_name,
virtual=virtual,
primary_key=False,
index=False,
pydantic_only=False,
default=None,
server_default=None,
)

return type("ManyToMany", (ManyToManyField, BaseField), namespace)


class ManyToManyField(ForeignKeyField):
through: Type["Model"]
Loading

0 comments on commit 1075e84

Please sign in to comment.