前提条件
- Geometry:
org.locationtech.jts.geom
パッケージを利用 - spring boot 2.2.5.RELEASE
- spring data jpa
- jackson
- kotlin
Spring data JPA
を使ってOneToMany
、ManyToOne
なリレーションを貼ったentityをJsonでレスポンスするようなAPIを作ろうとしたら、恐ろしい見た目のJsonが出来上がってしまったので対処法を調べました。
まずは失敗結果から
依存関係
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
runtimeOnly("org.postgresql:postgresql")
implementation("net.postgis:postgis-jdbc:2.2.1")
implementation("org.hibernate:hibernate-spatial:5.4.12.Final")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.+")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
implementation("org.springframework.boot:spring-boot-devtools")
implementation("org.flywaydb:flyway-core")
}
今回の趣旨に関係しそうなものとしては、Geometryクラスを扱える様にするためにhibernate-spatial
を入れています。
エンティティ
package entity
import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.ObjectIdGenerators
import org.hibernate.annotations.Where
import org.locationtech.jts.geom.MultiPoint
import javax.persistence.*
//駅
@Entity
@Table(name = "stations")
@Where(clause="is_deleted = false")
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator::class)
data class Station(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY, generator = "stations_id_seq")
val id: Long? = null,
var name: String? = null,
//リレーション
@OneToMany(mappedBy = "station")
var stationSections: List<StationSection>? = null
): CommonEntity()
//駅区画
@Entity
@Table(name = "station_sections")
@Where(clause="is_deleted = false")
data class StationSection(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY, generator = "station_sections_id_seq")
val id: Long? = null,
var sectionPoints: MultiPoint? = null,
//リレーション
@ManyToOne
var station: Station? = null
): CommonEntity()
@JsonIdentityInfo
アノテーションはOneToMany
、ManyToOne
なリレーション関係をjson化する時に相互参照になってしまう現象を防いでくれるものです。
→ こちらを参考にさせていただきました。
継承しているCommonEntity
クラスはcreated_at、updated_atなどのテーブル共通のカラムが定義してあるだけなので省略します。
JpaRepository、Serviceクラス、Controllerクラス
これらは大したことを書いていないので省略します。
- JpaRepository:基本的な使い方で、interface定義しただけ
- Serviceクラス:repositoryを使って検索しているだけ
- Controllerクラス:Serviceクラスのメソッドの戻り値をreturnしてるだけ
APIを実行
ブラウザからリクエストしてみました。
生データを表示したのがこちら
地獄絵図w
どうやらMultiPoint
型のsectionPointsをjson化する時のシリアライズあたりで無限ループ的な現象になっている様な・・・。
解決方法
色々ググっていたら、jackson標準のシリアライズクラスだとGeometryに対応していないとかなんとか?っぽいです。
シリアライズクラスとでシリアライズクラスを自作しなさいとか書いてるのも見ましたが、それは面倒くさいので最終手段にとっておいて、他の方法を探しました。
その1(解決しなかった)
com.bedatadriven jackson-datatype-jts
を入れればいけるよという記事がありましたが、これはGeometryのパッケージにcom.vividsolutions.jts.geom
を使っている場合で、今回のケースではClassCastExceptionが起きます。
その2(こっちで解決)
StackoverflowにJackson DataType JTSを組み込めばいけるよとか書いてあったので入れてみました。
依存関係
dependencies {
/**
* 省略
*/
//これを追記しました
implementation("com.graphhopper.external:jackson-datatype-jts:1.0-2.7")
}
エンティティ
package entity
import com.bedatadriven.jackson.datatype.jts.serialization.GeometryDeserializer
import com.bedatadriven.jackson.datatype.jts.serialization.GeometrySerializer
import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.ObjectIdGenerators
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.hibernate.annotations.Where
import org.locationtech.jts.geom.MultiPoint
import javax.persistence.*
//駅
@Entity
@Table(name = "stations")
@Where(clause="is_deleted = false")
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator::class)
data class Station(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY, generator = "stations_id_seq")
val id: Long? = null,
var name: String? = null,
//リレーション
@OneToMany(mappedBy = "station")
var stationSections: List<StationSection>? = null
): CommonEntity()
//駅区画
@Entity
@Table(name = "station_sections")
@Where(clause="is_deleted = false")
data class StationSection(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY, generator = "station_sections_id_seq")
val id: Long? = null,
@JsonSerialize(using = GeometrySerializer::class)
@JsonDeserialize(using = GeometryDeserializer::class)
var sectionPoints: MultiPoint? = null,
//リレーション
@ManyToOne
var station: Station? = null
): CommonEntity()
MultiPoint
型の変数に@JsonSerialize
アノテーションを付けて、using変数に今回追加したGeometrySerializer
クラスを指定してシリアライズします。
デシリアライズも同じ要領で指定します。
APIを実行
無事にJsonをレスポンスできました!
参考記事
-
Qiita含め、他多数のサイトを参考にさせていただきました。