utamaro’s blog

誰かの役に立つ情報を発信するブログ

mysqlclientでdict型のデータを取得する方法

通常、selectをしたときはtupple型で取得されます。

少し使いづらいので、dict型で取得できるようにします。

connectを取得して、cursorにDictCursorを設定するだけです。

from MySQLdb.cursors import DictCursor

self.connect = _mysql.connect(
    host=DATABASES['HOST'],
    user=DATABASES['USER'],
    password=DATABASES['PASSWORD'],
    database=DATABASES['NAME'],
    port=DATABASES['PORT'],
)
# ↓ を設定すると、self.cursor.fetchone等を使ったときにdict型で取得できます。
self.cursor = self.connect.cursor(DictCursor)

mysqlclientの簡易utilの紹介

pythonを使ってmysqlに対してselectを実行したり、insert、update、deleteをしたいときがあります。

そんなときに使える簡易的なutilを紹介します。

必要なライブラリは↓です。

mysqlclient
jinjasql

↓の簡易utilはjinjasqlを使用しています。

import MySQLdb as _mysql
from MySQLdb.cursors import DictCursor
from jinjasql import JinjaSql

DATABASES = {
    'NAME': 'Sample',
    'USER': 'root',
    'PASSWORD': 'admin',
    'HOST': '127.0.0.1',
    'PORT': 3306,
}


class MySqlUtil(object):

    source = ''
    data = None

    def __init__(self):
        pass

    def open(self):
        self.connect = _mysql.connect(
            host=DATABASES['HOST'],
            user=DATABASES['USER'],
            password=DATABASES['PASSWORD'],
            database=DATABASES['NAME'],
            port=DATABASES['PORT'],
        )
        # ↓ を設定すると、self.cursor.fetchone等を使ったときにdict型で取得できます。
        self.cursor = self.connect.cursor(DictCursor)

    def execute(self):
        if self.data is None:
            params = {}
            query = self.source
        else:
            query, params = self.create_query(self.source, self.data)
        self.open()
        self.cursor.execute(query, params)

    def select_all(self):
        datas = self.cursor.fetchall()
        return datas

    def select_one(self):
        data = self.cursor.fetchone()
        return data

    def close_cursor(self):
        self.cursor.close()

    def close_connect(self):
        self.connect.close()

    def close(self):
        self.close_cursor()
        self.close_connect()

    def create_query(self, source, datas):
        jsql = JinjaSql()
        query, params = jsql.prepare_query(source, datas)
        return query, params

    def rollback(self):
        self.connect.rollback()

    def commit(self):
        self.connect.commit()

使い方

MySqlUtilを継承したModelクラスを用意します。

あとは、インスタンスを生成して、データをセットし、関数を実行するだけです。

class SampleModel(MySqlUtil):
    id = None

    def select_id(self):
        self.source = """
        select 1
        where id = {{id}}
        """
        self.data = {
            'id': self.id,
        }
        self.execute()
        return self.select_one()

if __name__ == "__main__":
    sample = SampleModel()
    sample.id = 1
    sample.select_id()

watchdogを使ってファイルを監視する

カレントディレクトリの*.logに変更があった場合にMyHandler内のメソッドが実行される。

最初は.logファイルで修正したらon_modifiedが呼ばれると考えていたが、実際はon_deletedon_createdの順番で呼ばれていた。

class MyHandler(PatternMatchingEventHandler):
    def __init__(self, command, patterns):
        super(MyHandler, self).__init__(patterns=patterns)
        self.command = command

    def _run_command(self):
        subprocess.call([self.command, ])

    def on_moved(self, event):
        print("called on_moved")

    def on_created(self, event):
        print("called on_created")

    def on_deleted(self, event):
        print("called on_deleted")

    def on_modified(self, event):
        print("called on_modified")

def watch(path, command, extensions):
    event_handler = MyHandler(command, extensions)
    observer = Observer()
    observer.schedule(event_handler, path, recursive=True)
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()


if __name__ == '__main__':
    watch("./", "ls", ["*.log"])

