LoginSignup
13
18

More than 3 years have passed since last update.

Heroku、Flask、SQLAlchemyで掲示板を作る

Last updated at Posted at 2020-03-14

はじめに

今回、掲示板の作成について、以下の通り6つに分類にして記述した。
(1)環境構築
(2)csvで掲示板
(3)SQLとSQLAlchemyで掲示板
(4)PostgreSQLとSQLAlchemyで掲示板
(5)SQLAlchemyを使ってデータ操作
(6)Postgresqlを使ってデータ操作

(1)環境構築

デスクトップにディレクトリtestを作成。
test内に仮想環境を構築して起動。

python3 -m venv .
source bin/activate

必要なフレームワークとwebサーバーをインストール。

pip install flask
pip install gunicorn

(2)csvで掲示板

まず、ローカル環境でcsvを使って掲示板を作る。

①ディレクトリ構成

test
├app.py
├articles.csv
├Procfile
├requirements.txt
└templates
  ├index.html
  ├layout.html
  └index_result.html

②csvデータを用意する

articles.csvを作成し、分かりやすさの観点から、あらかじめ以下のデータを入力しておく。

たま,眠いにゃー
しろ,腹減ったにゃー
クロ,なんだか暖かいにゃー
たま,ぽえーぽえーぽえー
ぽんたん,トイレットペーパーがない
なおちん,チーン

③メインとなるapp.pyを作成する

app.py
#coding: utf-8
from flask import Flask,request,render_template
app = Flask(__name__)

@app.route('/')
def bbs():
    lines = []
    #with openしてcsvファイルを読み込む
    with open('articles.csv',encoding='utf-8') as f:
        lines = f.readlines() #readlinesはリスト形式でcsvの内容を返す
    #index.htmlに返す
    return render_template('index.html',lines=lines)

#postメソッドを受け取る
@app.route('/result',methods=['POST'])
def result():
    #requestでarticleとnameの値を取得する
    article = request.form['article']
    name = request.form['name']
    #csvファイルに上書きモードで書き込む
    with open('articles.csv','a',encoding='utf-8') as f:
        f.write(name + ',' + article + '\n')
    #index_result.htmlに返す
    return render_template('index_result.html',article=article,name=name)


if __name__ == '__main__':
    app.run(debug=False)

④掲示板本体とその他テンプレ

index.html
{% extends 'layout.html' %}
{% block content %}
    <h1> にゃん子掲示板</h1>
    <form action='/result' method='post'>
        <label for='name'>にゃん子の名前</label>
        <input type='text' name='name'>
        <p></p>
        <label for='article'>投稿</label>
        <input type='text' name='article'>

        <button type='subimit'>書き込む</button>
    </form>

    <p></p>
    <p></p>

    <table border=1>
        <tr><th>にゃん子の名前</th><th>投稿内容</th></tr>
        {% for line in lines: %}
        <!--columnという変数をセット(jinja2の変数セットにはsetが必要)  -->
        <!--splitを利用して,で分類する。splitはリストを返す  -->
            {% set column = line.rstrip().split(',') %}
            <tr><td>{{column[0]}}</td><td>{{column[1]}}</td></tr>
        {% endfor %}
    </table>

{% endblock %}
layout.html
<!DOCTYPE html>
<html lang='ja'>
  <head>
      <meta charset='utf-8'>
      <title>Nyanko BBS</title>
      <style>body{padding:10px;}</style>
  </head>
  <body>
    {% block content %}
    {% endblock %}
  </body>
</html>
index_result.html
{% extends 'layout.html' %}
{% block content %}
    <h1>にゃ-んと掲示板に書き込みました</h1>
    <p>{{name}}{{article}}</p>

    <!--formで/に戻る -->
    <form action='/' method='get'>
      <button type='submit'>戻る</button>
    </form>

{% endblock %}

⑤Herokuへデプロイする

ローカル環境でテストした後に、Herokuへデプロイする。
Herokuへのデプロイ詳細は以下の記事に書いた通りなので、エッセンスのみとし、詳細説明を省く。
Heroku、Flask、Python、Gitでアップロードする方法(その②)
Heroku、Flask、Python、Gitでアップロードする方法(その③)
Herokuにログインし、Heroku上にアプリを作成

heroku login

アプリ名はcat-bbsとした。

Heroku create cat-bbs

ディレクトリappを初期化して、

