2020年7月31日金曜日

Flask-Migrate で定義したテーブル定義から ER 図を作成する方法

概要

eralchemy というツールがあるのでこれを使います
素の SQLAlchemy と連携する方法は結構書かれているのですが Flask-SQLAlchemy と Flask-Migrate を使ったサンプルが少なかったので紹介します

環境

  • macOS 10.15.5
  • MySQL 8.0.19
  • Python 3.8.3
  • Flask-SQLAlchemy 2.5.3
  • Flask-Migrate 2.5.3
  • Flask 1.1.2

インストール

  • pipenv install eralchemy

サンプルコード

  • vim my_app/database.py
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from eralchemy import render_er # add

db = SQLAlchemy()
ma = Marshmallow()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128))
    age = db.Column(db.Integer)
    __table_args__ = (
        db.UniqueConstraint('name', 'age', name='unique_name_age'),
    )
    item = db.relationship("Item", backref="user")

    def __repr__(self):
        return "%s,%s,%i" % (self.id, self.name, self.age)

class Item(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    name = db.Column(db.String(20))

class UserSchema(ma.Schema):
    class Meta:
        fields = ("id", "name", "age")

class ItemSchema(ma.Schema):
    class Meta:
        fields = ("user_id", "name", "user")
    user = ma.Nested(UserSchema, many=False)

# add
if __name__ == '__main__':
    render_er(db.Model, 'er.png')

説明

基本的には過去に紹介したテーブル定義に render_er を使って画像を出力する main 部分を追加してあげるだけです

ポイントは render_er に指定するオブジェクトですが db.Model を渡します
SQLAlchemy と連携する記事だと declarative_base から生成したオブジェクトを渡すサンプルが多いのですが Flask-SQLAlchemy を使っている場合は declarative_base は基本使いません
なので渡すオブジェクトは db.Model を渡します

動作確認

  • pipenv run python my_app/database.py

参考サイト

2020年7月30日木曜日

Mac 上の VirtualBox に Windows10 Pro をインストールし VM として使用する方法

概要

Mac を使っていてどうしても Windows が必要な場合があると思います
別マシンを購入してもいいですができる限り安上がりにしたいので VirtualBox 上に構築してみました
いろんなところで同じような方法が紹介されているので備忘録として残しておきます

環境

  • macOS 10.15.6
  • VirtualBox 6.1.12
  • Windows10 Professional

Windows10 のライセンスの入手

何でも OK です
すでに使える Retail 版のライセンスがあればそれでも OK です
自分は consogame で Retail 版のライセンスを購入しました
購入時の通貨は RUB で日本円に換算して手数料も込で税込みで 1670 円くらいでした

クレカでの支払い時に「Card country does not match payment country」が発生する場合は VPN やプロキシが設定されてないか確認してください
それでもダメな場合はブラウザを変えたり端末を変えて別のネットワークから試してみてください

Windows10 の ISO のダウンロード

ここからダウンロードしましょう
ISO は 1 つですが Professional にも対応しています

Win10_2004_Japanese_x64.iso というファイルでサイズは約 5.19GB でした

VirtualBox に Windows の仮想マシンを作成

仮想マシンを 1 台作成しダウンロードした ISO ファイルをマウントしましょう

あとは起動してインストールを進めていきます
途中でライセンスキーを入力する画面になるので入力します

またインストールは完全新規なのでアップグレードを選択しないようにしましょう
インストールが完了したら ISO を外してから再起動しましょう

その他

  • マイクロソフトアカウントでログインするところがあるのでアカウントがある人はログインしておく
  • PIN の作成
  • Android のスマホ連携も必要であればやる
  • Cortana は日本語では使えない
  • Alt+Shift で入力言語の切り替え
  • インストール後に Windows Update を実施する

動作確認

普通に使えると思います
ちゃんとライセンスも正しく認識されて Windows アップデートが実行できれば OK です

2020年7月29日水曜日

Vue.js で折れ線グラフを描画してみた (vuetify 編)

概要

vuetify で v-sparkline を使うと簡単にグラフを描画できます
作業は vue-cli を使って進めます

環境

  • macOS 10.15.6
  • nodejs 14.5.0
  • yarn 1.22.4
  • vue cli 4.4.6
  • vuetify (vue-cli-plugin-vuetify) 2.0.7

vue プロジェクトの作成

  • vue create hello-world

エンターでデフォルトの設定で OK です
Formatter がほしい場合は手動で設定から行いましょう

vuetify の追加

  • cd hello-world
  • vue add vuetify

vuetify もデフォルト設定で進めて OK です

HelloWorld.vue にグラフを描画

vuetify を追加した際に作成される Vue コンポーネントを再利用します
とりあえずグラフを表示するだけのサンプルを記載します

  • vim src/components/HelloWorld.vue
<template>
  <v-container fluid>
    <v-sparkline
      :value="value"
      :gradient="gradient"
      :smooth="radius || false"
      :padding="padding"
      :line-width="lineWidth"
      :stroke-linecap="lineCap"
      :gradient-direction="gradientDirection"
      :fill="fill"
      :type="type"
      :auto-line-width="autoLineWidth"
      auto-draw
      :show-labels="showLabels"
      :label-size="labelSize"
    ></v-sparkline>
  </v-container>
</template>

<script>
const gradients = [
  ["#222"],
  ["#42b3f4"],
  ["red", "orange", "yellow"],
  ["purple", "violet"],
  ["#00c6ff", "#F0F", "#FF0"],
  ["#f72047", "#ffd200", "#1feaea"]
];

export default {
  data: () => ({
    showLabels: false,
    lineWidth: 2,
    labelSize: 7,
    radius: 10,
    padding: 8,
    lineCap: "round",
    gradient: gradients[5],
    value: [0, 2, 5, 9, 5, 10, 3, 5, -4, -10, 1, 8, 2, 9, 0],
    gradientDirection: "top",
    gradients,
    fill: false,
    type: "trend",
    autoLineWidth: false
  })
};
</script>

少し解説

v-sparkline に必要な属性 (value や gradient) などを設定します
サンプルではグラフの描画に必要なそれぞれの属性は script 内の data ですべて宣言しているのでそこの値を調整すればグラフの色や形を好きなように変更できます

重要なのは value でここにグラフに表示するデータの配列を設定します
その他の属性は見た目に関する属性がほとんどです
showLables や fill を true にしたりするとラバルが追加されたりグラフが色埋めされたりするのが確認できると思います

動作確認

  • yarn serve

最後に

vuetify の v-sparkline を使って二次元データを折れ線グラフにしてみました
vuetify では折れ線グラフしか書けないので他のグラフを描画したい場合は vue-charts など他のプラグインを使いましょう

参考サイト

2020年7月28日火曜日

Vue で作成したコードをプロダクション用として nginx にデプロイする方法

概要

vue cli で作成したプロジェクトであれば yarn build コマンド一発でプロダクション用の html, js, css を出力できます
今回は出力したファイルをデプロイする方法を紹介します

環境

  • macOS 10.15.6
  • nginx 1.19.1
  • yarn 1.22.4
  • vue cli 4.4.6

ビルド

  • yarn build

これだけで OK です
完了すると dist ディレクトリ配下に作成されるので基本はそれらを DocumentRoot に配置すれば OK です

デプロイ

例えば macOS 上の nginx であれば以下のような感じでできます
デフォルトの DocumentRoot は /usr/local/var/www になります
また nginx が起動するポートは 8080 になっています

  • mv /path/to/dist/* /usr/local/var/www
  • nginx

これで localhost:8080 にアクセスすると Vue で作成した Single Page Application が表示されます

VueRouter を使っている場合の注意点

VueRouter を使っている場合 VueRouter が生成する URI に再度アクセスすると 404 エラーになってしまいます
これは nginx 側で VueRouter が生成した URL を受ける設定をしていないためです

server ディレクティブに 404 時にリダイレクトする設定を入れましょう
そして reload します

  • vim /usr/local/etc/nginx/nginx.conf
error_page  404 /;
  • nginx -s reload

これで再度確認すると以下のように nginx のデフォルトの 404 ページが表示されずに現在のページにあたかもリダイレクトされているような挙動になります

ただコンソールを見るとわかりますが一度 nginx が 404 を返すことには変わりないのでエラーは表示されてしまうことには注意しましょう
もしかするともっと良い方法があるのかもしれませんがとりあえず手っ取り早く対応するならこれが簡単です

最後に

Vue で作成したアプリをビルドしてデプロイするところまでやってみました
vue cli で作成していればコマンド一つでビルドできるように設定してくれるので簡単です
が、内部で何をやっているのかはさっぱりなので詳しく知りたい方には向いていない方法かなとも思います

VueRouter を使っている場合には注意が必要だということもわかりました (VueRouter に関わらず angular などの SAP でも同様の現象になります)
今回は nginx にデプロイしてみましたが他の web サーバでも同様の対処が必要になると思います

参考サイト

2020年7月27日月曜日

Vue Router を使ってみた

概要

VueRouter は特定のパスにアクセスした際に Vue コンポーネントを呼び出すことができる機能です
今回はインストールから簡単な使い方まで紹介します

環境

  • macOS 10.15.6
  • nodejs 14.5.0
  • yarn 1.22.4
  • vue cli 4.4.6
  • vue router 4.4.6

インストール

vue cli を使ってインストールします
編集途中のコードがある場合はコミットしましょう
またインタラクティブに聞かれますがとりあえずエンターで OK です

  • vue add router

Vue Router を使う設定を追加

どうやら最新版だと何もせずとも src/main.js に router を使う設定を入れてくれるようです

  • vim src/main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";

Vue.config.productionTip = false;

new Vue({
  router,
  render: h => h(App)
}).$mount("#app");

またルーティングを定義するための src/router/index.js やルーティング先のビューを定義するための src/views/Home.vue なども自動で作成してくれます

なのでこの状態で起動すると VueRouter を使って作成したページを確認することができます

  • yarn serve

独自のルーティングとビューを追加してみる

ではここからは学習として独自のルーティングを追加してみます
まずは新規のルーティング (/hello) を追加します

  • vim src/router/index.js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
import HelloWorld from "../components/HelloWorld.vue"; // add

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "Home",
    component: Home
  },
  {
    path: "/about",
    name: "About",
    component: () =>
      import("../views/About.vue")
  },
  // add
  {
    path: "/hello",
    name: "HelloWorld",
    component: HelloWorld
  }
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

export default router;

import 文で Vue コンポーネントを追加しています (あとで作成)
そして routes 配列にルーティングを追加します
アクセスするパスと使用する Vue コンポーネントを指定しましょう

次に Vue コンポーネントを追加します
src/components/HelloWorld.vue がすでにある場合はそれを使っても OK です
今回は適当にカウントするコンポーネントを作っています

  • vim src/components/HelloWorld.vue
<template>
  <div>
    <h1>{{ counter }}</h1>
    <button v-on:click="countup">up</button>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data: () => ({
    counter: 0,
  }),
  methods: {
    countup: function() {
      this.counter += 1
    }
  }
};
</script>

最後にメインコンポーネントとなる App.vue を書き換えて完了です
全部記載していますが追記しているのは 6 行目の <router-link to="/hello">HelloWorld</router-link> になります

  • vim src/App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/hello">HelloWorld</router-link>
    </div>
    <router-view />
  </div>
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
</style>

動作確認

  • yarn serve

こんな感じで新しいルーティングとコンポーネントが表示されるのが確認できると思います

最後に

VueRouter を試してみました
最新版だと雛形も作成してくれるのでかなり簡単に導入できるかなと思います
VueRouter を使うと URL にも現在表示中のコンポーネントが表示されるのでただページが遷移するよりかは UX 的にも良くなるかと思います
またコーディングする側もルーティングごとに役割を分離できるのでメンテナンスもしやすくなるかなと思います

参考サイト

2020年7月26日日曜日

Vue + axios で CORS 対応を簡単にする方法

概要

一番手っ取り早いのはプロキシ機能を使うことです
ただしプロダクション用のビルド環境では使えないので注意が必要です

環境

  • macOS 10.15.6
  • nodejs 14.5.0
  • yarn 1.22.4
  • vue cli 4.4.6
  • vuetify (vue-cli-plugin-vuetify) 2.0.7
  • vue-axios 2.1.5

エラーの内容

こんな感じでブラウザに怒られます
サーバ側で対応してくれるのであればアプリ側は特に対応の必要はないですがそれでも開発時などは localhost で開発することが多いのでエラーに遭遇することが多いかなと思います

Access to XMLHttpRequest at 'https://kaka-request-dumper.herokuapp.com/' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

対応策

プロキシ機能というのがあるのでこれを使います

まず vue.config.jsdevServer.proxy という項目を追加します
ここにアクセス時に CORS エラーになった URL を記載します
最後の / スラッシュは記載しません

  • vim vue.config.js
module.exports = {
  transpileDependencies: ["vuetify"],
  devServer: {
    proxy: "https://kaka-request-dumper.herokuapp.com"
  }
};

あとは axios を使って呼び出す際にアクセスする URI の部分だけ指定するようにします
するとすべてのリクエストは一度プロキシを経由してアクセスするようになるため CORS エラーが発生しなくなります

  • vim src/components/HelloWorld.vue
<template>
  <v-container>
    <v-row>
      <v-col>
        <button v-on:click="getRequest">{{ button.name }}</button>
      </v-col>
    </v-row>
    <v-row>
      <v-col>
        <div>path_info: {{ path_info }}</div>
        <div>method: {{ method }}</div>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
export default {
  name: "HelloWorld",

  data: () => ({
    button: {
      name: "Get data"
    },
    path_info: "",
    method: ""
  }),
  methods: {
    getRequest: function() {
      this.axios
        .get("/api")
        .then(
          response => (
            (this.path_info = response.data.path_info),
            (this.method = response.data.method)
          )
        )
        .catch(err => console.log(err));
    }
  }
};
</script>

root はプロキシしてくれない?

ただこの方法を使う問題点として root パスはプロキシしてくれないっぽいです
https://github.com/vuejs/vue-cli/issues/3588

例えば .get("/") はプロキシしてくれません (もしかすると実現できる 方法があるかもしれないのですが自分はできませんでした)
具体的にどのような挙動になるかというと Vue アプリの root パスにアクセスするので作成しているアプリや UI の HTML が返ってきてしまいます
おそらく publicPath が影響しているのかなと思います

yarn build で作成したプロダクション用のソースではプロキシしてくれない

yarn build すると SAP 用の html, js, css が出力されます
完全にブラウザのみで動作するアプリになるのでプロダクション環境ではサーバ側に CORS 対応を入れてもらうしかありません
今回紹介した方法はあくまでも開発環境で使用するテクニックになります

最後に

Vue のプロキシ機能を使って CORS 対応してみました
簡単に CORS 対応できるので特に細かい仕様を気にしないのであればこれを使って良いかなと思います
ただ root パスはプロキシしてくれないのでそこだけ注意が必要かなと思います

参考サイト

2020年7月25日土曜日

Vue で HTTP 通信するサンプルコード (axios 編)

概要

vue-axios を使うと簡単に HTTP 通信を実現することができます
今回は GET のサンプルを紹介します
またプロジェクトには vuetify を適用しています

環境

  • macOS 10.15.6
  • nodejs 14.5.0
  • yarn 1.22.4
  • vue cli 4.4.6
  • vuetify (vue-cli-plugin-vuetify) 2.0.7
  • vue-axios 2.1.5

axios インストール

  • yarn add axios
  • yarn add vue-axios

axios を使用する設定

  • vim src/main.js
import Vue from "vue";
import axios from "axios"; // add
import VueAxios from "vue-axios"; // add
import App from "./App.vue";
import vuetify from "./plugins/vuetify";

Vue.config.productionTip = false;

Vue.use(VueAxios, axios); // add

new Vue({
  vuetify,
  render: h => h(App)
}).$mount("#app");

HelloWorld.vue の編集

  • vim src/components/HelloWorld.vue
<template>
  <v-container>
    <v-row>
      <v-col>
        <button v-on:click="getRequest">{{ button.name }}</button>
      </v-col>
    </v-row>
    <v-row>
      <v-col>
        <div>Name: {{ name }}</div>
        <div>Rate: {{ rate }}</div>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
export default {
  name: "HelloWorld",

  data: () => ({
    button: {
      name: "Get data"
    },
    name: "",
    rate: ""
  }),
  methods: {
    getRequest: function() {
      this.axios
        .get("https://api.coindesk.com/v1/bpi/currentprice.json")
        .then(
          response => (
            (this.name = response.data.chartName),
            (this.rate = response.data.bpi.USD.rate)
          )
        );
    }
  }
};
</script>

動作確認

  • yarn lint
  • yarn serve

軽く解説

まずは axios をプロジェクトで使えるようにする必要があります
npm or yarn でインストールしたあとに main.js で import しましょう
そして Vue.use を使って axios を Vue コンポーネントで使えるようにします

今回は v-on:click のイベントで getRequest メソッドがコールするようにしています
その中で axios を呼び出しています
GET の場合は .get に URL を指定するだけです
あとは Promise の .then で結果を待って結果が来たら Vue コンポーネントに予め定義しておいた name, rate にそれぞれデータを設定するだけです
今回はレスポンスが JSON で返ってくるので簡単に参照できていますがそうでない場合は少し工夫が必要です

また CORS は考慮していないのでサーバ側で CORS 対応していない場合には通信エラーになります

参考サイト

2020年7月24日金曜日

Vuetify 超入門

概要

過去に vue.js を HTML だけで試してみました
すべての機能を使うには vue cli を使ってプロジェクトを作成し UI を作成できるようにする必要があります
今回は vue cli でプロジェクトを作成し vue のプロジェクトを作成した上で vuetify という Vue.js で作成した UI にマテリアルデザインを当てることができる機能を試してみました

環境

  • macOS 10.15.6
  • nodejs 14.5.0
  • yarn 1.22.4
  • vue cli 4.4.6
  • vuetify (vue-cli-plugin-vuetify) 2.0.7

yarn インストール

yarn じゃなくてもいいのですが長いものには巻かれろ精神なので yarn を使います

  • brew install yarn

vue cli のインストール

次に vue cli を使います
vue cli は vue で UI を作るためのプロジェクトを簡単に作成できるツールです

  • yarn global add @vue/cli

vue -V でバージョンが確認できれば OK です

プロジェクトを作成する

それでも Vue.js で UI を作成するためのプロジェクトを作成しましょう

  • vue create hello-world

インタラクティブでいろいろ聞かれますが今回は基本すべてエンターで次に進めば OK です
現行のバージョンだと 3 つほど聞かれて終了でした

  • ? Your connection to the default yarn registry seems to be slow. Use https://registry.npm.taobao.org for faster installation? Yes
  • ? Please pick a preset: default (babel, eslint)
  • ? Pick the package manager to use when installing dependencies: Yarn

vue のプロジェクトを作成すると git リポジトリも同時に作成されます
とりあえずコミットしておきましょう

  • git commit -m "Created new vue project"

とりあえずデモ画面を表示する

  • cd hello-world
  • yarn serve

localhost:8080 にアクセスするとデモ UI が表示されるのが確認できると思います

vuetify を使ってみる

まずはインストールしてみます
vue cli を使ってインストールできます

  • vue add vuetify

ここでもインタラクティブに聞かれますがエンターで OK です
デフォルトセッティングでいい感じに作ってくれます

とりあえず動かしてみる

vuetify にもデモページがあるので動かしてみましょう

  • yarn serve

先ほどと同じコマンドですが localhost:8080 にアクセスすると vuetify 用のデモ UI が表示されるのが確認できると思います

既存のレイアウトを適用してみる

ここにいろいろとレイアウトがあるので試してみましょう
今回は dark を試してみましょう

  • wget 'https://raw.githubusercontent.com/vuetifyjs/vuetify/master/packages/docs/src/layouts/layouts/demos/dark.vue' -O src/App.vue
  • yarn serve

すると以下のような感じのダークモードな管理画面っぽいレイアウトが表示されるのが確認できると思います

少し書き換えてみる

先程のレイアウトのメイン部分を vuetify のデモ画面にでもしてみたいと思います
vuetify のデモ画面は src/components/HelloWorld.vue としてすでにテンプレートとして切り出されているのでこれを src/App.vue から参照してみたいと思います

まずは HelloWorld.vue を App.vue から参照する部分を書きます

  • vim src/App.vue
<template>
  <v-app id="inspire">
    <v-navigation-drawer v-model="drawer" app clipped>
      <v-list dense>
        <v-list-item link>
          <v-list-item-action>
            <v-icon>mdi-view-dashboard</v-icon>
          </v-list-item-action>
          <v-list-item-content>
            <v-list-item-title>Dashboard</v-list-item-title>
          </v-list-item-content>
        </v-list-item>
        <v-list-item link>
          <v-list-item-action>
            <v-icon>mdi-cog</v-icon>
          </v-list-item-action>
          <v-list-item-content>
            <v-list-item-title>Settings</v-list-item-title>
          </v-list-item-content>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>

    <v-app-bar app clipped-left>
      <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
      <v-toolbar-title>Application</v-toolbar-title>
    </v-app-bar>

    <!-- ここを修正 -->
    <v-main>
      <HelloWorld></HelloWorld>
    </v-main>

    <v-footer app>
      <span>&copy; {{ new Date().getFullYear() }}</span>
    </v-footer>
  </v-app>
</template>

<script>
// コンポーネントを import
import HelloWorld from "./components/HelloWorld.vue";

export default {
  // ここで使用するコンポーネントを定義する
  components: {
    HelloWorld
  },

  props: {
    source: String
  },

  data: () => ({
    drawer: null
  }),

  created() {
    this.$vuetify.theme.dark = true;
  }
};
</script>

書き換えているのはコメントの部分だけです
まず v-main の部分です
ここを HelloWorld コンポーネントを参照するように変更しています

また script 部分でコンポーネントのインポートを行います
デモのダークモードのレイアウトには当然 HelloWorld のコンポーネントを参照する定義がないので追加しましょう

これで UI を確認すると以下のようにメイン部分が vuetify のデモ画面になっていることが確認できると思います

あとは HelloWorld.vue コンポーネントを書き換えればメイン部分の編集もできます

おまけ: lint をかける

  • yarn lint

おまけ: format するには

プロジェクトを作成する際に「Manually select features」を選択し「Pick a linter / formatter config」で「ESLint + Prettier」を選択します
正直これは設定しておいたほうがいいです

そしていつフォーマットするかでどちらかもしくは両方を選択しましょう

「Lint on save」はリント時に同時にフォーマットします
「Lint and fix on commit」は git commit 時にフォーマットします

最後に

Vue.js でマテリアルデザインを実現できるプラグインの vuetify を試してみました
当然ですが前提知識として Vue.js の仕組みや使い方を知っておく必要があります
vuetify 専用の v-hogehoge がたくさんあるのでそれらの使い方を学習しないと使いこなすのは難しそうです

参考サイト

2020年7月23日木曜日

Flask-SQLAlchemy で実行される SQL クエリをログに表示する方法

概要

SQLALCHEMY_ECHO を設定するだけです

環境

  • macOS 10.15.5
  • MySQL 8.0.19
  • Python 3.8.3
  • Flask-SQLAlchemy 2.5.3
  • Flask 1.1.2

方法

app.config['SQLALCHEMY_ECHO'] = True

ただしこの方法だとテーブルチェックなどのデバッグ SQL も表示されるので注意しましょう

参考サイト

2020年7月22日水曜日

Flask-Migrate で外部キー制約のあるテーブルを作成してみる

概要

過去に Flask-Migrate を使ってテーブルを作成してみました
今回は外部キー制約のあるテーブルを作成する方法を紹介します
なおファイルはこちらの記事を元にリファクタリングしているものを使います

環境

  • macOS 10.15.5
  • MySQL 8.0.19
  • Python 3.8.3
  • Flask-Migrate 2.5.3
  • Flask-SQLAlchemy 2.5.3

サンプルコード

とりあえずサンプルコードです
Item というテーブルを追加して User テーブルの id カラムを外部キー制約として定義します
なので User テーブルに存在しない id を Item テーブルの user_id に登録しようとするとエラーになります

  • vim my_app/database.py
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow

db = SQLAlchemy()
ma = Marshmallow()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128))
    age = db.Column(db.Integer)
    __table_args__ = (
        db.UniqueConstraint('name', 'age', name='unique_name_age'),
    )
    # item テーブルに user という名前で参照させてあげることを宣言
    item = db.relationship("Item", backref="user")

    def __repr__(self):
        return "%s,%s,%i" % (self.id, self.name, self.age)

class Item(db.Model):
    # 必ず primary_key となる id がないとエラーになるので定義
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    name = db.Column(db.String(20))

class UserSchema(ma.Schema):
    class Meta:
        fields = ("id", "name", "age")

class ItemSchema(ma.Schema):
    class Meta:
        fields = ("user_id", "name", "user")
    user = ma.Nested(UserSchema, many=False)

参照させる側のテーブルで db.relationship を定義します
backref=user とすることで User テーブルを user. で参照できるようにさせます
外部キー制約を付与したいカラムに db.ForeignKey("user.id") という感じで定義すれば制約を付与することができます
今回は Item に対して User は一意に決まるので many=False にすることで配列にならないようにしています

Flask-Migrate か Flask-SQLAlchemy の制約で primary_key が必須なようなので Item テーブルに id カラムを定義しています

ItemSchema について

これもポイントです
外部キーの参照先がある場合 sqlalchemy は自動で引っ張ってきてくれます
そのためのフィールドを追加で定義してあげています
また外部のテーブルは ma.Nested を使って階層化することで同じフィールドでも問題なくシリアライズできるようにしています

マイグレート

テーブルを作成しましょう

  • FLASK_APP=my_app pipenv run flask db migrate -m "Create item table"
  • FLASK_APP=my_app pipenv run flask upgrade

データは適当に入れておきます

  • mysql -u root test -e "select * from user;"
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | a    |   10 |
|  2 | b    |   10 |
|  3 | c    |   20 |
+----+------+------+
  • mysql -u root test -e "select * from item;"
+----+---------+-------+
| id | user_id | name  |
+----+---------+-------+
|  2 |       1 | apple |
|  3 |       1 | grape |
+----+---------+-------+

データを取得してみる

実際に Flask からデータを取得してみましょう
まずは Item テーブルからデータを取得する処理を作成します

  • vim my_app/lib/__init__.py
from my_app.database import db, Item, ItemSchema

class ItemCRUD():
    def select(self, id):
        item = Item.query.get(id)
        # print(item.user)
        return ItemSchema(many=False).dump(item)

そしてそれを Flask アプリでそれを使います

  • vim my_app/__init__.py
from flask import Flask
from flask_migrate import Migrate
from my_app.database import db, ma
from my_app.lib import ItemCRUD

app = Flask(__name__)
app.debug = True
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+mysqldb://root:@localhost/test?charset=utf8"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

db.init_app(app)
migrate = Migrate(app, db)
ma.init_app(app)

@app.route('/<id>')
def select(id=None):
    crud = ItemCRUD()
    item = crud.select(id)
    return item

動作確認

  • FLASK_APP=my_app pipenv run flask run
  • curl localhost:5000/2

=> {"name":"apple","user":{"age":10,"id":1,"name":"a"},"user_id":1}

こんな感じで外部キーの参照先の User テーブルの情報も階層化されて取得されるのが確認できると思います

最後に

Flask-Migrate で外部キー制約を付与してみました
今回は One-to-Many 構成で外部キー制約を付与しましたが One-to-Many や Many-to- Many でも制約を付与することができます

作成したテーブルはマイグレートしたあとでちゃんと show create table などで確認したほうが良いでしょう

参考サイト

2020年7月21日火曜日

Sinatra で production や development としてデプロイする方法

概要

APP_ENV と configure を使うことでデプロイ時のモードを設定することができます
簡単な使い方とちょっと応用した使い方を紹介します

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • Sinatra 2.0.7

準備

  • bundle init
  • bundle config path vendor
  • vim Gemfile
gem "sinatra"
gem "sinatra-contrib"
  • bundle install

サンプルコード

とりあえず簡単なサンプルコードを紹介します

  • vim app.rb
require 'sinatra/base'

class App < Sinatra::Base
  configure :development do
    set :message, 'development'
  end

  configure :production do
    set :message, 'production'
  end

  get '/' do
    settings.message
  end
end
  • vim config.ru
require './app'
run App
  • bundle exec rackup config.ru
  • curl localhost:9292

=> development

  • APP_ENV=production bundle exec rackup config.ru
  • curl localhost:9292

=> production

という感じでモードを切り替えることができます
基本は configure 内で settings に対して値をセットすることでアプリ側で値を参照することができます

設定ファイルを使う

モードごとに設定ファイルを用意してそれを読み込む方法もあります
設定をアプリの外に出すことができるのでわかりやすくなります

  • vim production.yml
message: "production"
  • vim development.yml
message: "development"
  • vim app.rb
require 'sinatra/base'
require "sinatra/config_file"

class App < Sinatra::Base
  register Sinatra::ConfigFile

  def self.read_config(file)
    config_file file
  end

  configure :development do
    self.read_config('./development.yml')
  end

  configure :production do
    self.read_config('./production.yml')
  end

  get '/' do
    settings.message
  end
end

注意点としては configure 内部でコールするメソッドはクラスメソッドとして定義する必要があります
クラスメソッドで定義しないと NoMethodError: undefined method read_config for App:Class になります
helper などを使う場合もクラスメソッドとして定義しましょう

共通化したい場合は Base クラスを使う

コントローラを複数に分けることがある場合は Base となるコントローラを作成してそれを元にリソースごとなどでコントローラを作成しましょう

  • vim base.rb
require 'sinatra/base'
require "sinatra/config_file"

class Base < Sinatra::Base
  register Sinatra::ConfigFile

  def self.read_config(file)
    config_file file
  end

  configure :development do
    self.read_config('./development.yml')
  end

  configure :production do
    self.read_config('./production.yml')
  end
end
  • vim app.rb
require './base'

class App < Base
  get '/' do
    settings.message
  end
end

おまけ: 環境変数で上書きできるようにする

設定の話になりますが環境変数で設定を上書きしたい場合があると思います
今回の方式であれば一番単純なのは read_config で上書きする方法があります

  • vim base.rb
require 'sinatra/base'
require "sinatra/config_file"

class Base < Sinatra::Base
  register Sinatra::ConfigFile

  def self.read_config(file)
    config_file file
    set :message, ENV["MESSAGE"] if ENV["MESSAGE"]
  end

  configure :development do
    self.read_config('./development.yml')
  end

  configure :production do
    self.read_config('./production.yml')
  end
end
  • MESSAGE=hoge APP_ENV=production bundle exec rackup config.ru

ただこれだと設定ファイルに記載の項目ごとに read_config に上書きするための設定を記載しなければならなくなります
なので設定ファイルの YAML ファイルに環境変数を仕込んであげましょう

  • vim development.yml.erb
message: <%= ENV["MESSAGE"] || "development" %>
  • vim base.rb
require 'sinatra/base'
require "sinatra/config_file"

class Base < Sinatra::Base
  register Sinatra::ConfigFile

  def self.read_config(file)
    config_file file
  end

  configure :development do
    self.read_config('./development.yml.erb')
  end

  configure :production do
    self.read_config('./production.yml')
  end
end
  • MESSAGE=hoge bundle exec rackup config.ru
  • bundle exec rackup config.ru

たぶん erb を使う方法が一番キレイかなと思います

最後に

Sinatra で環境ごとに設定を切り分ける方法を紹介しました
APP_ENV と configure を使えば簡単でできることがわかりました
基本は初期設定を環境ごとに切り替える感じなるかなと思います
環境変数などを使って更に設定を上書きできるようにしてもいいかもしれません

参考サイト

2020年7月18日土曜日

Ubuntu18.04 に pyenv をインストールする方法

Ubuntu18.04 に pyenv をインストールする方法

概要

基本は公式の手順を実行すれば OK です

環境

  • Ubuntu 18.04
  • pyenv 1.2.20

事前準備

  • sudo apt-get install -y build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python-openssl git

pyenv インストール

  • curl https://pyenv.run | bash
  • exec “$SHELL”
  • pyenv install 3.8.4
  • pyenv global 3.8.4

.bash_profile

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"
if command -v pyenv 1>/dev/null 2>&1; then
 eval "$(pyenv init -)"
fi

参考サイト

2020年7月17日金曜日

一度削除してしまったテーブルを Flask-Migrate で再作成する方法

概要

過去に Flask-Migrate でテーブルの変更履歴管理を行う方法を紹介しました
テストなどでテーブルを削除してしまった場合に upgrade コマンドだと再作成できないようだったので再作成する方法を考えてみました

環境

  • macOS 10.15.5
  • MySQL 8.0.19
  • Python 3.8.3
  • Flask-Migrate 2.5.3

一度テーブルを削除して再度作成するには

  • mysql -u root test -e "drop table user;"

こんな感じでテーブルを削除してしまった場合に再度 head の定義にあるテーブルを作成する方法を紹介します

方法その1: 再度 migrate する

migrate -> upgrade と順番に実行すると再度最新の定義でテーブルを作成することができます
また create table の定義がかかれているので他の環境に展開する場合はこのコミットを使えば upgrade だけで作成できます
しかしこの方法だと無駄なコミットが増えてしまいます

  • FLASK_APP=my_app pipenv run flask db migrate -m "Recreate table"
  • FLASK_APP=my_app pipenv run flask db upgrade head

方法その2: flask shell を使って db.create_all() をコールする

  • FLASK_APP=my_app pipenv run flask shell

で python のインタラクティブモードに入ったら

from my_app.database import db
db.create_all()

を実行すればテーブルが作成されます
これ自体は Flask-SQLAlchemy の機能を使って行っています
これであればコミットを作成することなく最新の定義でテーブルを作成できます

方法その3: alembic_version も drop して upgrade する

alembic_version テーブルに現在適用されているバージョンが記載されているのでこれを削除して upgrade します

  • mysql -u root test -e "drop table alembic_version;"
  • FLASK_APP=my_app pipenv run flask db upgrade head

これが一番王道だと思います
が注意しなければいけないのはもし手動で drop table してその後 Recreate の migrate をしてしまった場合は Recreate の migrate が適用されません
なぜなら Recreate のマイグレートには手動で削除した drop table の定義が書かれていないため user テーブルがすでに存在する状態で再度 create table しようとします
なのでもしそのような状態になったら再度手動で drop table user してから upgrade するかマイグレート用のスクリプトファイルを直接編集してテーブル度ドロップする定義を追加してから upgrade すれば OK です

おまけ: コミットメッセージを変更するには

  • pipenv run flask db edit head

おまけ: コミットを削除するには

もし history が以下のようになっている場合はそれに対応するファイルを削除すれば OK です

2036fa9f973c -> b966967378d7 (head), Recreate table 68be7c84addb -> 2036fa9f973c, 2 <base> -> 68be7c84addb, 1
  • rm migrations/versions/b966967378d7_recreate_table.py

最後に

普通に別のデータベースにマイグレートする場合は upgrade すれば大丈夫なはずです
が、ローカルなどで開発しているときにテーブルを手動で削除した場合には少し工夫が必要な感じになります
それでも alembic_version を削除して upgrade するのが一番簡単かなと思います
ただマイグレートコミットが矛盾するような履歴になってしまうと upgrade 時にエラーになるので注意しましょう
もしそうなった場合は直接マイグレートスクリプトファイルを編集するか再度リポジトリを作成し直しでも良いかなと思います

2020年7月16日木曜日

Flask-SQLAlchemy で複数のカラムでユニークキーを設定する方法

概要

例えば name と age でユニークキーを貼る場合は以下の通りです

環境

  • macOS 10.15.5
  • MySQL 8.0.19
  • Python 3.8.3
  • Flask-SQLAlchemy 2.4.3

サンプルコード

__table_args__ を使って db.UniqueConstraint に複数のカラムを指定すれば OK です
ユニークキーに名前をつけるので好きな名前をつけましょう

from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow

db = SQLAlchemy()
ma = Marshmallow()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128))
    age = db.Column(db.Integer)
    __table_args__ = (
        db.UniqueConstraint('name', 'age', name='unique_name_age'),
    )

    def __repr__(self):
        return "%s,%s,%i" % (self.id, self.name, self.age)

class UserSchema(ma.Schema):
    class Meta:
        fields = ("id", "name", "age")

当然ですがユニークキーを設定場合にすでに重複している場合は設定できないのでご注意を

参考サイト

2020年7月15日水曜日

Flask-SQLAlchemy で循環参照しないようにするコツ

概要

ポイントは SQLAlchemy の db オブジェクトを管理するモジュールを別にする点です
そこでモデルなども管理することで循環参照させないようにします

環境

  • macOS 10.15.5
  • MySQL 8.0.19
  • Python 3.8.3
  • Flask-SQLAlchemy 2.4.3

ライブラリインストール

今回はマイグレートにも対応するので Flask-Migrate もインストールします

  • pipenv install Flask-Marshmallow Flask-SQLAlchemy Flask-Migrate mysqlclient marshmallow-sqlalchemy

データベースを管理するモジュールの作成

今回の肝になるモジュールです
ポイントはここではオブジェクトの作成とモデルの管理を行うだけで Flask のコンテキストの埋め込みは行いません
それは Flask アプリを管理するモジュールで行う点です
そうすることで実際に SQL を発行すると処理を別モジュールとして更に管理できるようになります

  • vim my_app/database.py
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow

db = SQLAlchemy()
ma = Marshmallow()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128))
    age = db.Column(db.Integer)

    def __repr__(self):
        return "%s,%s,%i" % (self.id, self.name, self.age)

class UserSchema(ma.Schema):
    class Meta:
        fields = ("id", "name", "age")

ライブラリ

例えばモデルをもとに実際に CRUD を行う場合は別モジュールを作成します
参照先が Flask アプリでなく切り離した database.py だけになります

  • vim my_app/lib/__init__.py
from my_app.database import db, User, UserSchema

class UserCRUD():
  def add(self, name, age):
      user = User(name=name, age=age)
      db.session.add(user)
      db.session.commit()
      return 'ok'

  def select(self):
      return UserSchema(many=True).dump(User.query.all())

  def _select_one(self, id):
      return User.query.get(id)

  def delete(self, id):
      db.session.delete(self._select_one(id))
      db.session.commit()
      return 'ok'

  def update(self, id, name, age):
      user = self._select_one(id)
      user.name = name
      user.aget = age
      db.session.add(user)
      db.session.commit()
      return 'ok'

Flask アプリ

最後に Flask アプリを管理するモジュールを作成します
database.py に定義した SQLAlchemy のオブジェクトと Marshmallow のオブジェクトはここで app を使って Flask 上で扱えるようにします
少し気持ち悪いですがマイグレートの定義はここに記載しています (もしかするとこれも別にする方法がありそうですが)

from flask import Flask
from flask_migrate import Migrate
from my_app.database import db, ma
from my_app.lib import UserCRUD

app = Flask(__name__)
app.debug = True
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+mysqldb://root:@localhost/test?charset=utf8"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

db.init_app(app)
migrate = Migrate(app, db)
ma.init_app(app)

@app.route('/add')
def add():
    crud = UserCRUD()
    crud.add("hawksnowlog", 10)
    return 'ok'

マイグレートする

今回の構成であればマイグレートにも対応しています

  • FLASK_APP=my_app pipenv run flask db init
  • FLASK_APP=my_app pipenv run flask db migrate -m "Initial migrate"
  • FLASK_APP=my_app pipenv run flask db upgrade"

動作確認

マイグレートしてテーブルができたら動作確認してみましょう

  • FLASK_APP=my_app pipenv run lask run
  • curl 'localhost:5000/add

ちゃんとデータを挿入されているのが確認できると思います

最後に

Flask-SQLAlchemy で循環参照しないコツを紹介しました
公式のドキュメントのクイックスタートは簡単に書けるようになっていますがそのまま 1 つのモジュールで進めると大変なことになるので注意しましょう

おまけ: テストを書くには

テストを書く場合はテスト側でデモ用の app を作成し登録する必要があります
pytest の場合はこんな感じです

from flask import Flask
from my_app.database import db
from my_app.lib import UserCRUD

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+mysqldb://root:@localhost/test?charset=utf8"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db.init_app(app)
app.app_context().push()

def test_add():
    crud = UserCRUD()
    ret = crud.add("hawksnowlog_test", 99)
    assert ret == 'ok'
  • PYTHONPATH=./ pipenv run pytest test

上記だと普通にデータベースが起動していることが前提でかつ実際にデータも入ってしまうので mock する場合は monkeypatch を db.session.commit に対して当てれば OK です

from flask import Flask
from my_app.database import db
from my_app.lib import UserCRUD

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+mysqldb://root:@localhost/test?charset=utf8"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db.init_app(app)
app.app_context().push()

def test_add(monkeypatch):
    monkeypatch.setattr(db.session, "commit", lambda: None)
    crud = UserCRUD()
    ret = crud.add("hawksnowlog_test", 99)
    assert ret == 'ok'

参考サイト

2020年7月14日火曜日

Flask で SQLAlchemy を使ってみた (Flask-SQLAlchemy)

概要

前回 Flask-Migrate でデータベースをマイグレーションしてみました
今回はそのテーブルを使って Flask-SQLAlchemy を使ってデータの CRUD をしてみます

環境

  • macOS 10.15.5
  • MySQL 8.0.19
  • Python 3.8.3
  • Flask-SQLAlchemy 2.4.3

準備

必要なライブラリをインストールします
Flask-SQLAlchemy は MySQL に対して操作するので mysqlclient もインストールします
また MySQL から取得したデータをシリアライズするのに Flask-Marshmallow を使います
marshmallow-sqlalchemy は警告が出るのでインストールしているだけなので直接使うことはありません

  • pipenv install Flask-Marshmallow Flask-SQLAlchemy mysqlclient marshmallow-sqlalchemy

MySQL は事前に起動しておきます

  • brew services start mysql

なお test データベースの user というテーブルにはすでに以下のデータが入っていることを想定しています

mysql> mysql> select * from user;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | hawk |   10 |
|  2 | hawk |   20 |
+----+------+------+
2 rows in set (0.00 sec)

CRUD するモジュールの作成

各種 SQL を発行するモジュールを作成します
追加や削除に関しては db.session を使い取得する場合は User.query を使います
また更新の場合は一度対象のレコードを取り出しモデルにバインドしてから更新したいフィールドを書き換えて再度 db.session.add する感じになります

  • vim lib/__init__.py
from my_app import db, User, UserSchema

class CRUD():
  def add(self, name, age):
      user = User(name=name, age=age)
      db.session.add(user)
      db.session.commit()
      return 'ok'

  def select(self):
      return UserSchema(many=True).dump(User.query.all())

  def _select_one(self, id):
      return User.query.get(id)

  def delete(self, id):
      db.session.delete(self._select_one(id))
      db.session.commit()
      return 'ok'

  def update(self, id, name, age):
      user = self._select_one(id)
      user.name = name
      user.aget = age
      db.session.add(user)
      db.session.commit()
      return 'ok'

今回は 1 つのテーブルに対する操作になりますが複数のテーブルを跨いだリレーションなどを扱う場合ももう少し複雑になります

Flask アプリ側で CRUD をテストするルーティングの追加

Flask アプリの定義やデータベースのマイグレーション処理も含まれているので少し長いです

  • my_app/__init__.py
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from flask_migrate import Migrate

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+mysqldb://{}:{}@{}/{}?charset=utf8".format("root", "", "localhost", "test")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

db = SQLAlchemy(app)
migrate = Migrate(app, db)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128))
    age = db.Column(db.Integer)

ma = Marshmallow(app)
class UserSchema(ma.Schema):
    class Meta:
        fields = ("id", "name", "age")

@app.route('/add')
def add_user():
    name = request.args.get('name')
    age = request.args.get('age')
    crud = CRUD()
    return crud.add(name, age)

@app.route('/select')
def select_user():
    crud = CRUD()
    return jsonify(crud.select())

@app.route('/delete')
def delete_user():
    id = request.args.get('id')
    crud = CRUD()
    return crud.delete(id)

@app.route('/update')
def update_user():
    id = request.args.get('id')
    name = request.args.get('name')
    age = request.args.get('age')
    crud = CRUD()
    return crud.update(id, name, age)

from my_app.lib import CRUD # 循環参照になるので最後に import する

route に関しては各 CRUD 処理分定義しています
クエリストリングにしていますが好きな形式でリクエストを受け取れるようにして OK です
UserSchema クラスは SQLAlchemy で取得したデータを json にシリアライズするために追加したクラスです
あとは前回マイグレーションで作成した処理になります

動作確認

では動作確認しましょう
まずはアプリを起動します

  • FLASK_APP=my_app pipenv run flask run

アプリが起動したら API を叩いてみましょう

  • curl 'localhost:5000/add?name=test&age=30'
  • curl 'localhost:5000/select'
  • curl 'localhost:5000/delete?id=3'
  • curl 'localhost:5000/update?id=2&name=snowlog&age=99'

最後に

Flask-SQLAlchemy を使って Flask から MySQL に対して簡単な CRUD 処理を行ってみました
エラーハンドリングなど全くしていないのでプロダクトではちゃんと行うようにしてください
正規化をしまくっているデータベースだともっと複雑なクエリを発行することになると思いますが基本的な使い方は理解できるかなと思います

ちょっと書き方的に気になるのが Flask アプリ内にデータベースのモデルを追加しているところです
Flask-Migrate を使う上でそうしていますがもう少し工夫すれば別モジュールとして管理できるかもしれません

参考サイト