念のため、別のイベントハンドラを使って試してみた。

同様に.logファイルを更新した場合の動作。

この場合だと、以下の順番で実行される

  1. file './secure_test.log' deleted.
  2. file './secure_test.log' created.
  3. directory './' modified.
  4. directory './.idea' modified.
class EventHandler(FileSystemEventHandler):
    def on_created(self, event):
        if event.is_directory:
            print('directory \'{0}\' created.'.format(event.src_path))
        else:
            print('file \'{0}\' created.'.format(event.src_path))

    def on_modified(self, event):
        if event.is_directory:
            print('directory \'{0}\' modified.'.format(event.src_path))
        else:
            print('file \'{0}\' modified.'.format(event.src_path))

    def on_deleted(self, event):
        if event.is_directory:
            print('directory \'{0}\' deleted.'.format(event.src_path))
        else:
            print('file \'{0}\' deleted.'.format(event.src_path))

def watch():
    event_handler = EventHandler()
    observer = Observer()
    observer.schedule(event_handler, "./")
    observer.start()

    try:
        while True:
            time.sleep(10)
    except (Exception, KeyboardInterrupt):
        observer.stop()
    observer.join()


if __name__ == '__main__':
    watch()

まとめ

どうやら、modifiedが実行されるのはディレクトリに変更があった場合に限るらしい

最初のプログラムではカレントディレクトリ以下の.logファイルのみを見ているため、modifiedが実行されなかったと思われる。

ファイルの変更をキャッチする場合は、deletecreateの順番で実行されたことが確認されたら処理するようにするのが良い。

ファイルの生成時はcreateが実行される

ファイルの削除時はdeleteが実行される

ファイルの編集時はdeletecreateが実行される

ファイル名を変更した場合はmoveが実行される

DjangoのORMを使わないでクエリを実行する方法

ORM(object relational mapper)を使うべきか、使わざるべきかの議論を時々目にします。

Author.objects.all()←こういうやつです。

今回は、DjangoでORMを使わずにクエリを書いてDBからデータを取得する方法を紹介します。

クエリの生成にはjinjasqlというpythonのライブラリを使用します。

ライブラリは↓のようにインストールが可能です。

pip install jinjasql

jinjasqlはjinja2というテンプレートエンジンを基にしているので、柔軟にクエリを書くことができます。

github.com

紹介

まずはModelについてです。

class Data(models.Model):

    title = models.TextField(max_length=400)
    is_deleted = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    modified_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'card'

    def select_data_list(self, limit):
        template, data = select_data_list_template(limit)
        query, bind_params = j.prepare_query(template, data)
        datas = Data.objects.raw(query, bind_params)
        return list(datas)

このモデル(Data)を使って、dataテーブルからデータを取得します。

select_data_listメソッドの中身に注目してください。

template, data = select_data_list_template(limit)  # 1
query, bind_params = j.prepare_query(template, data)  # 2
datas = Data.objects.raw(query, bind_params)  # 3

1でクエリのもととなる文字列と、データを用意します。

1の関数は↓のように作成します。

def select_data_list_template(limit):
    template = """
    select
        id,
        title,
        is_deleted,
        created_at
    from
        data
    order by created_at desc
    limit {{limit}}
    """
    data = {
        'limit': limit,
    }
    return template, data

jinjasqlではjinja2と同じようにfor文を使ったり、if文を使ったりできます。

例ではlimitの指定しかしていませんが、「ある値の場合には条件1を、別の値の場合は条件2をwhereに設定する」といったことができます。

これはspringというjavaフレームワークで使えるmybatisと同じようなことができるということです。

次に、2でjinjasqlというライブラリを使ってDjangoで使えるクエリを生成します。

queryには↓の文字列が作成されます。

select
    id,
    title,
    is_deleted,
    created_at
from
    data
order by card.created_at desc
limit %s

bind_paramsには↓のオブジェクトが作成されます。

odict_values([30])

最後に、3でquerybind_paramsDjangoで用意されているraw関数を使って実行します。

