Сериализация отношений "многие ко многим" с Пиви и Зефиром
У меня есть база данных PostgreSQL с отношением "многие ко многим пользователям к тегам" со следующими таблицами:
- social_user: информация о пользователе
- тег: информация тега
- user_tag: отношение " многие ко многим" между social_user и тегом
Я пытаюсь создать простой API для доступа к данным в этой базе данных, используя Flask, Peewee и Marshmallow. Пока что мы можем игнорировать Flask, но я пытаюсь создать схему для social_user, которая позволит мне добавить запрос, который возвращает пользователя или пользователей с их соответствующими тегами. Я ищу ответ, который выглядит примерно так:
{ "id": "[ID]", "handle": "[HANDLE]", "local_id": "[LOCAL_ID]", "platform_slug": "[PLATFORM_SLUG]", "tags": [ { "id": "[ID]", "title": "[TITLE]", "tag_type": "[TAG_TYPE]" }, { "id": "[ID]", "title": "[TITLE]", "tag_type": "[TAG_TYPE]" } ] }
Мне удалось сделать это, включив второй запрос, который извлекает теги из схемы social_user в обернутую функцию @post_dump, однако это похоже на хак, а также кажется, что это будет медленно с большим количеством пользователей (ОБНОВЛЕНИЕ: это ОЧЕНЬ медленно, я проверил это на 369 пользователях). Я думаю, что я могу что-то сделать с зефиром fields.Nested
тип поля. Есть ли лучший способ сериализации этих отношений только с одним запросом Peewee? Мой код ниже:
# just so you are aware of my namespaces
import marshmallow as marsh
import peewee as pw
Peewee Models
db = postgres_ext.PostgresqlExtDatabase(
register_hstore = False,
**json.load(open('postgres.json'))
)
class Base_Model(pw.Model):
class Meta:
database = db
class Tag(Base_Model):
title = pw.CharField()
tag_type = pw.CharField(db_column = 'type')
class Meta:
db_table = 'tag'
class Social_User(Base_Model):
handle = pw.CharField(null = True)
local_id = pw.CharField()
platform_slug = pw.CharField()
class Meta:
db_table = 'social_user'
class User_Tag(Base_Model):
social_user_id = pw.ForeignKeyField(Social_User)
tag_id = pw.ForeignKeyField(Tag)
class Meta:
primary_key = pw.CompositeKey('social_user_id', 'tag_id')
db_table = 'user_tag'
Схемы зефира
class Tag_Schema(marsh.Schema):
id = marsh.fields.Int(dump_only = True)
title = marsh.fields.Str(required = True)
tag_type = marsh.fields.Str(required = True, default = 'descriptive')
class Social_User_Schema(marsh.Schema):
id = marsh.fields.Int(dump_only = True)
local_id = marsh.fields.Str(required = True)
handle = marsh.fields.Str()
platform_slug = marsh.fields.Str(required = True)
tags = marsh.fields.Nested(Tag_Schema, many = True, dump_only = True)
def _get_tags(self, user_id):
query = Tag.select().join(User_Tag).where(User_Tag.social_user_id == user_id)
tags, errors = tags_schema.dump(query)
return tags
@marsh.post_dump(pass_many = True)
def post_dump(self, data, many):
if many:
for datum in data:
datum['tags'] = self._get_tags(datum['id']) if datum['id'] else []
else:
data['tags'] = self._get_tags(data['id'])
return data
user_schema = Social_User_Schema()
users_schema = Social_User_Schema(many = True)
tags_schema = Tag_Schema(many = True)
Вот несколько тестов для демонстрации функциональности:
db.connect()
query = Social_User.get(Social_User.id == 825)
result, errors = user_schema.dump(query)
db.close()
pprint(result)
{'handle': 'test', 'id': 825, 'local_id': 'test', 'platform_slug': 'tw', 'tags': [{'id': 20, 'tag_type': 'descriptive', 'title': 'this'}, {'id': 21, 'tag_type': 'descriptive', 'title': 'that'}]}
db.connect()
query = Social_User.select().where(Social_User.platform_slug == 'tw')
result, errors = users_schema.dump(query)
db.close()
pprint(result)
[{'handle': 'test', 'id': 825, 'local_id': 'test', 'platform_slug': 'tw', 'tags': [{'id': 20, 'tag_type': 'descriptive', 'title': 'this'}, {'id': 21, 'tag_type': 'descriptive', 'title': 'that'}]}, {'handle': 'test2', 'id': 826, 'local_id': 'test2', 'platform_slug': 'tw', 'tags': []}]
1 ответ
Похоже, это можно сделать с помощью ManyToMany
поле в моделях Peewee и вручную установить through_model
, ManyToMany
поле позволяет вам добавить в ваши модели поле, связывающее две таблицы друг с другом, обычно оно автоматически создает таблицу отношений (through_model
), но вы можете установить его вручную.
Я работаю с версией 3.0 Peewee, но я уверен, что многие люди работают с текущей стабильной версией, поэтому я включу обе версии. Мы будем использовать DeferredThroughModel
объект и ManyToMany
в Peewee 2.x они находятся в "playhouse" в 3.x и являются частью основного релиза Peewee. Мы также удалим @post_dump
обернутая функция:
Peewee Models
# Peewee 2.x
# from playhouse import fields
# User_Tag_Proxy = fields.DeferredThroughModel()
# Peewee 3.x
User_Tag_Proxy = pw.DeferredThroughModel()
class Tag(Base_Model):
title = pw.CharField()
tag_type = pw.CharField(db_column = 'type')
class Meta:
db_table = 'tag'
class Social_User(Base_Model):
handle = pw.CharField(null = True)
local_id = pw.CharField()
platform_slug = pw.CharField()
# Peewee 2.x
# tags = fields.ManyToManyField(Tag, related_name = 'users', through_model = User_Tag_Proxy)
# Peewee 3.x
tags = pw.ManyToManyField(Tag, backref = 'users', through_model = User_Tag_Proxy)
class Meta:
db_table = 'social_user'
class User_Tag(Base_Model):
social_user = pw.ForeignKeyField(Social_User, db_column = 'social_user_id')
tag = pw.ForeignKeyField(Tag, db_column = 'tag_id')
class Meta:
primary_key = pw.CompositeKey('social_user', 'tag')
db_table = 'user_tag'
User_Tag_Proxy.set_model(User_Tag)
Схемы зефира
class Social_User_Schema(marsh.Schema):
id = marsh.fields.Int(dump_only = True)
local_id = marsh.fields.Str(required = True)
handle = marsh.fields.Str()
platform_slug = marsh.fields.Str(required = True)
tags = marsh.fields.Nested(Tag_Schema, many = True, dump_only = True)
user_schema = Social_User_Schema()
users_schema = Social_User_Schema(many = True)
На практике это работает точно так же, как использование @post_dump
обернутая функция. К сожалению, хотя это и кажется "правильным" способом решения этой проблемы, на самом деле он немного медленнее.
--ОБНОВИТЬ--
Мне удалось сделать то же самое за 1/100 времени. Это что-то вроде взлома и может использовать некоторую очистку, но это работает! Вместо того, чтобы вносить изменения в модели, я изменил способ сбора и обработки данных, прежде чем передать их в схему для сериализации.
Peewee Models
class Tag(Base_Model):
title = pw.CharField()
tag_type = pw.CharField(db_column = 'type')
class Meta:
db_table = 'tag'
class Social_User(Base_Model):
handle = pw.CharField(null = True)
local_id = pw.CharField()
platform_slug = pw.CharField()
class Meta:
db_table = 'social_user'
class User_Tag(Base_Model):
social_user = pw.ForeignKeyField(Social_User, db_column = 'social_user_id')
tag = pw.ForeignKeyField(Tag, db_column = 'tag_id')
class Meta:
primary_key = pw.CompositeKey('social_user', 'tag')
db_table = 'user_tag'
Схема зефира
class Social_User_Schema(marsh.Schema):
id = marsh.fields.Int(dump_only = True)
local_id = marsh.fields.Str(required = True)
handle = marsh.fields.Str()
platform_slug = marsh.fields.Str(required = True)
tags = marsh.fields.Nested(Tag_Schema, many = True, dump_only = True)
user_schema = Social_User_Schema()
users_schema = Social_User_Schema(many = True)
запрос
Для нового запроса мы будем присоединяться (LEFT_OUTER
) три таблицы (Social_User, Tag и User_Tag) с Social_User в качестве нашего источника правды. Мы хотим убедиться, что мы получаем каждого пользователя, есть ли у него теги или нет. Это будет возвращать пользователей несколько раз в зависимости от количества тегов, которые у них есть, поэтому нам нужно уменьшить это, перебирая каждый из них и используя словарь для хранения объектов. В каждом из этих новых Social_User
объекты добавят tags
список, к которому мы добавим Tag
объекты.
db.connect()
query = (Social_User.select(User_Tag, Social_User, Tag)
.join(User_Tag, pw.JOIN.LEFT_OUTER)
.join(Tag, pw.JOIN.LEFT_OUTER)
.order_by(Social_User.id))
users = {}
last = None
for result in query:
user_id = result.id
if (user_id not in users):
# creates a new Social_User object matching the user data
users[user_id] = Social_User(**result.__data__)
users[user_id].tags = []
try:
# extracts the associated tag
users[user_id].tags.append(result.user_tag.tag)
except AttributeError:
pass
result, errors = users_schema.dump(users.values())
db.close()
pprint(result)