Как создать вложенный JSON из запроса к базе данных с помощью объединений?Использование Python / SQLAlchemy - PullRequest
0 голосов
/ 05 апреля 2019

У меня есть конкретный вариант использования, но мой вопрос касается наилучшего способа сделать это в целом.

У меня есть три стола

Order - первичный ключ order_id

OrderLine - Таблица связывания с идентификатором_оказания, идентификатором_оценки и количеством. В заказе 1 или более строк заказа

Продукт - первичный ключ product_id, в каждой строке заказа есть один продукт

В sqlachemy / python, как мне создать вложенный JSON в соответствии с:

{
    "orders": [
        {
            "order_id": 1
            "some_order_level_detail": "Kansas"
            "order_lines": [
                {
                    "product_id": 1,
                    "product_name": "Clawhammer",
                    "quantity": 5
                },
                ...
            ]
        },
        ...
    ]
}

Потенциальные идеи

Забей на выполнение последовательных запросов

Первая идея, от которой я хочу избавиться, если это возможно, - это использование списка и метод грубой силы.

def get_json():
    answer = {
        "orders": [
            {
                "order_id": o.order_id,
                "some_order_level_detail": o.some_order_level_detail,
                "order_lines": [
                    {
                        "product_id": 1,
                        "product_name": Product.query.get(o_line.product_id).product_name,
                        "quantity": 5
                    }
                    for o_line in OrderLine.query.filter(order_id=o.order_id).all()
                ]
            }
            for o in Order.query.all()
        ]
    }

Сложно поддерживать смешивание запросов с json. В идеале я хотел бы сначала сделать запрос ...

Сначала объедините результаты, а потом как-нибудь манипулируйте

Вторая идея - сделать запрос на объединение, чтобы объединить три таблицы, отображающие в каждой строке в OrderLine информацию о заказе и продукте.

Мой вопрос к pythonista: есть ли хороший способ преобразовать это во вложенный json.

Другой способ?

Это действительно кажется таким распространенным требованием. Мне действительно интересно, существует ли метод книг для такого рода вещей? Существует ли версия SQLAchemy this

Ответы [ 2 ]

1 голос
/ 06 апреля 2019

Посмотрите на marshmallow-sqlalchemy , так как он делает именно то, что вы ищете.

Я настоятельно рекомендую не включать вашу сериализацию непосредственно в вашу модель, так как в конечном итоге у вас будет двасервисы, запрашивающие те же данные, но сериализованные другим способом (включая, например, меньшее или большее количество вложенных отношений для повышения производительности), и вы либо получите (1) множество ошибок, которые пропустит ваш набор тестов, если только вы не 'проверяя буквально каждое поле или (2) больше сериализованных данных, чем вам нужно, и вы столкнетесь с проблемами производительности по мере масштабирования вашего приложения.

С помощью marshmallow-sqlalchemy вам необходимо определитьсхема для каждой модели, которую вы хотите сериализовать.Да, это немного лишний шаблон, но, поверьте мне, в конечном итоге вы будете намного счастливее.

Мы создаем приложения, используя такие параметры как flask-sqlalchemy и marshmallow-sqlalchemy (также настоятельно рекомендуем factory_boy , чтобы вы могли смоделировать свой сервис и написать модульные тесты вместо интеграционных тестов, которые должны касаться базы данных):

# models

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    children = relationship("Child", back_populates="parent")

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))
    parent = relationship('Parent', back_populates='children',
                          foreign_keys=[parent_id])

# schemas. Don't put these in your models. Avoid tight coupling here

from marshmallow_sqlalchemy import ModelSchema
import marshmallow as ma


class ParentSchema(ModelSchema):
    children = ma.fields.Nested(
        'myapp.schemas.child.Child', exclude=('parent',), many=True)
    class Meta(ModelSchema.Meta):
        model = Parent
        strict = True
        dump_only = ('id',)


class ChildSchema(ModelSchema):
    parent = ma.fields.Nested(
        'myapp.schemas.parent.Parent', exclude=('children',))
    class Meta(ModelSchema.Meta):
        model = Child
        strict = True
        dump_only = ('id',)

# services

class ParentService:
    '''
    This service intended for use exclusively by /api/parent
    '''
    def __init__(self, params, _session=None):
        # your unit tests can pass in _session=MagicMock()
        self.session = _session or db.session
        self.params = params

    def _parents(self) -> typing.List[Parent]:
        return self.session.query(Parent).options(
            joinedload(Parent.children)
        ).all()

    def get(self):
        schema = ParentSchema(only=(
            # highly recommend specifying every field explicitly
            # rather than implicit
            'id',
            'children.id',
        ))
        return schema.dump(self._parents()).data

# views

@app.route('/api/parent')
def get_parents():
    service = ParentService(params=request.get_json())
    return jsonify(data=service.get())


# test factories
class ModelFactory(SQLAlchemyModelFactory):
    class Meta:
        abstract = True
        sqlalchemy_session = db.session

class ParentFactory(ModelFactory):
    id = factory.Sequence(lambda n: n + 1)
    children = factory.SubFactory('tests.factory.children.ChildFactory')

class ChildFactory(ModelFactory):
    id = factory.Sequence(lambda n: n + 1)
    parent = factory.SubFactory('tests.factory.parent.ParentFactory')

# tests
from unittest.mock import MagicMock, patch

def test_can_serialize_parents():
    parents = ParentFactory.build_batch(4)
    session = MagicMock()
    service = ParentService(params={}, _session=session)
    assert service.session is session
    with patch.object(service, '_parents') as _parents:
        _parents.return_value = parents
        assert service.get()[0]['id'] == parents[0].id
        assert service.get()[1]['id'] == parents[1].id
        assert service.get()[2]['id'] == parents[2].id
        assert service.get()[3]['id'] == parents[3].id
0 голосов
/ 05 апреля 2019

Я бы добавил .json() метод к каждой модели, чтобы они вызывали друг друга. По сути, это ваше «взломанное» решение, но немного более читаемое / поддерживаемое. Ваша Order модель может иметь:

def json(self):
    return {
        "id": self.id,
        "order_lines": [line.json() for line in self.order_lines]
    }

Ваша OrderLine модель может иметь:

def json(self):
    return {
        "product_id": self.product_id,
        "product_name": self.product.name,
        "quantity": self.quantity
    }

Ваш ресурс на верхнем уровне (где вы делаете запрос на заказы) может затем сделать:

...
orders = Order.query.all()
return {"orders": [order.json() for order in orders]}
...

Вот как я обычно структурирую это требование JSON.

...