【2021Q4・エンジニア合宿報告】graphene-django (GraphQL) 使ってみた

Naohiro Kaide
MICIN Developers
Published in
14 min readJan 20, 2022

MICINのデータデータソリューション部の kaide です。データソリューション部は医療データを機械学習などで解析・活用することを主な業務としている部署です。今回の投稿では、MICIN開発合宿のときにやったことについて共有しようかと思います。(機械学習関連の話ではないです。。)

2021年12月3~4日に、MICIN開発合宿が開催されました。MICINのアプリケーションエンジニアだけでなく、デザイナーや機械学習エンジニアも参加しています。今回は開発合宿と言ってもどこかに泊まるわけではなく、2日間会社に出社できる人はして、しない人はオンラインでという、オンラインとオフラインのハイブリッドで開催されました。

この時期はコロナも落ち着いており、自分は出社してイベントに参加しました。

開発合宿でどんなことをやった?

大まかには、チームごとに好きなテーマをそれぞれ選んで開発し、最終日に結果報告するという流れです。開催前に社内 slack で需要がありそうなサービス案を募ったりもしていたので、そこからテーマを選んだチームが多かったです。

また、チーム内やチーム間の交流も深める目的で近くの飲食店にランチに行ったり夜にデリバリーのお寿司を食べたりなど、普段関わらないメンバーとも色々コミュニケーションできる機会でもありました。自分としては、デザイナーの方からUXの話を詳しく聞けたのが大変勉強になりましたね。

自分のチームのテーマ

以下の社員要望から、今回自分のチームでは、社内の書籍を管理するサービスについて開発することにしました。

[要望]
会社が所有している本の管理・貸し出しツールがあるといなと思いました!
技術本などのリストはあるかと社員から質問いただいたのですが、slackで検索するというイマイチな感じしかなく管理にも手が回せていないので・・!
あわせてそのツールを利用してコミュニケーションが取りやすい感じだといいなと思いました。(なんとなくイメージ、XXさんのおすすめの一冊とか、一番MICINで借りられている本とかそういったタグ付けがあるとコミュニケーション取りやすいかも!?)

上の要望とあわせて、自分たちでもサービスのゴールについて色々議論しましたが、今回開発が2日間ということもあり、今回の要求は以下に限定することにしました。

要求

  • まずは、現状の本が何があるかわからない状況を改善したい → 現状会社にある本を登録しやすくして、閲覧できるようにしたい。

そして、要件は以下とし、開発に着手することにしました。

要件

  • 本の CRUD
  • 本の検索が可能
  • 本の登録をバーコードを使って、スムーズに登録

自分は主に back-end を担当することになったのですが、自分の興味もあり、アプリケーションエンジニアの協力もいただけるということで、RESTful API ではなく、GraphQL で実装することにしました。

簡単なGraphQLの紹介

詳しい説明は Wikipedia の GraphQL(グラフQL) に記載してありますが、大雑把に紹介すると、

  • GraphQLとは、Web API のために作られたクエリ言語であり、既存のデータでクエリを実行するためのランタイム
  • 様々な処理を一つのエンドポイントからリクエストできる
  • オペレーションには、「(1)query: データ取得, (2)mutation: データ作成・変更・削除, (3)subscription: 変更監視」がある
  • 複雑なデータ同士の関係をグラフ構造で管理できる
  • クエリの記述により、レスポンス構成を動的に変更できる

との理解をしています。間違ってたり、表現おかしいようでしたら、コメントください。

開発内容

私たちのシステムの大雑把な構成は以下の図のようになります。front-end は Next.js, Apollo, Chakra UI を用いて実装されたようです。

システム構成図

システム構成

国立国会図書館サーチAPIは、バーコードのISBNを使って本の情報を取得するために利用しています。Amazon の Product Advertising API の利用も考えたのですが、パッと利用できるこちらを選択しました。

参考: Qiita記事 書籍検索に使える登録不要APIちゃんはちょっと足りない

環境構築

barcode reader には pyzbar を利用しているので、それに必要なソフトウェアもinstallします。

$ pip install -r requirements.txt# Mac での install
$ brew install zbar
# Ubuntu での install
$ sudo apt-get install libzbar0
  • requirements.txt
# --------- graphene-django --------- #
Django==3.1.2
django-cors-headers==3.5.0
graphene-django==2.13.0
django-filter==2.4.0
django-graphql-jwt==0.3.1
graphene-file-upload
PyJWT==1.7.1
# --------- barcode reader --------- #
pyzbar
pillow
requests

GraphQL Serverの実装

本のCRUD と検索については割愛して、「本の登録をバーコードを使って、スムーズに登録」に限定して書こうかと思います。

バーコード登録の流れとしては、「(1)ブラウザからバーコードの画像が post する (2) GraphQL Server側で zbarを使って ISBNを読み取る (3) そのISBN を国立国会図書館サーチAPI の書籍検索にリクエストして、書籍情報を取得する」となっています。