git init

Herokuとローカル環境を紐つけて、

heroku git:remote -a cat-bbs

ディレクトリappにrequirements.txtを作成して、

pip freeze > requirements.txt

ディレクトリapp内にProcfileを作成し、以下を入力。
この時、gの前はブランク一つ必要、また、:appの前のappは、app.pyのappという意味なので注意が必要(form.pyなら、form:app)

web: gunicorn app:app --log-file -

全てをaddして、

git add .

今回は、the-firstという名前でcommitして、

git commit -m'the-first'

Herokuにpushする。

git push heroku master

最後に、

heroku open

heroku openのコマンドを入力すると、ブラウザが立ち上がり以下が表示された。
スクリーンショット 2020-03-10 23.16.27.png
にゃん子の名前を”いわし”とし、投稿内容を”魚が大好き”と投稿すると、
スクリーンショット 2020-03-10 23.16.54.png
ちゃんと掲示板に書き込みされた。
herokuでは書き込みされたcsvは一定時間(30分)経過すると消えてしまうので、データベースの組み込みに取り掛かる。

(3)SQLとSQLAlchemyで掲示板

①ディレクトリ構成等

test
├app.py
├articles.csv
├Procfile
├requirements.txt
├assets
│ ├init.py   
│ ├database.py
│ └models.py
│
└templates
  ├index.html
  ├layout.html
  └index_result.html

SQLAlchemyとは、Pythonの中では最もよく利用されているORMの一つ。
最初にsqlite3のバージョン確認(Mac)と、sqlalchemyをインストールする。

sqlite3 --version
pip install sqlalchemy

また、app.pyから、database.pyやmodels.pyをモジュールとして読み込むために必要なファイルとして、 init.pyをassetsフォルダ内に作成する(アンダーバーがつくので注意)

touch __init__.py

②SQLAlchemyの初期設定

以下の2つのファイルをassetsフォルダ内に作成する。

database.py・・・sqliteやmysqlなど、どのデータベースを使うのかを定義するファイル
models.py・・・そのデータベースにどのような情報を入れるかを定義するファイル
まず、database.pyは以下の通り。

database.py
#coding: utf-8

#database.py/sqliteなど、どのデータベースを使うのか初期設定を扱うファイル
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session,sessionmaker
from sqlalchemy.ext.declarative import declarative_base

import datetime
import os

#data_dbという名前で、database.pyのある場所に(os.path.dirname(__file__))、絶対パスで(os.path.abspath)、data_dbを保存する
database_file = os.path.join(os.path.abspath(os.path.dirname(__file__)),'data.db')

#データベースsqliteを使って(engin)、database_fileに保存されているdata_dbを使う、またechoで実行の際にsqliteを出す(echo=True)
engine = create_engine('sqlite:///' + database_file,convert_unicode=True,echo=True)
db_session = scoped_session(
                sessionmaker(
                    autocommit = False,
                    autoflush = False,
                    bind = engine
                )
            )

#declarative_baseのインスタンス生成する
Base = declarative_base()
Base.query = db_session.query_property()


#データベースの初期化をする関数
def init_db():
    #assetsフォルダのmodelsをインポート
    import assets.models
    Base.metadata.create_all(bind=engine)

次に、models.pyは以下の通り。
ここで、投稿日時も掲示板に反映させるようした。

models.py
#coding: utf-8


from sqlalchemy import Column,Integer,String,Boolean,DateTime,Date,Text
from assets.database import Base
from datetime import datetime as dt

#データベースのテーブル情報
class Data(Base):
    #テーブルnameの設定,dataというnameに設定
    __tablename__ = "data"
    #Column情報を設定、uniqueはFalseとする(同じ値でも認めるという意味)
    #主キーは行を検索する時に必要、通常は設定しておく
    id = Column(Integer,primary_key=True)
    #nameは投稿者
    name = Column(Text,unique=False)
   #articleは投稿内容
    article = Column(Text,unique=False)
    #timestampは投稿日時
   timestamp = Column(DateTime,unique=False)

    #初期化する
    def __init__(self,name=None,article=None,timestamp=None):
        self.name = name
        self.article = article
        self.timestamp = timestamp

③app.pyを修正する

データベースの作成や削除等には以下2つが必要なためインポートする。
assetsフォルダのdatabaseモジュールから変数de_sessionのインポートと、assetsフォルダのmodelsモジュールから、Dataクラスをインポート。