取得されるデータはlist化する必要があるので少し面倒ですが、複雑なクエリをORMを使って書くよりは面倒ではないと思います。

Djangoのtemplate内でurlの逆引きをする方法

逆引きをやることで、何がいいのか。

やらなくてもできますが、べた書きする必要があります。

例えば、anchorタグのhref属性に/api/hoge/delete/{{id}}と書いたりすることになります。

複数の場所で↑のように書いてた場合は修正が大変になります。

あと、templateがスッキリします。

url.pyに以下のように書かれているとします。

urlpatterns = [
    path('admin/', admin.site.urls),
    path('v1/<int:id1>', TopView1.as_view(), name='top1'),  # pattern1
    path('v2/<int:id1>/<int:id2>', TopView2.as_view(), name='top2'),  # pattern2
    path('v3/<int:id1>_<int:id2>', TopView3.as_view(), name='top3'),  # pattern3
]

pattern1 ~ pattern3に関して、どのように書くか。

viewでは↓のように、kwargsから値(id)を取得できます。

# TopView1, TopView2, TopView3
class TopView(TemplateView):
    template_name = 'top.html'

    def get(self, request, *args, **kwargs):
        context = {
            'id': kwargs['id'],  # id1, id2, 
        }
        return self.render_to_response(context)

pattern1の場合

↑のpathの場合、/v1/1234でGETリクエストが届いた場合、TopViewのgetメソッドが実行されます。

template(htmlファイル)側で、anchorタグをクリックするとv1/1234のページが表示されるのを想定します。

<a href="{% url 'top1' 1234 %}">
    トップページへ
</a>

すると、/v1/1234に変換されます。

pattern2の場合

次に、v2の方の場合の説明です。

v2は/で区切られていて、さらに値を2つ渡す必要があります。

こちらも簡単です。

<a href="{% url 'top2' 1234 6789 %}">
    トップページへ
</a>

すると、/v2/1234/5678に変換されます。

自分で/で区切り必要がないので凄く見やすいです。

pattern3の場合

v3は/で区切られていなくて、2つの値を_で連結しています。

こちらも簡単です。

<a href="{% url 'top2' 1234 6789 %}">
    トップページへ
</a>

すると、/v2/1234_5678に変換されます。

自分で_で連結する必要がないので凄く助かります。

Django+herokuで無料サービスを作る

Djangoとherokuを使って費用0(初期投資を含めません)でサービスを公開するところまで解説します。

  • サーバ代0円
  • 期間はだいたい3日

この記事は

https://sticky-cards.herokuapp.com/

を作ったときに得たノウハウをまとめています。
(ログイン不要で使えるポストイットのサービスです)

↓の流れで記事を書いています。

  • はじめてのデプロイまで
  • プロジェクト構造についての説明

はじめてのデプロイ

utamaro.hatenablog.jp

↑ のリンクでは、以下のことが書かれています。

  • herokuの準備
  • herokuの基本的なコマンド
  • herokuにデプロイするためのDjangoの設定

※ 一部プロジェクト構造についての説明を参照している箇所があります。

プロジェクト構造についての説明

utamaro.hatenablog.jp

↑ のリンクでは、以下のことが書かれています。

  • 作ったサービスでのプロジェクトの構造を紹介
  • おまけ
    • jinjasqlを使ったクエリ
    • serializerの例

Djangoをherokuにデプロイする

herokuの用意

  • アカウント作成 f:id:miyaji-y26:20180926003338p:plain
  • アプリケーションを作成 f:id:miyaji-y26:20180926003406p:plain

f:id:miyaji-y26:20180926003422p:plain

開発環境を用意する

macを対象にしています。
brewをインストールしていることが前提です。

herokuをインストールする

brew install heroku

以上です。

herokuのコマンド

ログインする

heroku login

herokuのアプリ一覧を取得する

heroku apps

gitを使ってデプロイするための準備をする

heroku git:remote -a sample

これをすると、git remote add origin <url>を実行したような動作をする。

