2021年7月8日木曜日

SQLAlchemy で JSON 型を扱う方法

SQLAlchemy で JSON 型を扱う方法

概要

MySQL の JSON 型を SQLAlchemy から操作する方法を紹介します テーブルの定義からデータの登録、取得、更新あたりを試してみました なお以下のサンプルの SQLAlchemy は Flask-SQLAlchemy 経由で操作しています

環境

  • macOS 11.4
  • Python 3.8.3
  • MySQL 8.0.25
  • SQLAlchemy 1.3.19

テーブル/カラム定義

db.JSON() を使います

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    profile = db.Column(db.JSON())

データの登録

今回は insert するためのクラスを作成しました

  • vim user_table.py
from schema import User

class UserTable():
    def insert(self, profile):
        p = User(profile=profile)
        db.session.add(p)
        db.session.commit()

これを使って以下のように登録できます Python の dict を使ってそのまま JSON 型のカラムにデータを保存することができます

from user_table import UserTable

user_table = UserTable()
profile = {"name": "hawksnowlog"}
user_table.insert(profile)

データの取得

今回は Mashmallow を使って JSON 型から Python の dict へシリアライズしてます 発行したクエリの結果をそのまま Mashmallow に渡せば OK です

from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
ma = Marshmallow()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    profile = db.Column(db.JSON())

class UserResponseSchema(ma.Schema):
    id = ma.String(attribute="id")
    profile = ma.Dict(attribute="profile")
  • vim user_table.py
from schema import User

class UserTable():
    def insert(self, profile):
        p = User(profile=profile)
        db.session.add(p)
        db.session.commit()

    def select_all(self):
        return User.query.filter().all()
from user_table import UserTable

user_table = UserTable()
records = parameter_table.select_all()
UserResponseSchema(many=True).dump(records)

データの更新

基本的には登録と同じです 一度レコードを取得してそのオブジェクトに対して更新をかけます

  • vim user_table.py
from schema import User

class UserTable():
    def insert(self, profile):
        p = User(profile=profile)
        db.session.add(p)
        db.session.commit()

    def select_all(self):
        return User.query.filter().all()

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

    def update(self, id, profile):
        p = self.select_one(id)
        p.profile = profile
        db.session.add(p)
        db.session.commit()
from user_table import UserTable

user_table = UserTable()
profile = {"name": "updated_hawksnowlog"}
user_table.update(profile)

最後に

基本的なクエリであれば特に詰まることはなくできると思います JSON_EXTRACT や JSON_VALUE を使う場合は今回は紹介していませんが少しハマるかもしれません (SQLAlchemy が対応しているかによる)

また JSON 型を使用する場合は MySQL のバージョンに気をつけましょう

Rack で起動するポートを指定する方法

Rack で起動するポートを指定する方法

概要

引数やコメント、Ruby スクリプト内で設定することができます

環境

  • macOS 11.4
  • Ruby 3.0.1p64
  • Sinatra 2.1.0

サンプルアプリ

以下のアプリを使ってい起動するポートをいろいろと変更してみます デフォルトだと 9292 ポートで起動します

app.rb

require 'sinatra/base'

class WebApp < Sinatra::Application  
  get '/' do
    'ok'
  end
end

config.ru

require './app'

run WebApp

引数で指定

起動する際の引数で指定できます

コメントで指定

現在は非推奨になっているようです 実行すると「Parsing options from the first comment line is deprecated!」が表示されます

#\ -p 1111
require './app'

run WebApp

スクリプト内で指定

run する際に Rack::Handler から run をすることで起動時の引数として指定できます こうすることでスクリプト内の変数を使って起動ポートを指定することができます

require './app'

port = ENV["PORT"] || 1111
Rack::Handler::Thin.run(WebApp, Port: port)

ただこの場合アプリ停止時に「to_app: missing run or map statement (RuntimeError)」が表示されてしまいます

2021年7月7日水曜日

GCP の Cloud Run を試してみた

GCP の Cloud Run を試してみた

概要

Cloud Run はコンテナ化したアプリをデプロイすることができるプラットフォームです コンテナ化さえしていれば何でもデプロイできるので便利です (ローカルパスなどをマウントしていない場合) 今回は簡単な Web アプリを作成してデプロイしてみました

環境

  • macOS 11.4
  • docker
  • Ruby 3.0.1p64
  • Cloud Run
  • Container Registry

サンプルアプリの作成

今回は Ruby アプリをデプロイしてみます まずはアプリを作成します

  • mkdir test
  • cd test
  • bundle init
  • vim Gemfile
gem "sinatra"
gem "thin"
  • bundle config path vendor
  • bundle install
  • bundle lock --add-platform x86_64-linux
  • vim app.rb
require 'sinatra/base'

class WebApp < Sinatra::Application
  get '/' do
    'ok'
  end
