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_deleted
、on_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ファイルを更新した場合の動作。
この場合だと、以下の順番で実行される
- file './secure_test.log' deleted.
- file './secure_test.log' created.
- directory './' modified.
- 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が実行されなかったと思われる。
ファイルの変更をキャッチする場合は、delete
、create
の順番で実行されたことが確認されたら処理するようにするのが良い。
ファイルの生成時はcreate
が実行される
ファイルの削除時はdelete
が実行される
ファイルの編集時はdelete
、create
が実行される
ファイル名を変更した場合はmove
が実行される
DjangoのORMを使わないでクエリを実行する方法
ORM(object relational mapper)を使うべきか、使わざるべきかの議論を時々目にします。
Author.objects.all()
←こういうやつです。
今回は、DjangoでORMを使わずにクエリを書いてDBからデータを取得する方法を紹介します。
クエリの生成にはjinjasql
というpythonのライブラリを使用します。
ライブラリは↓のようにインストールが可能です。
pip install jinjasql
jinjasqlはjinja2というテンプレートエンジンを基にしているので、柔軟にクエリを書くことができます。
紹介
まずは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でquery
とbind_params
をDjangoで用意されている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/
を作ったときに得たノウハウをまとめています。
(ログイン不要で使えるポストイットのサービスです)
↓の流れで記事を書いています。
- はじめてのデプロイまで
- プロジェクト構造についての説明
はじめてのデプロイ
↑ のリンクでは、以下のことが書かれています。
- herokuの準備
- herokuの基本的なコマンド
- herokuにデプロイするためのDjangoの設定
※ 一部プロジェクト構造についての説明を参照している箇所があります。
プロジェクト構造についての説明
↑ のリンクでは、以下のことが書かれています。
- 作ったサービスでのプロジェクトの構造を紹介
- おまけ
- jinjasqlを使ったクエリ
- serializerの例
Djangoをherokuにデプロイする
herokuの用意
- アカウント作成
- アプリケーションを作成
開発環境を用意する
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>
これをやると、
ビルドパックが設定されていないというエラーが出た場合
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の構造は修正しすぎるとうまく動かなかったり、問題が起きたときにググっても解決方法が出てこなくなったりします。
今回紹介したレベルでの修正が開発しやすいのではないでしょうか。