image の post は、graphene_file_upload というライブラリを利用して mutation の一つとして実装しました。

  • directory の構成
.
├── README.md
├── api
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations/
│ ├── models.py
│ ├── schema.py
│ ├── service
│ │ ├── __init__.py
│ │ ├── barcode_reader.py
│ │ └── fetch_book_info.py
│ ├── tests.py
│ └── views.py
├── app
│ ├── __init__.py
│ ├── asgi.py
│ ├── schema.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── db.sqlite3
├── manage.py
├── requirements.txt
└── schema.graphql
  • api/model.py
from django.db import models
class Books(models.Model):
name = models.CharField(max_length=255)
author = models.CharField(max_length=255)
amazon_url = models.URLField(max_length=200, null=True, blank=True)
description = models.TextField(null=True, blank=True)
image_url = models.URLField(max_length=200, null=True, blank=True)
published_year = models.IntegerField(null=True, blank=True)
active = models.BooleanField(default=True, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
  • api/schema.py の一部部分
from typing import castimport graphene
import graphene_django
import graphql_relay
import PIL
from django.core import exceptions
from graphene import relay
from graphene_django import filter
from graphene_file_upload import scalars
from api import models
from api.service import barcode_reader
from api.service import fetch_book_info
class BarcodeReadMutation(graphene.Mutation):
class Arguments:
file = scalars.Upload(required=True)
success = graphene.Boolean()
isbn = graphene.String()
name = graphene.String()
author = graphene.String()
def mutate(self, info, file, **kwargs):
# do something with your file
if not file.content_type.startswith("image/"):
raise exceptions.ValidationError(f"File '{file.filename}' is not an image.")
contents = file.read()
contents_bytes = cast(bytes, contents)
pil_image = PIL.Image.open(io.BytesIO(contents_bytes))
results_list = barcode_reader.qr_parser(pil_image)
if len(results_list) == 2 and results_list[0].startswith("97"):
# ISBNの接頭辞: 「書籍」を表す3桁の番号。「978」と「979」があり、日本は「978」。
isbn = results_list[0]
info = fetch_book_info.fetch_info_from_jpn_library_api(isbn)
name = info.get("name")
author = info.get("author")
else:
isbn = None
return BarcodeReadMutation(success=True, isbn=isbn, name=name, author=author)class Mutation(graphene.AbstractType):
...
read_barcode = BarcodeReadMutation.Field()
  • app/urls.py
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from graphene_file_upload.django import FileUploadGraphQLView
from app import schema# from graphene_django import viewsurlpatterns = [
path(
"graphql",
csrf_exempt(FileUploadGraphQLView.as_view(graphiql=True, schema=schema.schema)),
),
]
  • api/service/barcode_reader.py
import mathfrom PIL import Image
from pyzbar import pyzbar
THRES_COS = math.sqrt(1 / 2)def get_sort_func(qrcode):
poly = qrcode.polygon
angle = math.atan2(poly[1].y - poly[0].y, poly[1].x - poly[0].x)
rotate_angle = angle - math.pi / 2
rotate_angle_cos = math.cos(rotate_angle)
rotate_angle_sin = math.sin(rotate_angle)
if THRES_COS < rotate_angle_cos <= 1:
# orientation = TOP
sort_func = lambda qr_code_data: qr_code_data.polygon[0].x
elif -THRES_COS <= rotate_angle_cos <= THRES_COS:
if rotate_angle_sin < 0:
# orientation = LEFT
sort_func = lambda qr_code_data: -qr_code_data.polygon[0].y
else:
# orientation = RIGHT
sort_func = lambda qr_code_data: qr_code_data.polygon[0].y
else:
# orientation = BOTTOM
sort_func = lambda qr_code_data: -qr_code_data.polygon[0].x
return sort_funcdef qr_parser(pil_image: Image, encoding="utf-8"):
decoded_data = pyzbar.decode(pil_image)
if decoded_data:
sort_func = get_sort_func(decoded_data[0])
decoded_data = sorted(decoded_data, key=sort_func)
res = [data_i.data.decode(encoding) for data_i in decoded_data]
else:
res = []
return res

その他のメモ

GraphQL を使って画像ファイルを post するためのGUIとして、Altair GraphQL Client が使いやすかったです。

また、フロントサイドでの実装に schema.json が必要なようだったので、以下のコマンドより出力して連携しています。

$ python manage.py graphql_schema --schema app.schema.schema --out=schema.graphql

参考: https://docs.graphene-python.org/projects/django/en/latest/introspection/#introspection-schema

最後に

開発合宿では、普段使わないような技術や人と関わることで勉強になりました。また、今後のMICINのサービスについて色々アイデアを話したりと、大変いい時間を過ごせたと思います。

MICINの機械学習エンジニアやアプリケーションエンジニアにご興味がある方は、カジュアル面談からでもお待ちしておりますので、下記のリンクからどしどしご応募ください!

採用ページ: https://hrmos.co/pages/micin/jobs

--

--