end
require './app'

run WebApp

サンプルアプリのビルドとプッシュ

次に docker を使ってアプリをビルドします gcloud コマンドに専用のビルドコマンドがあるのでそれを使うとレジストリの認証をしてプッシュまでしてくれます

  • vim Dockerfile
FROM ruby:3.0.0

WORKDIR /usr/src/app
COPY Gemfile Gemfile.lock ./
ENV BUNDLE_FROZEN=true
RUN gem install bundler && bundle install

COPY . ./

CMD ["bundle", "exec", "rackup", "config.ru", "-o", "0.0.0.0", "-p", "8080"]

デフォルトだと Cloud Run は 8080 でアプリがリッスンすることを想定しているので 8080 を指定します

  • vim .dockerignore
Dockerfile
.bundle/
vendor/

dockerignore も作成します ビルドは Cloud Build を使うため不要なファイルはビルドに含めないようにします

  • gcloud builds submit --tag us.gcr.io/PROJECT_ID/test

PROJECT_ID の部分は gcloud projects list で確認できます またプッシュ可能なレジストリのリージョンは「gcr.io」「us.gcr.io」「eu.gcr.io」「asia.gcr.io」があり今回は us.gcr.io を選択しています

これは Cloud Run と Registry のリージョンを同じにするためでありそうすることでデプロイ時のネットワーク転送料が無料になります

gcloud build コマンドを使うとクラウドビルドしてくれるのでローカルで docker が動作している必要はありません

GCP の管理画面からコンテナレジストリの一覧を確認するとちゃんとイメージがプッシュされているのが確認できると思います

サンプルアプリのデプロイ

イメージがプッシュできたら Cloud Run にデプロイしてみます 今回は UI からデプロイしてみます

サービスの作成

まずはサービスを作成します Cloud Run の画面から「サービス作成」をクリックします

そしてサービス名とリージョンを指定します 今回は us-central-1 を指定します

次に先程プッシュしたイメージを選択します

最後のアプリを起動させるトリガーを選択します 今回は Web アプリなので Ingress で「すべてのトラフィックを許可する」を選択します また認証は「未認証の呼び出しを許可」にチェックを入れます

これで「作成」をクリックするとアプリのデプロイが完了します

動作確認

デプロイが完了すると以下のようにアクセス可能な URL が表示されます (何度かデプロイミスしたのでエラーの履歴があります)

あとは表示されている URL にアクセスするとアプリが動作しているのが確認できると思います

クリーンアップ

プロジェクトを作成できるのであればプロジェクトごと削除しましょう

プロジェクトを削除できないのであれば Cloud Run 上のサービスと Registry 上にプッシュしたイメージを削除しましょう

おまけ: コマンドでデプロイする

UI でやった作業をコマンドで行うこともできます いくつか質問されるので答える必要がありますが認証の部分の設定になるので UI でデプロイしていればわかると思います

  • gcloud run deploy --image us.gcr.io/PROJECT_ID/test --platform managed

最後に

Cloud Run に Web アプリをデプロイしてみました CPU やメモリの量を調整したり起動トリガーなどもいろいろ変更できるのでかなり使えるサービスだと思います

アプリをコンテナ化しているのであればプッシュするだけで起動できるのでわざわざインスタンスを構築する必要もなくなります

またクラウドストレージにクラウドビルドのリソースも残っているのでそれらも削除しておきましょう

トラブルシューティング

Your bundle only supports platforms ["x86_64-darwin-20"] but your local platform is x86_64-linux. Add the current platform to the lockfile with `bundle lock --add-platform x86_64-linux` and try again.

が表示される場合は bundle lock --add-platform x86_64-linux してからビルド&プッシュしましょう クラウドビルドを使っているためプラットフォームが異なるのが原因です

Dockerfile の書き方にも少しくせがあります 自分が遭遇したエラーは

bundler: command not found: rackup
Install missing gem executables with `bundle install`

bundle config path vendor を Dockerfile から削除するとうまくパスが見えました

参考サイト

2021年7月6日火曜日

Firebase の File Storage の使い方

Firebase の File Storage の使い方

概要

Firebase の File Storage を使ってみました ファイルのダウンロードをする際に少し気をつけることがありました インストール方法や初期化の部分は省略しています

環境

  • macOS 11.4
  • Xcode 12.5.1
  • Firebase/Storage 7.11.0

ローカルへのファイルのダウンロード

ポイントはファイルを保存できるパスが決まっているという点です 自分のアプリ配下の Documents にしか保存できないので注意しましょう

自分の Documents 配下を参照するためのプロパティがちゃんと提供されているのでそれを使うのが無難です

import Firebase

let storage = Storage.storage()
let storageRef = storage.reference()
let name = "test.txt"
let testfileRef = storageRef.child(name)
let fileManager = FileManager.default
let articlePath = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
let localURL = articlePath.appendingPathComponent(name)

