Flatten data on Marshallow / SQLAlchemy Schema

大城市里の小女人 提交于 2019-12-11 09:39:53

问题


I'm having some issues with how I'm getting my data back from one of my endpoints - specifically, with Marshmallow and SQLAlchemy.

I have a many-to-many relationship between cocktails and ingredients, but I also have more data than just foreign keys on the relational table, ings_in_cocktail, such as ounces. When I GET /cocktails/, it returns something like this:

{
  "cocktails": [
    {
      "glass": "rocks",
      "ingredients": [
        {
          "ingredient": {
            "ing_type": "liquor",
            "id": 1,
            "name": "gin"
          },
          "ounces": 20
        }
      ],
      "finish": "stirred",
      "id": 1,
      "name": "gin and tonic"
    }
  ]
}

What I'd like to do is combine the spread the ounces property with the ingredient dict.

I want the data to look like the following:

{
  "cocktails": [
    {
      "glass": "rocks",
      "ingredients": [
        {
          "ing_type": "liquor",
          "id": 1,
          "name": "gin",
          "ounces": 20
        }
      ],
      "finish": "stirred",
      "id": 1,
      "name": "gin and tonic"
    }
  ]
}

After searching the web for hours, I can't find a way to do this easily with Marshmallow. Is there some easy way I'm missing?

Code

ingredients.py

from flask import Flask
from settings import db, ma

class Ingredient(db.Model):
    __tablename__ = 'ingredients'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False)
    ing_type = db.Column(db.String(20), nullable=False)

class IngredientSchema(ma.ModelSchema):
    class Meta:
        model = Ingredient

ings_in_cocktail.py

from flask import Flask
from settings import db, ma

from models.ingredients import Ingredient, IngredientSchema

class CocktailIngredient(db.Model):
    __tablename__ = 'ings_in_cocktail'
    ing_id = db.Column(db.Integer, db.ForeignKey('ingredients.id'), primary_key=True)
    cocktail_id = db.Column(db.Integer, db.ForeignKey('cocktails.id'), primary_key=True)
    ounces = db.Column(db.Integer, nullable=False)

    ingredient = db.relationship('Ingredient')

# Necessary for transforming sqlalchemy data into serialized JSON


class CocktailIngredientSchema(ma.ModelSchema):
    ingredient = ma.Nested(IngredientSchema, strict=True)

    class Meta:
    model = CocktailIngredient

cocktails.py

from flask import Flask
from settings import db, ma

from models.ing_in_cocktails import CocktailIngredient, CocktailIngredientSchema
from models.ingredients import Ingredient, IngredientSchema

class Cocktail(db.Model):
    __tablename__ = 'cocktails'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False)
    glass = db.Column(db.String(20), nullable=False)
    finish = db.Column(db.String(20), nullable=True)
    ingredients = db.relationship(
        'CocktailIngredient',
        # secondary='ings_in_cocktail',
        backref=db.backref('cocktails'),
        # primaryjoin=id == CocktailIngredient.cocktail_id
    )

# Necessary for transforming sqlalchemy data into serialized JSON
class CocktailSchema(ma.ModelSchema):
    # this is responsible for returning all the ingredient data on the cocktail
    ingredients = ma.Nested(CocktailIngredientSchema, many=True, strict=True)
    class Meta:
    model = Cocktail

回答1:


You can using a method field in IngredientSchema

https://marshmallow.readthedocs.io/en/stable/custom_fields.html#method-fields

Please check this to see how to use this field in documents.




回答2:


I ended up solving it like this:

class CocktailSchema(ma.ModelSchema):
    # this is responsible for returning all the ingredient data on the cocktail
    ingredients = ma.Nested(CocktailIngredientSchema, many=True, strict=True)
    ingredients = fields.Method('concat_ingredients_dicts')

    """
    at this point the ingredients field on the cocktail object looks something like this

    ingredients: [{
        ingredient: {
            name: 'white russian',
            glass: 'rocks',
            finish: 'stirred'
        },
        ounces: 2,
        action: 'muddle',
        step: 1
    }]

    what we want is to concat this data so "ingredients" just turns
    into an list of dicts
    """
    def concat_ingredients_dicts(self, obj):
        result_ingredients_list = []
        i = 0
        while i < len(list(obj.ingredients)):
            # create a dict from the fields that live in the relational table
            relational_fields_dict = {
                'ounces': obj.ingredients[i].ounces,
                'action': obj.ingredients[i].action,
                'step': obj.ingredients[i].step
            }

            # create a dict from the fields on each ingredient in the cocktail
            ingredients_dict = obj.ingredients[i].ingredient.__dict__
            ingredients_dict_extracted_values = {
                'name': ingredients_dict.get('name'),
                'type': ingredients_dict.get('ing_type'),
                'id': ingredients_dict.get('id')
            }

            # merge the two dicts together
            merged = dict()
            merged.update(ingredients_dict_extracted_values)
            merged.update(relational_fields_dict)

            # append this merged dict a result array
            result_ingredients_list.append(merged)
            i += 1
        # return the array of ingredients
        return result_ingredients_list

    class Meta:
      model = Cocktail



回答3:


Update: I put a better solution at the bottom.

You can achieve this by using Method or Function fields to allow a parent to get data from its child. Here is what you need to do:

  1. Leave CocktailSchema alone.
  2. Get rid of IngredientSchema entirely (unless you also need it for something else).
  3. Inside CocktailIngredientSchema, replace the Nested field with several Function fields that pull data right out of the inner "ingredient" object.

Schemas Before

class IngredientSchema(ma.ModelSchema):
    class Meta:
        model = Ingredient


class CocktailIngredientSchema(ma.ModelSchema):
    ingredient = ma.Nested(IngredientSchema)

    class Meta:
        model = CocktailIngredient


class CocktailSchema(ma.ModelSchema):
    ingredients = ma.Nested(CocktailIngredientSchema, many=True)

    class Meta:
        model = Cocktail

Schemas After

class CocktailIngredientSchema(ma.ModelSchema):
    ing_type = ma.Function(lambda obj: obj.ingredient.ing_type)
    id = ma.Function(lambda obj: obj.ingredient.id)
    name = ma.Function(lambda obj: obj.ingredient.name)

    class Meta:
        model = CocktailIngredient


class CocktailSchema(ma.ModelSchema):
    ingredients = ma.Nested(CocktailIngredientSchema, many=True)

    class Meta:
        model = Cocktail

A cleaner alternative for marshmallow version 3+ (requires Python 3)

  1. Leave CocktailSchema alone.
  2. Leave IngredientSchema alone (instead of getting rid of it as shown above).
  3. Inside CocktailIngredientSchema, replace the Nested field with several Pluck fields that pull data right out of the inner schema.

Schemas After

class IngredientSchema(ma.ModelSchema):
    class Meta:
        model = Ingredient


class CocktailIngredientSchema(ma.ModelSchema):
    ing_type = ma.Pluck(IngredientSchema, 'ing_type')
    id = ma.Pluck(IngredientSchema, 'id')
    name = ma.Pluck(IngredientSchema, 'name')

    class Meta:
        model = CocktailIngredient


class CocktailSchema(ma.ModelSchema):
    ingredients = ma.Nested(CocktailIngredientSchema, many=True)

    class Meta:
        model = Cocktail
  • documentation for fields.Function() and fields.Method()
  • documentation for fields.Pluck()


来源:https://stackoverflow.com/questions/54174938/flatten-data-on-marshallow-sqlalchemy-schema

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!