Сериализация отношений "многие ко многим" с Пиви и Зефиром

У меня есть база данных 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)
Другие вопросы по тегам