Spring Data Restで@Projection

Spring Data RestでサクッとRestできるようにします。

DB

適当クエリでテーブル2つとデータを作成

create table memo_title (
  id serial primary key,
  title varchar(255) not null
);

create table memo_value (
  id serial primary key,
  value varchar(255) not null,
  title_id int not null
);

insert into memo_title(id, title) values(1, 'TESTタイトル');
insert into memo_value(id, value, title_id) values(1, 'ほげ', 1);
insert into memo_value(id, value, title_id) values(2, 'ふが', 1);

memo_value が memo_title の id を持ってて親子関係がある感じです。
(面倒なのでFKとか無しで)

コード

コードを書いていきます。
(例によってkotlinです)

MemoTitle.kt
@Entity
@Table(name="memo_title")
class MemoTitle {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0

    @Column(nullable = false)
    lateinit var title: String
}

@Repository
interface MemoTitleRepository : PagingAndSortingRepository<MemoTitle, Long>
MemoValue.kt
@Entity
@Table(name="memo_value")
class MemoValue {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0

    @Column(nullable = false)
    lateinit var value: String

    @Column(nullable = false)
    var title_id: Long = 0
}

@Repository
interface MemoValueRepository : PagingAndSortingRepository<MemoValue, Long>

こういう時に kotlin だと関連したコードが1ファイルに書けるってだけでウレシイ。。。

結果

この時点で実行すると

MemoTitle
> curl http://localhost:9000/api/memoTitles
{
  "_embedded" : {
    "memoTitles" : [ {
      "title" : "TESTタイトル",
      "_links" : {
        "self" : {
          "href" : "http://localhost:9000/api/memoTitles/1"
        },
        "memoTitle" : {
          "href" : "http://localhost:9000/api/memoTitles/1"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:9000/api/memoTitles"
    },
    "profile" : {
      "href" : "http://localhost:9000/api/profile/memoTitles"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 1,
    "totalPages" : 1,
    "number" : 0
  }
}
MemoValue
> curl http://localhost:9000/api/memoValues
{
  "_embedded" : {
    "memoValues" : [ {
      "id" : 1,
      "value" : "ほげ",
      "title_id" : 1,
      "_links" : {
        "self" : {
          "href" : "http://localhost:9000/api/memoValues/1"
        },
        "memoValue" : {
          "href" : "http://localhost:9000/api/memoValues/1"
        },
        "title" : {
          "href" : "http://localhost:9000/api/memoValues/1/title"
        }
      }
    }, {
      "id" : 2,
      "value" : "ふが",
      "title_id" : 1,
      "_links" : {
        "self" : {
          "href" : "http://localhost:9000/api/memoValues/2"
        },
        "memoValue" : {
          "href" : "http://localhost:9000/api/memoValues/2"
        },
        "title" : {
          "href" : "http://localhost:9000/api/memoValues/2/title"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:9000/api/memoValues"
    },
    "profile" : {
      "href" : "http://localhost:9000/api/profile/memoValues"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 2,
    "totalPages" : 1,
    "number" : 0
  }
}

改変

MemoValueをGETしたときにtitleも欲しかったりします。
なのでMemoValueにProjectionを入れます。

MemoValue.kt
@Entity
@Table(name="memo_value")
class MemoValue {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0

    @Column(nullable = false)
    lateinit var value: String

    @Column(nullable = false)
    var title_id: Long = 0

    @ManyToOne(targetEntity = MemoTitle::class, fetch = FetchType.EAGER)
    @JoinColumn(name = "title_id", insertable = false, updatable = false, nullable = false)
    lateinit var title: MemoTitle
}

@Projection(types = arrayOf(MemoValue::class))
interface MemoValueProjection {
    var id: Long
    var value: String
    var title: MemoTitle
}

@RepositoryRestResource(excerptProjection = MemoValueProjection::class)
interface MemoValueRepository : PagingAndSortingRepository<MemoValue, Long>

@Entity が FROM
@Projection が SELECT
@Repository が WHERE(今回はfindByxxとか書いてないけど)
SQL発行結果を返してくれてると思うとわかりやすいです。
jpaddl-autoとか使ってたらMemoTitle側にも@OneToManyいるかも。)

改変後結果

MemoValue
> curl http://localhost:9000/api/memoValues
{
  "_embedded" : {
    "memoValues" : [ {
      "id" : 1,
      "value" : "ほげ",
      "title" : {
        "title" : "TESTタイトル"
      },
      "_links" : {
        "self" : {
          "href" : "http://localhost:9000/api/memoValues/1"
        },
        "memoValue" : {
          "href" : "http://localhost:9000/api/memoValues/1{?projection}",
          "templated" : true
        },
        "title" : {
          "href" : "http://localhost:9000/api/memoValues/1/title"
        }
      }
    }, {
      "id" : 2,
      "value" : "ふが",
      "title" : {
        "title" : "TESTタイトル"
      },
      "_links" : {
        "self" : {
          "href" : "http://localhost:9000/api/memoValues/2"
        },
        "memoValue" : {
          "href" : "http://localhost:9000/api/memoValues/2{?projection}",
          "templated" : true
        },
        "title" : {
          "href" : "http://localhost:9000/api/memoValues/2/title"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:9000/api/memoValues"
    },
    "profile" : {
      "href" : "http://localhost:9000/api/profile/memoValues"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 2,
    "totalPages" : 1,
    "number" : 0
  }
}

無事にアソシエーション先の情報も取れました。
逆に親から子を取る時は子のサイズがわからないので個別にLazyで。

これだけの記述で必要な情報のGETに加えてPOSTやらPUTやらPATCHでのINSERTやDELETEやUPDATEなんかもルーティングされてるとかステキですね。

いじょ