from assets.database import db_session
from assets.models import Data

③−1データベースへの書き込み

index.htmlからarticle、nameの値を取得する処理が必要。また、それぞれの値をで取得時の(書き込み時の)日時をtoday()で取得し、today変数に代入する処理が必要。具体的には以下の通り。


article = request.form['article']
name = request.form['name']
today = datetime.datetime.today()

上記の内容をrowに格納し、db_sessionとde_commitでデータベースに書き込む処理が必要。具体的には以下の通り。

row = Data(name=name,article=article,timestamp=today)
db_session.add(row)
db_session.commit()

③−2データベースからの読み込み

データベースからデータを読み込むには、db_session.query(Data).all()で取得できる。
例えば、データベースの中の値を取り出すために以下のように記述すると、

db_session.query(Data.name,Data.article,Data.timestamp).all()

以下のようにリスト形式で出力される(分かりやすさの観点から、掲示板で何件か投稿し、データベースに保存された場合を想定)

('ミケ', '本日は晴れ', datetime.datetime(2020, 3, 13, 0, 7, 4, 828409)),
 ('しろ', '明日は雨だにゃー', datetime.datetime(2020, 3, 13, 0, 7, 4, 828409)),
 ('クロ', 'ぽかぽか', datetime.datetime(2020, 3, 13, 0, 7, 4, 828409)),
 ('ぽんたん', 'にゃーにゃーカラスは紙飛行機', datetime.datetime(2020, 3, 13, 0, 7, 4, 828409)),
 ('しろ', '腰が痛いにゃー', datetime.datetime(2020, 3, 13, 0, 7, 46, 513144)),
 ('ミケ', 'もんだろにゃ?', datetime.datetime(2020, 3, 13, 0, 8, 57, 193710)),
 ('クロ', 'ぽかぽか', datetime.datetime(2020, 3, 13, 0, 9, 42, 45228)),
 ('ミケ', '本日は曇り', datetime.datetime(2020, 3, 13, 0, 17, 13, 709028)),
 ('ブー太郎', '今日は一日雨かにゃー', datetime.datetime(2020, 3, 14, 13, 26, 29, 438012)),

index.htmlに読み込んだデータベースの内容を返す処理が必要。具体的には以下の通り。

data = db_session.query(Data.name,Data.article,Data.timestamp).all()
return render_template('index.html',data=data)

これまでの修正についてまとめると、app.py全体としては以下の通り。

app.py
#coding: utf-8
from flask import Flask,request,render_template
import datetime

#データベースを使うにあたり追加
from assets.database import db_session
from assets.models import Data

app = Flask(__name__)


@app.route('/')
def bbs():

    #データベースから読み込む
    data = db_session.query(Data.name,Data.article,Data.timestamp).all()

    #index.htmlに返す
    return render_template('index.html',data=data)


#postメソッドを受け取る
@app.route('/result',methods=['POST'])
def result():
    #requestでarticleとnameの値を取得する
    article = request.form['article']
    name = request.form['name']
    #today関数でpostメソッドを受け取った日時を変数に代入
    today = datetime.datetime.today()

    #index_resultからの情報をデータベースに書き込む
    row = Data(name=name,article=article,timestamp=today)
    db_session.add(row)
    db_session.commit()

    #index_result.htmlに返す
    return render_template('index_result.html',article=article,name=name)


if __name__ == '__main__':
    app.run(debug=False)

③−4(参考)データベースからの削除

参考として、読み込んだデータベースからの削除は以下の通り。
db_session.query(Data).allから削除したい項目を指定して(以下のケースは1番目の項目)、de_session.deleteを使う

#coding: utf-8

from assets.database import db_session
from assets.models import Data

def csv_sakujo():
    data = db_session.query(Data).all()
    datum = data[0]
    db_session.delete(datum)
    db_session.commit()

csv_sakujo()

③−5(参考)読み込んだデータベースをcsvに書き出し

参考として、読み込んだデータベースをcsvに書き出すファイルは以下の通り。

to_csv.py
#coding: utf-8

from assets.database import db_session
from assets.models import Data

