23 mar 2017

Python Flask i MySQL - łatwo i szybko czy droga przez mękę?

Kontynuując post z poniedziałku pozostaję w temacie połączenia Flask i MySQL. W dzisiejszym wpisie będzie case study jak to połączenie odbyło się w moim projekcie - w końcu pora zacząć spinać wszystkie elementy w jedną, działającą całość.

W poprzednim wpisie podałem kilka modułów do MySQL dostępnych dla Flask, ale nie wspomniałem którego użyję. Pierwsza wersja backendu używała Flask-MySQL, zdecydowałem jednak o zmianie na Flask-MySQLdb. Różnica niewielka, ale przekonała mnie kompatybilność z Python 3+. Niestety po wydaniu polecenia
pip install flask-mysqldb
zobaczyłem jedynie błąd
running build_ext
building '_mysql' extension
error: Microsoft Visual C++ 9.0 is required. Get it from http://aka.ms/vcpython27
pobranie Microsoft Visual C++ Compiler for Python 2.7 i uruchomienie instalacji zmieniło tylko komunikat na
_mysql.c(29) : fatal error C1083: Cannot open include file: 'my_config.h': No such file or directory
Na szczęście jest stackoverflow, zgodnie ze znalezioną poradą pobrałem odpowiednią paczkę ze strony http://www.lfd.uci.edu/~gohlke/pythonlibs/#mysqlclient (w moim przypadku mysqlclient-1.3.10-cp27-cp27m-win32.whl), kilka poleceń w konsoli
pip install wheel
Successfully installed wheel-0.29.0

pip install .\mysqlclient-1.3.10-cp27-cp27m-win32.whl
Successfully installed mysqlclient-1.3.10

pip install flask-mysqldb
Installing collected packages: flask-mysqldb
Successfully installed flask-mysqldb-0.2.0
Zatem w końcu sukces, a to dopiero początek drogi.

Jak wczytać ustawienia z zewnętrznego pliku
Niezależnie jednak od użytego modułu pojawiła się potrzeba zapisania danych połączenia z bazą w osobnym pliku. Można to zrobić na wiele sposobów, np.
  • plik z kodem Python, który importuje się jak inne moduły
  • JSON
  • YAML, czyli specjalny format do przechowywania konfiguracji
  • plik INI
  • standardowy XML
Bardzo dobre zestawienie tych metod razem z przykładami użycia jest dostępne tutaj.
Najłatwiejsza i wystarczająca w moim przypadku wydaje się metoda nr 1 i tak powstał plik databaseconfig.py:
mysql = {'host': 'database_host',
         'user': 'database_user',
         'passwd': 'database_passwd',
         'db': 'database_name'}
Kod!
Pora na programistyczne mięcho. Mam już wszystko żeby napisać kolejną metodę do API,  postanowiłem więc przygotować dane do wyświetlenia na wykresie w interfejsie użytkownika.
Dodaję import flask_mysqldb oraz jsonify z modułu flask (tworzy obiekt response z jsonem który można zwrócić bezpośrednio z metody), wczytuję konfigurację z pliku. Nagłówek pliku app.py z wygląda teraz tak:
from flask import Flask
from flask import Response
from flask import jsonify
from flask_mysqldb import MySQL

app = Flask(__name__)

import databaseconfig as cfg
app.config['MYSQL_USER'] = cfg.mysql['user']
app.config['MYSQL_PASSWORD'] = cfg.mysql['passwd']
app.config['MYSQL_DB'] = cfg.mysql['db']
app.config['MYSQL_HOST'] = cfg.mysql['host']
mysql = MySQL(app)
a metoda pobierająca dane z bazy tak:
@app.route("/dbtest")
def fetchfromdb():
    query_string = "SELECT Date, Value FROM rate WHERE Date > '2015-01-01' AND FundId = 1" 
    cursor = mysql.connection.cursor()
    cursor.execute(query_string)
    return jsonify(data=cursor.fetchall())
Sukces? Jeszcze nie. Niestety przygotowana przeze mnie metoda po wywołaniu wyrzuciła błąd z parsowaniem typu decimal
TypeError: Decimal('110.11') is not JSON serializable
Parsowanie decimal
O co chodzi z tym błędem? Otóż JSON w swojej specyfikacji nie uwzględnia takiego typu jak decimal, więc autorzy Flask nie implementowali swojej wersji tego jak ma być serializowany. Chcesz mieć decimal - musisz sam zdecydować jak ma on być zapisywany. Można oczywiście pozbyć się decimali z zapytania do bazy
SELECT Date, CAST(Value * 100 AS INT) FROM rate WHERE Date > '2015-01-01' AND FundId = 1
ale to tylko obejście problemu zamiast jego rozwiązania.
Na szczęście domyślny parser JSON można nadpisać swoją implementacją, w której zdefiniujemy zachowanie dla wybranych typów, a reszta zostanie w domyślnej postaci:
import flask.json
import decimal

class MyJSONEncoder(flask.json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, decimal.Decimal):
            # Convert decimal instances to strings.
            return str(obj)
        return super(MyJSONEncoder, self).default(obj)
app.json_encoder = MyJSONEncoder
Po takiej zmianie aplikacja działa już poprawnie i zwraca dane jakie możemy odebrać we frontendzie.

Po rozwiązaniu powyższego problemu dowiedziałem się jeszcze o bibliotece simplejson, która zawiera domyślną metodę obsługi decimal. Chciałem jednak pozostać przy Flask.jsonify że względu na to, że zwraca od razu obiekt Response gotowy do zwrócenia z funkcji. Nie testowałem, ale ponoć
If you install the simplejson package, flask.jsonify will automatically use that instead of the stdlib json library.
Na koniec odniosę się do pytania, które umieściłem w tytule. Niestety nie mogę powiedzieć żeby połączenie z MySQL było wybitnie łatwe - w całym procesie pojawiło się kilka problemów. Część z nich wynika na pewno z mojej nieznajomości Pythona. Na szczeście nie była to również droga przez mękę i nadal uważam że Flask był dobrym wyborem jeśli chodzi o szybkie prototypowanie backendu. Zobaczymy czy i jak długo ta opinia się utrzyma :)

0 komentarze:

Prześlij komentarz