0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Geometry型のオブジェクトをJacksonでJson化する時の循環参照を回避する

Posted at

前提条件

  • Geometry:org.locationtech.jts.geomパッケージを利用
  • spring boot 2.2.5.RELEASE
  • spring data jpa
  • jackson
  • kotlin

Spring data JPAを使ってOneToManyManyToOneなリレーションを貼ったentityをJsonでレスポンスするようなAPIを作ろうとしたら、恐ろしい見た目のJsonが出来上がってしまったので対処法を調べました。

まずは失敗結果から

依存関係

build.gradle.kts
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アノテーションはOneToManyManyToOneなリレーション関係をjson化する時に相互参照になってしまう現象を防いでくれるものです。
こちらを参考にさせていただきました。

継承しているCommonEntityクラスはcreated_at、updated_atなどのテーブル共通のカラムが定義してあるだけなので省略します。

JpaRepository、Serviceクラス、Controllerクラス

これらは大したことを書いていないので省略します。

  • JpaRepository:基本的な使い方で、interface定義しただけ
  • Serviceクラス:repositoryを使って検索しているだけ
  • Controllerクラス:Serviceクラスのメソッドの戻り値をreturnしてるだけ

APIを実行

ブラウザからリクエストしてみました。

image.png

生データを表示したのがこちら

image.png

地獄絵図w

どうやらMultiPoint型のsectionPointsをjson化する時のシリアライズあたりで無限ループ的な現象になっている様な・・・。

解決方法

色々ググっていたら、jackson標準のシリアライズクラスだとGeometryに対応していないとかなんとか?っぽいです。
シリアライズクラスとでシリアライズクラスを自作しなさいとか書いてるのも見ましたが、それは面倒くさいので最終手段にとっておいて、他の方法を探しました。

その1(解決しなかった)

com.bedatadriven jackson-datatype-jtsを入れればいけるよという記事がありましたが、これはGeometryのパッケージにcom.vividsolutions.jts.geomを使っている場合で、今回のケースではClassCastExceptionが起きます。

その2(こっちで解決)

StackoverflowにJackson DataType JTSを組み込めばいけるよとか書いてあったので入れてみました。

依存関係

build.gradle.kt
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を実行

image.png

無事にJsonをレスポンスできました!

参考記事

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?