Motivation

Webanwendungen empfangen bzw. senden Daten meist über HTTP Requests. Die Daten werden dabei in einer Beschreibungssprache wie JSON oder XML kodiert, und anschließend als Byte-Array im HTTP Body versendet. Die Umwandlung von Objekten einer Programmiersprache in eine Byte-Repräsentation wird Serialisierung bezeichnet. Die Umkehrfunktion wandelt eine Byte-Repräsentation in ein programmiersprachen-spezifisches Objekt um und wird Deserialisierung genannt.

Der folgende Artikel soll zeigen, wie Datenbank-Entitäten, welche über SQLAlchemy als Python-Objekte repräsentiert werden (de)serialisiert werden können. Es wird demonstriert, dass es nicht aufwändig ist, eigene Serialisierungsklassen zu schreiben. Das Vorgehen wird dabei an einer simplen Flask-Applikation erklärt.

(De)serialisierung in Django

Die meisten Webframeworks besitzen für das (De)serialisieren eigene Bibliotheken. Django besitzt zum Beispiel eigene Serialisierungsklassen, welche über das Django Rest Framwork eingebunden werden. Sehr komfortabel ist dabei die Klasse ModelSerializer. Eigene Serialisierungsklassen, welche von dieser Klasse erben, erlauben das (De)serialisieren von Model-Instanzen, ohne dabei die gesamten Model-Felder angeben zu müssen. Die Serialisierungsklasse erkennt, welche Felder in der Model-Klasse vorhanden sind und mappt diese automatisch auf die Serialisierungsklasse.

(De)serialisierung in Flask

Flask unterstützt dieses Feature auch. Da jedoch Flask ein Micro-Framework ist und deswegen minimalistisch gehalten wurde, müssen die passenden Erweiterungen erst installiert werden.

Im Folgenden möchte ich zeigen, dass automatische (De)serialisierung auch in Flask möglich ist. Ich verwende dabei SQLAlchemy also ORM und Marshmallow als (De)serialisierungs-Engine. Da Marshmallow SQLAlchemy nicht out  of the box unterstützt, ist die Installation der Extension  marshmallow-sqlalchemy notwendig.

Beispielapplikation

Als Anwendungsfall dient eine sehr einfachen Flask-Anwendung, welche aus einer handvoll REST-Endpoints und zwei Model-Klassen besteht. Das Model Company soll ein Unternehmen abbilden, das Model Product speichert Produkte in die Datenbank und weißt das Produkt einem Unternehmen über einen Fremdschlüssel zu.

Produkte bestehen im Wesentlichen aus einem Namen, einer Beschreibung und einem Preis. Außerdem kann ein Discount auf ein Produkt vergeben werden. Ein Datenbank-Modell wie dieses findet typischerweise Anwendung in einem Webshop. Natürlich in sehr viel ausgereifterer Form.

Das Product Model wird über GET/PUT/POST Requests modifiziert, wie weiter unten gezeigt wird.

Die Python Klassen sehen wie folgt aus:

class Company(db.Model, CreatedModifiedMixin):
    id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4)
    company_name = db.Column(db.String(255), nullable=False, unique=True)

    def __repr__(self):
        return "<Company {} ({})".format(self.id, self.company_name)


class Product(db.Model, CreatedModifiedMixin):
    id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4)
    name = db.Column(db.String(255), nullable=False, unique=True)
    description = db.Column(db.String(255), nullable=False, unique=True)
    price = db.Column(db.String(255), nullable=False, unique=True)
    discount = db.Column(db.String(255), nullable=False, unique=True)

    # relationships
    company_id = db.Column(
        UUIDType(binary=False), db.ForeignKey("company.id"), nullable=False
    )
    company = db.relationship("Company", backref=db.backref("products", lazy=True))

    def __repr__(self):
        return "<Product {} ({})".format(self.id, self.name)

Die folgende REST-API wurde mit der Extension Flask-RESTful implementiert.
Die Klasse ProduktCreateView ermöglicht das Hinzufügen von neuen Produkten.

from flask_restful import Resource

class ProductCreateView(Resource):
    def post(self):
        product_data = request.get_json(force=True)
        schema = ProductSchema()
        try:
            new_product = schema.load(product_data, session=db.session)
            db.session.add(new_product)
            db.session.commit()
        except ValidationError as err:
            return err.messages, 400

        return 200