pushする際は↓のようにする。

git push heroku master

実行すると、herokuでビルドされる。

herokuでログを確認する

heroku logs --tail

herokuでdynoを起動する

heroku ps:scale web=1

停止する場合は0にセットする。

webというのは、Procfileに書いているwebに該当する。

web: gunicorn DjangoForHeroku.wsgi

herokuの再起動

heroku restart

herokuのdynoをstop

heroku ps:stop <web>

これをやると、がidle状態になる。 停止するわけではないため、dynoを止める場合はscaleを使用すること。

ビルドパックが設定されていないというエラーが出た場合

heroku buildpacks:set heroku/python

↑はpythonのビルドパックを設定している。 使えるビルドパックは(https://devcenter.heroku.com/articles/buildpacks)にかかれています。

ローカルで動作確認する場合

heroku local

Djangoの場合は事前にcollectstaticを実行しておくこと。
(herokuのリモートで自動実行されているものを、手動でやる必要がある)

herokuのpostgresqlに接続する

heroku pg:psql

Djangoをherokuにデプロイ

  • Djangoのベースとなるアプリを作る
    • django-admin startproject DjangoForHeroku .で十分
    • ライブラリをインストールしたり
    • heroku用の設定を書いたり

プロジェクトを作成する

django-admin startproject DjangoForHeroku .

ライブラリをインストールする。

pip listで表示されたものをすべて載せています)

certifi (2018.8.24)
chardet (3.0.4)
defusedxml (0.5.0)
dj-database-url (0.5.0)
Django (2.1)
djangorestframework (3.8.2)
gunicorn (19.9.0)
idna (2.7)
Jinja2 (2.10)
jinjasql (0.1.7)
MarkupSafe (1.0)
mysqlclient (1.3.13)
pip (9.0.1)
psycopg2 (2.7.5)
psycopg2-binary (2.7.5)
python3-openid (3.1.0)
pytz (2018.5)
requests (2.19.1)
setuptools (28.8.0)
six (1.11.0)
urllib3 (1.23)
whitenoise (4.1)

インストールが完了したらheroku用の設定ファイルを用意します。

また、Djangoの構造については↓を参考にしてください。 utamaro.hatenablog.jp

herokuに必要なファイルを作成します。

manage.pyがあるディレクトリに作成します。

touch Procfile runtime.txt

Procfile

release: python manage.py migrate --settings=DjangoForHeroku.settings.product
web: gunicorn DjangoForHeroku.wsgi

webというプロセスを、gunicornを使って、○.wsgiを実行する。

あとから知ったのですが、release phaseというものがあり、deploy時のコマンドを設定できるみたいです。

これを知るまではheroku run python ...ryというように実行してました。

runtime.txt

python-3.6.6

settings.pyを編集する。

settingsパッケージを作成して、以下の構造にします。

settings
├── __init__.py
├── base.py
├── local.py
└── product.py

検索してヒットする情報ではsettings.py上でlocal-setting.pyがある場合に~といったコードを書いているのを見かけます。

セッティングファイルは分けておいたほうが何かと便利なので、分けるのがおすすめです。

base.pyを編集します。

DEBUG = False

ALLOWED_HOSTS = ['*']
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',  # ← を追加します。
    'django.contrib.sessions.middleware.SessionMiddleware',
    # ... ry
]

# ↓ をコメントアウトします。消してもいいです。この設定は、local.pyとproduct.pyに書きます。
# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.postgresql_psycopg2',
#         'NAME': 'database',
#         'USER': 'root',
#         'PASSWORD': 'pass',
#         'HOST': '127.0.0.1',
#         'PORT': '5432',
#         'AUTOCOMMIT': True,
#     }
# }

# ↓ /static/css/A.cssとか、/static/js/A.jsのように読み込まれます。
STATIC_URL = '/static/'

# collectstaticを実行したとき、静的ファイルがstaticfilesにまとめられます。
# herokuでビルドする際にデフォルトでcollectstaticが実行されます。
STATIC_ROOT = 'staticfiles'