#データを読み込む
def csv_kakikomi():
    data = db_session.query(Data.name,Data.article,Data.timestamp).all()
    print(data)
    #csvファイルに書き込みモードで書き込む#
    with open('articles2.csv','w',encoding='utf-8') as f:
        for i in data:
            f.write(str(i[0])+',')
            f.write(str(i[1])+',')
            f.write(str(i[2])+',' + '\n')

csv_kakikomi()

④index_html.pyを修正する

app.pyから送られてきたdataの値を表示する。
dataの値のうち、data[2]は現在日時であるが、投稿前はNoneの値があるため、if文でエラーにならないように設定。投稿後はdatatime型をstr型にstrftimeを用いて変換した上で表示。

index.html
{% extends 'layout.html' %}
{% block content %}
    <h1> にゃん子掲示板</h1>
    <form action='/result' method='post'>
        <label for='name'>にゃん子の名前</label>
        <input type='text' name='name'>
        <p></p>
        <label for='article'>投稿</label>
        <input type='text' name='article'>

        <button type='subimit'>書き込む</button>
    </form>

    <p></p>
    <p></p>

    <table border=1>
        <tr>
          <th>にゃん子の名前</th>
          <th>投稿内容</th>
          <th>投稿日時</th>
        </tr>
        {% for datum in data %}
             <tr>
              <td>{{datum[0]}}</td>
              <td>{{datum[1]}}</td>
              {% if datum[2] == None %}
                  <td>{{datum[2]}}</td>
              {% else %}
                  <td>{{datum[2].strftime('%Y年%m月%d日/%H時%M分%S秒')}}</td>
              {% endif %}
            </tr>
        {% endfor %}
    </table>

{% endblock %}

ここまでを、一度ローカル環境で正常に動くかどうかを試す。
スクリーンショット 2020-03-14 22.16.56.png
問題なく稼働するのを確認したら、次にHeokuへのデプロイと、HerokuのPostgreSQLを使う。

(4)PostgreSQLとSQLAlchemyで掲示板

Herokuへデプロイし、PostgreSQLを使う。

①環境準備

postgresqlをbrewを使ってインストールする。

brew install postgresql

次に postgresqlを使うためのpython用のドライバーとして、 psycopg2-binaryをインストールする。psycopg2をそのままインストールすると、なぜかエラーが出るため、psycopg2-binaryをインストール(原因不明)。

pip install  psycopg2-binary

次にdatabase.pyを修正するが、environというHeroku上の環境変数を見に行ってDATABASE_URLというデータベースを取得する処理を記述する。environには接続先のURLがセットされる。また、orをつけることで、ローカル環境上はsqliteをデータベースとして参照することとした。herokuに接続されている場合はpostgresqlのurlを参照して、接続されていない場合はsqlを参照に行くという格好。具体的には以下の通り。

engine = create_engine(os.environ.get('DATABASE_URL') or 'sqlite:///' + database_file,convert_unicode=True,echo=True)

修正後のapp.py全体は以下となる

database.py
#coding: utf-8

#database.py/sqliteなど、どのデータベースを使うのか初期設定を扱うファイル
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session,sessionmaker
from sqlalchemy.ext.declarative import declarative_base

import datetime
import os

database_file = os.path.join(os.path.abspath(os.path.dirname(__file__)),'data.db')

engine = create_engine(os.environ.get('DATABASE_URL') or 'sqlite:///' + database_file,convert_unicode=True,echo=True)
db_session = scoped_session(
                sessionmaker(
                    autocommit = False,
                    autoflush = False,
                    bind = engine
                )
            )

#declarative_baseのインスタンス生成する
Base = declarative_base()
Base.query = db_session.query_property()


#データベースの初期化をする
def init_db():
    #assetsフォルダのmodelsをインポート
    import assets.models
    Base.metadata.create_all(bind=engine)

②Herokuへデプロイする

Herokuへデプロイする。

heroku login

Herokuとローカル環境を紐つけて、

heroku git:remote -a cat-bbs

あらためて、ディレクトリappにrequirements.txtを作成する。
(psycopg2-binaryをインストールしたため、再度の作成が必要。)

pip freeze > requirements.txt

Prockfileは作成済みのため今回は触らず。

全てをaddして、

git add .

今回は、the-secondという名前でcommitして、

git commit -m'the-second'

Herokuにpushする。

git push heroku master

最後にheroku openする

herokuにデプロイする前に、データベースの初期化を行う。
heroku上でpythonを起動する(pythonモード)。

heroku run python