Bei einem HTTP POST-Request, werden die Produktdaten, welche im JSON-Format mitgeschickt werden gelesen und in das Python-Dictionary product_data gespeichert. Die ProductSchema Instanz deserialisiert das Dictionary in ein neues Objekt des Product Models. Dieses wird anschließend mithilfe von SQLAlchemy in die Datenbank geschrieben. Bei der Deserialisierung kann es zu Fehlern bei der Validierung kommen. Wenn zum Beispiel die Produktbeschreibung kein gültiger String ist, wird ein ValidationError geworfen. Die Daten werden in diesem Fall nicht in die Datenbank geschrieben und  die Fehlerbeschreibungen an den Client zurückgeschickt.

Die ProductSchema-Klasse leitet von SQLAlchemyAutoSchema ab. Diese Klasse ermöglicht das (De)serialisieren von Produkt-Entitäten. Das “Auto” im Namen bezeichnet das automatische Mapping von Datenbankfeldern auf die Serialisierungsklasse. In der Meta Klasse muss das Modell, auf welches gemappt wird, angegeben werden. Dadurch wissen ProductSchema Instanzen, auf welche Datenbankfelder der Model-Klasse gemappt werden können. load_instance muss in unserem Fall auf True gesetzt sein, damit die Produktdaten in Model-Instanzen deserialisiert werden können. Wenn load_instance=False wäre, würde der schema.load() Aufruf eine Exception werfen. include_fk bedeutet, dass wir Fremdschlüssel ebenfalls abspeichern möchten.

Manche Felder möchten wir bewusst von der Serialisierung ausschließen. Das können wir durch den Parameter load_only erreichen. Das Feld discount kann in unserem Fall deserialisert, jedoch nicht serialisiert werden. Solche Felder werden als write-only bezeichnet. Das Pendant zu load_only ist dump_only. Felder, welche als dump_only deklariert werden, können serialisert, jedoch nicht deserialisert werden und sind somit read-only.

class ProductSchema(SQLAlchemyAutoSchema, FilteredSchema):
    class Meta:
        model = Product
        load_instance = True
        include_fk = True

    discount = auto_field(load_only=True)

Die zweite View Klasse ProductEntityView ist für das Ändern und Zurückliefern einzelner Produkte zuständig.

class ProductEntityView(Resource):
    def get(self, id):
        product = db.session.query(Product).filter_by(id=id).first()
        schema = ProductSchema(many=False)
        return schema.dump(product), 200

    def put(self, id):
        product = db.session.query(Product).filter_by(id=id).first()
        product_data = request.get_json(force=True)
        try:
            schema = ProductSchema()
            schema.load(
                product_data, instance=product, session=db.session, partial=True
            )
        except ValidationError as err:
            return err.messages, 400
        return 200

ProductSchema wird hier in der gleichen Weise eingesetzt wie in ProductCreateView. In get() werden Produkte aus der Datenbank gelesen und mit schema.dump() serialisiert. Mit dem Parameter many=False weiß die Schema-Klasse, dass nur eine Instanz serialisiert werden muss. Falls eine Datenbank-Query mehrere Produkte liefert und diese anschließend serialisiert werden sollen, dann muss many=True angegeben werden.

put() ist für das Ändern einzelner Prdukte zuständig. schema.load() nimmt die neuen Produkt-Daten entgegen und updated die Instanz, welche aus der Datenbank geladen wurde. partial=True bedeutet, dass nur ein Teil der Felder der Produkt-Entität geändert werden soll und nicht das gesamte Objekt. Auch hier wird wie bei post() ein ValidationError geworfen, sollten die Daten aus dem Request nicht gültig sein.

Zusammenfassung

(De)serialisierung von Datenbankobjekten, auf welche mittels SQLAlchemy zugegriffen werden, ist mit der Klasse SQLAlchemyAutoSchema aus der Erweiterung marshmallow-sqlalchemy sehr einfach möglich. Es müssen keine komplizierten Mapper-Klassen geschrieben werden. Das Mapping passiert dank marshmallow automatisch. Die einzigen zwei Hürden sind die richtige Konfiguration der Serialisierungsklassen und das Übergeben der richtigen Parametern an schema.load() sowie schema.dump(). Dieser Artikel hat aber hoffentlich gezeigt, dass auch dies keine Hexerei ist.

 

 

Der Source-Code zu diesem Artikel ist hier verfügbar.

Abonnieren
Benachrichtige mich bei
guest
0 Comments
Inline Feedbacks
View all comments