# 自分で作った静的ファイルのディレクトリをSTATIC_ROOTにまとめたいときに設定します。
# ↓の場合だとassets/static以下のファイルがstaticfilesにまとめられます。
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "assets/static/"),
)

# httpをhttpsにリダイレクトさせる設定です。
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

local.pyを編集します。

from DjangoForHeroku.settings.base import *

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'sample',
        'USER': 'sample_admin',
        'PASSWORD': 'sample_password',
        'HOST': '127.0.0.1',
        'PORT': '5432',
        'AUTOCOMMIT': True,
    }
}

DEBUG = True

ALLOWED_HOSTS = []

product.pyを編集します。

from DjangoForHeroku.settings.base import *
import dj_database_url

# herokuの環境変数でDATABASEが更新される。
db_from_env = dj_database_url.config()
DATABASES['default'].update(db_from_env)

wsgi.pyを編集する

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DjangoForHeroku.settings.product')

application = get_wsgi_application()

Procfileを作成したときに↓のように書いたと思います。

web: gunicorn DjangoForHeroku.wsgi

このときのwsgiが対象のファイルです。

requirements.txtを作成する

初めてやったときに↓のコマンドを実行してなくて、ビルドに失敗しました。

pip freeze > requirements.txt

ちなみにですが、pip freeze > rpip.txtもだめでした。

requirements.txtと打つのが面倒で、rpip.txtと省略してたのがダメみたいです。

manage.pyを編集する

↓で、起動時にはbaseが読み込まれます。

if __name__ == '__main__':
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DjangoForHeroku.settings.base')
    try:
        from django.core.management import execute_from_command_line
    # ... ry

wsgiで実行される際は、productで上書きされます。

.gitignoreを作成する

余分なファイルはcommitに含めないようにします。

.idea
.python-version
*.egg-info
*.pot
*.py[co]
__pycache__
MANIFEST
*/django.log
staticfiles

これで準備完了です。

(思い出しながら記事を書いているので、もしかしたらエラーが出るかもしれません。エラーが出たらログを確認してみてくださいmm)

はじめてのデプロイ

git add ./
git commit -m "first deploy."
git push heroku master

以上です。

openコマンドを使って、動作を確認してみましょう。

デフォルトのブラウザが開かれます。

heroku open

Djangoのプロジェクト構造について

Djangoのプロジェクト構造について紹介します。

一般的な(ドキュメントに書いてあるような)構造とは少し異なります。

まずは全体の構造を紹介します。

./
├── Procfile  <== heroku用
├── README.md
├── app
│   ├── __init__.py
│   └── card
│       ├── __init__.py
│       ├── admin.py
│       ├── apps.py
│       ├── migrations
│       │   ├── 0001_initial.py
│       │   └── __init__.py
│       ├── models.py
│       ├── query.py
│       ├── serializer.py
│       ├── tests.py
│       └── views.py
├── manage.py
├── package-lock.json
├── requirements.txt
├── runtime.txt  <== heroku用
├── DjangoForHeroku
│   ├── __init__.py
│   ├── assets
│   │   └── static
│   │       ├── css
│   │       │   ├── card.css
│   │       │   ├── reset.css
│   │       │   └── top.css
│   │       └── js
│   │           └── card.js
│   ├── django.log
│   ├── settings
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── local.py
│   │   └── product.py
│   ├── templates
│   │   └── top.html
│   ├── urls.py
│   └── wsgi.py
├── staticfiles
│   └── >> 省略
└── utils
    ├── CommonResponse.py
    └── __init__.py

Djangoのアプリケーションでは、↓のように、1階層目にアプリケーションが配置されるのが一般的です。

project:
    ├─ application:
    │    ├─ view.py
    │    └─ model.py
    ├─ application:
    │    └─ view.py
    └─ project:
    │    ├─ urls.py
    │    └─ settings.py
    └─ manage.py

ですが↑のような構造にした場合、applicationの数が多くなると横に広くなってしまうので、見通しが悪くなってしまいます。

