こんにちは、SOMPO Digital Lab ソフトウェアエンジニアの木村です。
私の開発チームではトータルヘルスケアアプリWiTH Healthというプロダクトを開発しています。
このアプリケーションは私のチームで0から開発を始めたプロダクトであり、初期の技術選定などもチームで検討しながら行いました。
アプリケーションのバックエンドAPIはGo言語で実装されており、データ永続化のための
データベースとしてAWS Aurora PostgreSQLを使用しています。
今回はこのアプリケーションの実装にあたって導入したPostGISによるデータ型を、どのようにアプリケーションで扱っているのかについて、ORMの選定理由と合わせて見ていきたいと思います。
アプリケーションの要件
まずWiTH Healthのアプリケーションで満たすべき要件について説明します。
WiTH Healthでは「クーポン」と呼ばれる機能を提供しています。
これは一定の条件を満たしたユーザがご自身のお住まいの地域で利用できるクーポンを受け取れるといった機能です。
クーポンは地域内の多種多様な店舗が発行しており、飲食店やホテル、レジャー施設など様々な場所で使用できるものがあります。
クーポンの検索には、店舗名・クーポン名での検索に加えて、ユーザの位置情報を利用して現在地から近い店舗のクーポンも検索できるようになっています。
このためクーポン・店舗データはその店舗の位置情報と合わせてデータベース上に保存されています。位置情報の保存また高速な検索のために、データベースにはPostgreSQLの拡張モジュールであるPostGISを追加しています。
PostGISについての詳細は長くなるので割愛しますが、モジュールを追加すると位置情報を効率的に扱うために専用のデータ型やインデックスの追加されます。
ORMの候補
ではどのORMを選択するのかについてですが、Goでよく使用されているORMライブラリとしては以下の様なものがあります
あるいはGoの場合は標準ライブラリの database/sql
が利用されるケースも多いかもしれません。
それぞれ特徴はありますが、今回の選定する基準として
クエリの組み立て時に静的な型検査が行われてほしい
これは実装効率を上げるために是非ともほしい要件でした。
静的型検査があることでエディタの補完に導かれるだけで適切なクエリが組み立てられる上に、実行時のエラー発生削減が望めるはずです。
チームメンバーの中にはGoを書くのが初めてのメンバーも在籍しており、どれだけエディタのサポートを受けられるかといった部分は開発効率の高低にかなり影響する要素だと考えていました。
Go自体は静的型検査がある言語ですが、SQLクエリの組み立て操作でも静的型検査を行ってくれるライブラリとなると、候補の中では ent
あるいは SQLBoiler
に限られます。
PostGISによる拡張されたデータ型を扱えてほしい
前節でも述べたように今回はアプリケーションの要件を踏まえた上で、PostGISを導入しています。
PostGISが独自のデータ型を追加するため、データベース上の独自データ型とアプリケーション上のデータ型がORMにより簡単にマッピングできる必要がありました。
基本的にGoのデータ型とデータベース上のデータ型をマッピングさせるためには以下の2つの要件を満たす必要があります
1については位置情報用に次のような独自のデータ型を定義して、driver.Valuer
インターフェースを実装すれば問題ありません。
struct Point { Lat float64 Lng float64 }
2については独自定義したデータ型をORMが扱えるのかが問題になってきます。
ent
とSQLBoiler
はどちらもスキーマ構造と対応するGoの構造体を生成してくれるコマンドラインツールを持っています。このコマンドで生成された構造体のフィールドとして、独自に定義した Point
型を埋め込めるのであればマッピング可能となります。
今回選定に当たって、ent
とSQLBoiler
でツールを実行し構造体を生成してみたところ、SQLBoiler
ではPoint
型を埋め込むことはできず、string型にマッピングされてしまいました。
ent
とSQLBoilder
が生成した構造体は次のような物です。
type Shop struct { // ID ID string `json:"id,omitempty"` // 位置情報 Latlng *Point `json:"latlng,omitempty"` // 省略 }
type Shop struct { ID int `boil:"id" json:"id" toml:"id" yaml:"id"` LatLang string `boil:"latlng" json:"latlng" toml:"latlng" yaml:"latlng"` // 位置情報がstring型と見なされている }
よって今回は要件を満たすORMとしてentを使用することに決定しました。
entの使い方
ではどのように独自のデータ型に対応するのでしょうか。
entの基本的な使い型はドキュメントに書いてあるため割愛しますが、entではまずGoでテーブル定義を書いた後に、コマンドラインツールを実行することでマイグレーションファイルや、テーブル構造に対応するデータ型が生成されます。
テーブル定義は以下のように書きます。
// Fields of the Shop. func (Shop) Fields() []ent.Field { return []ent.Field{ field.String("id").Immutable().Comment("店舗ID"), field.Other("latlng", &PointS).SchemaType(map[string]string{ dialect.Postgres: "geometry(point, 4326)", }).Comment("店舗の位置座標"), // 省略 } }
位置情報を扱うために行っている事は次の2点です
- 通常テーブルのカラムに対応する型を
field.String
やfield.Int
で指定しますが、今回は独自定義したPoint
型を使用したいので、filed.Other
を使ってPoint
型を指定しています - PostgreSQL上のどの型とマッピングさせるのかを
SchemaType
で指定しています。今回はPostGISのgeometry(point, 4326)
型とマッピングしています。
こうして生成された構造体を使用することでPostGISで拡張された型に対して、独自定義の型をマッピングできます。
その後は、構造体のメソッドを用いることでデータベースの操作が可能です。例えば、店舗情報を作成したい場合は以下のようにします。
tx.Shop.Create(). SetID("dummyID"). SetLatlng(&Point{Lat: 35.681236, Lng: 139.767125}). SaveX(ctx)
まとめ
今回はORMとしてentを導入した理由と、PostGISの独自のデータ型の扱いについて触れてみました。
実際にent
を使用みての所感ですが、やはりクエリの組み立て時に静的型検査が走ることの恩恵は大きく、データベースから取得したデータの型不整合のようなエラーはほぼ発生していません。
今回はPostGISで定義された位置情報型を扱いたいという明確な要件があったため、すんなりとent
を使用することが決まりましたが、通常は単純な技術要件でだけではなく、開発メンバーのスキルセットや意向に合わせて選定していくことが大切だと思います。
SOMPO Digital Labでは一緒に働くソフトウェアエンジニアを募集しています
弊社ではGoで開発をするソフトウェアエンジニアを募集中です。
以下のリンクからカジュアル面談の応募ができるので、興味を持っていただけた方は是非話を聞きに来て下さい。