【问题标题】:Serializing a Many to Many Relationship w/ Peewee and Marshmallow用 Peewee 和 Marshmallow 序列化多对多关系
【发布时间】:2018-01-25 22:01:26
【问题描述】:

我有一个带有多对多用户的 PostgreSQL 数据库,以标记与下表的关系:

  • social_user:用户信息
  • 标签:标签信息
  • user_tag: social_usertag 之间的多对多关系

我正在尝试使用 Flask、Peewee 和 Marshmallow 构建一个简单的 API 来访问该数据库中的数据。我们现在可以忽略 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]"
        }
    ]
}

我已经设法做到这一点,方法是在 @post_dump 包装函数中包含第二个查询,该查询将 social_user 架构中的标签提取出来,但是,这感觉像是一个 hack,而且看起来它会对大量用户来说很慢(更新:这很慢,我在 369 个用户上测试过)。我想我可以用 Marshmallow 的fields.Nested field type 做点什么。有没有更好的方法可以只用一个 Peewee 查询来序列化这种关系?我的代码如下:

# just so you are aware of my namespaces
import marshmallow as marsh
import peewee as pw

Peewee 模型

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': []}]

【问题讨论】:

    标签: python postgresql peewee marshmallow


    【解决方案1】:

    看起来这可以使用 Peewee 模型中的 ManyToMany field 并手动设置 through_model 来完成。 ManyToMany 字段允许您向模型中添加一个字段,将两个表相互关联,通常它会自动创建关系表 (through_model),但您可以手动设置。

    我正在使用3.0 alpha of Peewee,但我相信很多人都在使用当前的稳定版本,所以我将包括这两个版本。我们将使用 DeferredThroughModel 对象和 ManyToMany 字段,在 Peewee 2.x 中,它们位于 3.x 中的“游戏室”中,它们是 Peewee 主要版本的一部分。我们还将删除 @post_dump 包装函数:

    Peewee 模型

    # 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 模型

    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_UserTagUser_TagSocial_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)
    

    【讨论】:

    • 干得好!这真的很有帮助。我使用了这个库,而不是从头开始创建模式。 github.com/klen/marshmallow-peewee。我希望它对某人有用。谢谢
    猜你喜欢
    • 1970-01-01
    • 2020-02-11
    • 2013-11-20
    • 1970-01-01
    • 1970-01-01
    • 2016-08-09
    • 1970-01-01
    • 2019-09-06
    • 1970-01-01
    相关资源
    最近更新 更多