そこで、applicationを一つにまとめるappというパッケージを作成して管理することにしました。 ↓の部分です。

├── app
│   ├── __init__.py
│   └── card
│       ├── __init__.py
│       ├── admin.py
│       ├── apps.py
│       ├── migrations
│       │   ├── 0001_initial.py
│       │   └── __init__.py
│       ├── models.py
│       ├── query.py
│       ├── serializer.py
│       ├── tests.py
│       └── views.py

ただし、djangoで用意されているstartappを実行するのが少し面倒です。

↓のように一度appに移動してからコマンドを実行する必要があります。

cd app
../manage.py startapp card

※ 移動せずに実行すると↓のように怒られます。

CommandError: 'app/hoge' is not a valid app name. Please make sure the name is a valid identifier.

また、settings/base.pyには以下のように書いています。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
]

INSTALLED_APPS += list(map(lambda x: 'app.' + x, [
    'card',
]))

いちいちapp.Aとかapp.Bと書くのが美しくないなと思って、lambdaを使ってます。

これはこれでイケてない気がしますが。。

おまけ

お気づきの方がいらっしゃるかと思いますが、startappで作成されるファイル以外も2つ追加しています。 - query.py - serializer.py

それぞれのファイルの役割を説明します。

query.pyについて

読んで時の如く、クエリをまとめたファイルです。

ファイルの中身は↓のようなことを書いています。

def select_card(card_id):
    template = """
    select
        id
    from
        card
    where
        id = {{card_id}}
    """
    data = {
        'card_id': card_id,
    }
    return template, data

↑の例だとORMを書いたほうが簡単なのですが、joinやwhereが複雑だった場合にはクエリを書いたほうが良いと考えています。

また、createやinsert、deleteなどもORMを使ったほうが簡単と感じました。

ちなみに、クエリの生成ですが、jinjasqlを使っています。

jinjasqlを使った場合は、↓のように書きます。

j = JinjaSql()
template, data = select_card(card_id)
query, bind_params = j.prepare_query(template, data)
cards = Card.objects.raw(query, bind_params)
return list(cards)

複雑なクエリをORMを使って書いて、生成されたクエリを確認して、思ってたクエリと違ってて、悩んで、ググって、ヒットしなくて、詰まるよりは↑のほうが簡単です(実体験)

serializer.py

このファイルはdjangorestframeworkように作りました。

中はこのようになっています。(例)

class DataUpdateSerializer(serializers.Serializer):
    id = serializers.IntegerField(required=True)
    uuid = serializers.UUIDField(required=True)
    title = serializers.CharField(max_length=125, required=True, allow_blank=False)
    content = serializers.CharField(allow_blank=True)

少しトリッキーな書き方を思いついたので、ついでに紹介します。

リアライザを使ってリクエストパラメータのバリデーションをしようとしたときがありました。

デフォルト値をハードコーディングすると、後々の修正が大変になるので、enumで管理したいと考えました。 (条件とデフォルト値を同時にもつフィールドが欲しかった)

というわけで、↓のようなシリアライザを作りました。

class DataListSerializer(serializers.Serializer):
    class SortEnum(Enum):
        SORT_DIRECTION = (r"^(asc|desc)?$", 'asc')

        def __new__(cls, _pattern, _default):
            obj = object.__new__(cls)
            obj.pattern = re.compile(_pattern)
            obj.default = _default
            return obj

    sort_direction = serializers.RegexField(
        regex=SortEnum.SORT_DIRECTION.pattern, 
        default=SortEnum.SORT_DIRECTION.default
    )

他のシリアライザでも使う場合、SortEnumを外に出しても良いと思います。

ただし、複数のシリアライザで共有している場合、修正時の影響が大きくなるので気をつけなければなりません。

まとめ

Djangoの構造は修正しすぎるとうまく動かなかったり、問題が起きたときにググっても解決方法が出てこなくなったりします。

今回紹介したレベルでの修正が開発しやすいのではないでしょうか。