_ = testfileRef.write(toFile: localURL) { url, error in
    if let error = error {
        // firebase からファイルの取得失敗
        print(error)
    } else {
        // ファイルのダウンロードに成功
	print("success")
    }
}

メタ情報の参照

例えばファイルの md5 ハッシュを取得するには以下のようにします 取得可能なメタデータの一覧はこちらにあります

fndbfileRef.getMetadata { metadata, error in
    if let error = error {
        // firebase のメタデータの取得に失敗
        print(error)
    } else {
      	// メタデータの取得に成功
        print(metadata?.md5Hash)
    }
}

参考サイト

2021年7月5日月曜日

Swift5 でファイルとして保存した JSON 情報をシリアライズする方法

Swift5 でファイルとして保存した JSON 情報をシリアライズする方法

概要

JSON でローカルに保存したファイルを開いて構造体にシリアライズする方法です 今回はルートドキュメントが配列の場合をシリアライズする方法を紹介します

環境

  • macOS 11.4
  • Xcode 12.5.1

サンプルコード

test.json は配列内に JSON データ入っていることを想定しています

例えば以下のようなファイルとします

[
  {
    "title": "xxx",
    "content": "xxx",
  }
]

まず JSON 部分の構造体を作成します キー名と変数名を合わせるのがポイントです

struct ArticleEntity: Codable {
    var title: String
    var content: String
}

そして JSONDecoder.decode を使ってシリアライズします ファイルは Data オブジェクトとして渡ってくるのでそのまま渡します

この際にシリアライズするデータの構造体を渡します 今回は配列の中にデータがあるので [ArticleEntity].self という感じで配列で括ってから指定します こうすることで配列がルートドキュメントにある JSON でもシリアライズすることができます

let name = "test.json"
let fileManager = FileManager.default
let articlePath = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
let localURL = articlePath.appendingPathComponent(name)

let articlesData = try Data(contentsOf: localURL)
let articleEntities: [ArticleEntity] = try! JSONDecoder().decode([ArticleEntity].self, from: articlesData)
for articleEntity in articleEntities {
    print(articleEntity.title)
}

2021年7月2日金曜日

Google SpreadSheet でカーソルを移動する方法

Google SpreadSheet でカーソルを移動する方法

概要

例えばスプレッドシートを開いたときに特性のセルにカーソルを移動したい場合などに使えます

環境

  • macOS 11.4
  • Chrome 91.0.4472.106
  • Google SpreadSheet
  • Apps Script

サンプルコード

例えば一番左上にカーソルを合わせる場合は以下のような感じになります ポイントは Range に対して activate() する点です

function setCursorToStart() {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadsheet.getActiveSheet();
  var range = sheet.getRange("A1").activate();
  // var range = sheet.getRange(1, 1).activate(); こっちでも OK
}

ちょっと応用1: 起動時に移動させる

onOpen というイベントハンドラが用意されているのでそれを実行してあげます

function onOpen(e) {
  setCursorToStart();
  Browser.msgBox("Automatically moved cursor to today's cell.");
}

ダイアログはテストのために入れているので実際は消してください

ちょっと応用2: 特定の条件に合致したセルに移動する

例えば日付を管理しているカラムがある場合に今日の日付のセルに移動したりもできます

以下のサンプルでは A 列で日付を管理しています new Date() で今の時刻を取得できるのであとは年月日を照らし合わせているだけです

row と col が String になっているので parseInt で変換してから Range を取得しましょう

function moveCursorToToday() {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadsheet.getActiveSheet();
  var range = sheet.getRange("A:A");
  var a_values = range.getValues();
  var date = new Date();
  for (var row in a_values) {
    for (var col in a_values[row])
    if (checkToday(date, a_values[row][col])) {
      var range = sheet.getRange(parseInt(row) + 1, parseInt(col) + 1).activate();
      return;
    }
  }
}

function checkToday(day, oday) {
  if (day.getFullYear() == oday.getFullYear() && day.getMonth() == oday.getMonth() && day.getDate() == oday.getDate()) {
    return true
  }
  return false
}

参考サイト

2021年7月1日木曜日

TypeError: Cannot read property 'getBody' of null

TypeError: Cannot read property 'getBody' of null

概要

SpreadSheet でドキュメントオブジェクトを取得する際に発生しました 原因と対策を紹介します

環境

  • macOS 11.4
  • Chrome 91.0.4472.106
  • Google SpreadSheet
  • Apps Script

原因

SpreadSheet を扱おうとしているのに Documents を取得しようとしているのが原因です

対策

var doc = DocumentApp.getActiveDocument();

var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

SpreadSheet にはちゃんと SpreadSheet を扱うための関数があります サンプルに DocumentApp.getActiveDocument() を使うのが多いのでごっちになるのかもしれません