データベースを初期化する。
pythonモードで以下を記述。

from assets.database import init_db
init_db()

pythonモードを終了し、herokuをrestartして、openする。

heroku restart
heroku open

ブラウザで以下を確認して成功。
スクリーンショット 2020-03-14 22.58.37.png

(5)SQLAlchemyを使ってデータ操作

例として、データベースの1番上の項目を削除してみる(”クロ”)。
Heokuのpythonモード起動。

heroku run python

pythonモードで以下を記述

from assets.database import db_session
from assets.models import Data
data = db_session.query(Data).all()
datum = data[0]
db_session.delete(datum)
db_session.commit()

として、heroku openしてブラウザで確認すると、
スクリーンショット 2020-03-14 23.05.59.png
一番上の”クロ”が削除された。
(pythonモードは忘れずに終了させること)

(6)Postgresqlを使ってデータ操作

PostgreSQL をインストールすると、heroku pg コマンドで Heroku Postgres を操作できるようになる。
例えば以下を入力すると、インストールした Heroku Postgresの状況は以下のように確認できる。

heroku pg:info
=== DATABASE_URL
Plan:                  Hobby-dev
Status:                Available
Connections:           2/20
PG Version:            12.2
Created:               2020-03-14 04:53 UTC
Data Size:             8.2 MB
Tables:                1
Rows:                  3/10000 (In compliance)
Fork/Follow:           Unsupported
Rollback:              Unsupported
Continuous Protection: Off

hobby-dev プラン (無料枠) で、Status は Available (有効) 。

以下を入力すると、Heroku Postgresに接続できる。

heroku pg:psql

接続後はPostgreSQLのコマンドを使用
例えば、1番上の項目を削除してみる(”テスト”、”うんち”)。

テーブル一覧の表示のコマンド
\dt;
テーブル内のデータを一覧するコマンド
select * from data(テーブル名);

以下が出力される。


cat-bbs::DATABASE=> select * from data;
 id |    name    |      article       |         timestamp          
----+------------+--------------------+----------------------------
  3 | テスト     | うんち             | 2020-03-14 05:59:38.062361
  4 | プーさん   | なし               | 2020-03-14 15:14:12.453124
  5 | まちゃあき | それがどうした     | 2020-03-14 15:14:12.453124
  6 | どぶろっく | だから             | 2020-03-14 15:14:12.635542
  7 | 変だ       | なし               | 2020-03-14 15:14:12.635542
  8 | おう       | そだね             | 2020-03-14 15:14:12.453124
  9 | ニューあ   | ムーン             | 2020-03-14 15:32:49.082485
 10 | 女子       | 高め               | 2020-03-14 15:59:30.175208
 11 | ほんま     | 相談               | 2020-03-14 15:59:47.029891
 12 | え?       | フォンド           | 2020-03-14 16:15:58.35794
 13 | なおき     | テスト             | 2020-03-14 16:24:47.435301
 14 | ぽち       | ぽちでも猫だにゃん | 2020-03-14 22:52:41.633207
(12 rows)

次に、deleteで1番上の項目を削除する(”テスト”、”うんち”)。

delete from data(テーブル名) where id=3;

とすると、

cat-bbs::DATABASE=> select * From data;
 id |    name    |      article       |         timestamp          
----+------------+--------------------+----------------------------
  4 | プーさん   | なし               | 2020-03-14 15:14:12.453124
  5 | まちゃあき | それがどうした     | 2020-03-14 15:14:12.453124
  6 | どぶろっく | だから             | 2020-03-14 15:14:12.635542
  7 | 変だ       | なし               | 2020-03-14 15:14:12.635542
  8 | おう       | そだね             | 2020-03-14 15:14:12.453124
  9 | ニューあ   | ムーン             | 2020-03-14 15:32:49.082485
 10 | 女子       | 高め               | 2020-03-14 15:59:30.175208
 11 | ほんま     | 相談               | 2020-03-14 15:59:47.029891
 12 | え?       | フォンド           | 2020-03-14 16:15:58.35794
 13 | なおき     | テスト             | 2020-03-14 16:24:47.435301
 14 | ぽち       | ぽちでも猫だにゃん | 2020-03-14 22:52:41.633207
(11 rows)

削除した。
ブラウザで確認してもちゃんと削除されている。
スクリーンショット 2020-03-14 23.21.23.png

13
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
18