Featured image of post SpringBoot2のJPAでテーブル結合(INNER JOIN)する方法

SpringBoot2のJPAでテーブル結合(INNER JOIN)する方法

SpringBoot2でJPAを使い、SQLを書かずにテーブル結合(INNER JOIN)してデータを取得する方法。Entityのアノテーションでマッピングし、Repository・Service・Controllerまで実装して動作確認します♪

3575文字

JPAを使ってみよう

O/R Mapperを使ってみよう

前回はFlywayを使ったDB構築の自動化についてご紹介しました。

関連記事 【DBマイグレーション】GradleにFlywayを組み込んでパッチ適用を自動化しよう【SpringBoot2】 2019/04/16 3488文字 約7分で読めます IT Java Mac Windows アプリケーション

今回はJPAを利用したデータベースからの参照処理のO/R Mappingを実装してみようと思います。

JPAとは?

JPAは正式にはJava Persistence APIと呼び、Java EE標準のO/R MappingおよびDAOの技術仕様の事を指します。

[adsense-responsive]

Java Persistence API(JPA)とは、関係データベースのデータを扱う Java SE および Java EE のアプリケーションを開発するためのJava用フレームワークである。

JPA は、以下の3つの部分から成る。

API(javax.persistence パッケージで定義されている) Java Persistence Query Language オブジェクト/関係メタデータJava Persistence API - Wikipedia

また、JPAはあくまで仕様なので実際にはEclipseLink,Hibernate ORM,Apache Open JPAなどのライブラリを使うことになると思います。

今回は、SpringBootでJPAを利用する際にデフォルトで仕様されるHibernateを使ってみようと思います。

手順

マッピングするテーブル構造

今回も前回作成したテーブル構造をベースにマッピングしていきます。

今回の目的は、ユーザー一覧を取得するためにマッピングテーブルであるcurrent_account_infomationsテーブルを軸に取得してこようと思います

今回利用するJPAアノテーション

JPAは基本的にアノテーションベースでマッピングの設定をしていきます。

今回登場するのは以下のアノテーションです。

アノテーション概要
@Column( name = {カラム名} )フィールドにマッピングするDBのカラム名をname属性で設定する
@Enumerated( EnumType.STRING )Enumにカラムをマッピングしたい場合に設定する
@Entityテーブルと対応するEntityクラスに設定する
@Table( schema = {スキーマ名}, name = {テーブル名} )Entityクラスに紐付けたいテーブル情報を設定する
@EmbeddedIdPKカラムを紐付けたいクラスを参照する際に設定する
@EmbeddedPKではないカラムを紐付けたいクラスを参照する際に設定する
@OneToOne1...1のテーブルを結合したい際に設定する
※今回はテーブル定義情報は1..1~nだが、こちらを使うことでやりたいことは実現出来た
@ManyToOneN...0~1のテーブルを結合したい際に設定する
@JoinColumn( name = {カラム名} )結合する際のFKカラム名を指定したい場合に設定する
@AttributeOverride(name = {上書き対象フィールド}, column = @Column(name = {上書き後の値} ) )紐付けるEntity内のフィールドのカラム定義を上書きする
※今回のテーブル定義としては自テーブルと参照元によって同じコンテキストでも冗長的記述を端折るために異なるカラム名を付与しているので利用しています

開発用afterMigrateデータ

今回はローカル開発動作確認用として、以下のデータを投入します。

期待値としてはマッピングテーブルを用いて(新)のものだけ取得出来ればOKです。

ローカル確認用データ
-- ####################
-- # アカウント1
-- ####################
INSERT INTO
	account.accounts
(
	id
) VALUES (
	(SELECT nextval('account.account_id'))
);

INSERT INTO account.account_infomations
(
	id,
	account_id,
	name,
	age,
	sex_type,
	display_name
) values (
	(SELECT nextval('account.account_infomation_id')),
	(SELECT MAX(id) FROM account.accounts),
	'山田 太郎',
	19,
	'MALE',
	'山田 ローカル 太郎(古)'
);
INSERT INTO account.account_infomations
(
	id,
	account_id,
	name,
	age,
	sex_type,
	display_name
) values (
	(SELECT nextval('account.account_infomation_id')),
	(SELECT MAX(id) FROM account.accounts),
	'山田 太郎',
	19,
	'MALE',
	'山田 ローカル 太郎(新)'
);

INSERT INTO account.current_account_infomations
(
	account_id,
	account_infomation_id
) values (
	(SELECT MAX(id) FROM account.accounts),
	(SELECT MAX(id) FROM account.account_infomations)
);

-- ####################
-- # アカウント2
-- ####################
INSERT INTO
	account.accounts
(
	id
) VALUES (
	(SELECT nextval('account.account_id'))
);

INSERT INTO account.account_infomations
(
	id,
	account_id,
	name,
	age,
	sex_type,
	display_name
) values (
	(SELECT nextval('account.account_infomation_id')),
	(SELECT MAX(id) FROM account.accounts),
	'田中 花子',
	20,
	'FEMALE',
	'田中 ローカル 花子(古)'
);

INSERT INTO account.account_infomations
(
	id,
	account_id,
	name,
	age,
	sex_type,
	display_name
) values (
	(SELECT nextval('account.account_infomation_id')),
	(SELECT MAX(id) FROM account.accounts),
	'田中 花子',
	20,
	'FEMALE',
	'田中 ローカル 花子(新)'
);

INSERT INTO account.current_account_infomations
(
	account_id,
	account_infomation_id
) values (
	(SELECT MAX(id) FROM account.accounts),
	(SELECT MAX(id) FROM account.account_infomations)
);

-- ####################
-- # アカウント3
-- ####################
INSERT INTO
	account.accounts
(
	id
) VALUES (
	(SELECT nextval('account.account_id'))
);

INSERT INTO account.account_infomations
(
	id,
	account_id,
	name,
	age,
	sex_type,
	display_name
) values (
	(SELECT nextval('account.account_infomation_id')),
	(SELECT MAX(id) FROM account.accounts),
	'山田 二郎',
	15,
	'NONE',
	'山田 ローカル 二郎(古)'
);

INSERT INTO account.account_infomations
(
	id,
	account_id,
	name,
	age,
	sex_type,
	display_name
) values (
	(SELECT nextval('account.account_infomation_id')),
	(SELECT MAX(id) FROM account.accounts),
	'山田 二郎',
	15,
	'NONE',
	'山田 ローカル 二郎(新)'
);

INSERT INTO account.current_account_infomations
(
	account_id,
	account_infomation_id
) values (
	(SELECT MAX(id) FROM account.accounts),
	(SELECT MAX(id) FROM account.account_infomations)
);

依存関係の追加

まずはgradleの依存関係にJPAを追加していきましょう。

[adsense-responsive]

devtools/build.gradle
buildscript {
  apply from: 'property.gradle'

  ext {
  }
  repositories {
      mavenCentral()
  }
  dependencies {
      classpath("org.springframework.boot:spring-boot-gradle-plugin:${accountService.springBootVersion}")
      classpath "org.yaml:snakeyaml:${accountService.snakeyamlVersion}"
      classpath "org.postgresql:postgresql:${accountService.postgresVersion}"
  }
}
allprojects {
	apply plugin: 'eclipse'
}
subprojects {
	apply plugin: "java"
	apply plugin: "io.spring.dependency-management"

	group = accountService.group
	version = accountService.version
	sourceCompatibility = accountService.sourceCompatibility

	repositories {
		mavenCentral()
	}

    dependencyManagement {
    }

	dependencies {
		implementation "org.springframework.boot:spring-boot-starter:${accountService.springBootVersion}"
                implementation "org.springframework.boot:spring-boot-starter-data-jpa:${accountService.springBootVersion}" // 追加
		testImplementation "org.springframework.boot:spring-boot-starter-test:${accountService.springBootVersion}"
	}

}

Valueプロジェクトの修正

次にValueモデルを修正します。

AccountIdValue
package tech.blogenist.service.account.value.account;

import java.io.Serializable;

import javax.persistence.Column;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor(
        staticName = "of" )
@NoArgsConstructor(
        staticName = "empty" )
@Data
public class AccountIdValue implements Serializable
{
    public static final String LAVEL          = "会員ID";
    public static final String COLUMN_NAME    = "id";
    public static final String FK_COLUMN_NAME = "account_id";
    @Column(
            name = COLUMN_NAME )
    private Integer            value;
}

AccountInfomationIdValue
package tech.blogenist.service.account.value.account;

import java.io.Serializable;

import javax.persistence.Column;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor(
        staticName = "of" )
@NoArgsConstructor(
        staticName = "empty" )
@Data
public class AccountInfomationIdValue implements Serializable
{
    public static final String LAVEL          = "会員情報ID";
    public static final String COLUMN_NAME    = "id";
    public static final String FK_COLUMN_NAME = "account_infomation_id";
    @Column(
            name = COLUMN_NAME )
    private Integer            value;
}

[adsense-responsive]

AccountNameValue
package tech.blogenist.service.account.value.account;

import javax.persistence.Column;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor(
        staticName = "of" )
@NoArgsConstructor(
        staticName = "empty" )
@Data
public class AccountNameValue
{
    public static final String LAVEL       = "会員名";
    public static final String COLUMN_NAME = "name";
    @Column(
            name = COLUMN_NAME )
    private String             value;
}

AgeValue
package tech.blogenist.service.account.value.account;

import javax.persistence.Column;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor(
        staticName = "of" )
@NoArgsConstructor(
        staticName = "empty" )
@Data
public class AgeValue
{
    public static final String LAVEL       = "年齢";
    public static final String COLUMN_NAME = "age";
    @Column(
            name = COLUMN_NAME )
    private Integer            value;
}

SexValue
package tech.blogenist.service.account.value.account;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;

@AllArgsConstructor(
        staticName = "of" )
@NoArgsConstructor(
        staticName = "empty" )
@Data
public class SexTypeValue implements Serializable
{
    public static final String LAVEL          = "性別";
    public static final String COLUMN_NAME    = "type";
    public static final String FK_COLUMN_NAME = "sex_type";
    @Enumerated( EnumType.STRING )
    @Column(
            name = COLUMN_NAME )
    private Type               type;

    @AllArgsConstructor
    public enum Type
    {
        NONE( "未設定" ),
        MALE( "男性" ),
        FEMALE( "女性" );

        @Getter
        private String lavel;
    }
}

CommonCreatedAtValue
package tech.blogenist.service.account.value.common.datetime;

import java.time.LocalDateTime;

import javax.persistence.Column;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor(
        staticName = "of" )
@NoArgsConstructor(
        staticName = "empty" )
@Data
public class CommonCreatedAtValue
{
    public static final String LAVEL       = "作成日時";
    public static final String COLUMN_NAME = "created_at";
    @Column(
            name = COLUMN_NAME )
    private LocalDateTime      value;
}

CommonUpdatedAtValue
package tech.blogenist.service.account.value.common.datetime;

import java.time.LocalDateTime;

import javax.persistence.Column;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor(
        staticName = "of" )
@NoArgsConstructor(
        staticName = "empty" )
@Data
public class CommonUpdatedAtValue
{
    public static final String LAVEL       = "更新日時";
    public static final String COLUMN_NAME = "updated_at";
    @Column(
            name = COLUMN_NAME )
    private LocalDateTime      value;
}

MasterNameValue
package tech.blogenist.service.account.value.common.datetime;

import javax.persistence.Column;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor(
        staticName = "of" )
@NoArgsConstructor(
        staticName = "empty" )
@Data
public class MasterNameValue
{
    public static final String LAVEL       = "名称";
    public static final String COLUMN_NAME = "name";
    @Column(
            name = COLUMN_NAME )
    private String             value;
}

ValueFieldConst
package tech.blogenist.service.account.value.infrastructure.field;

public class ValueFieldConst
{
    public static final String FIELD = "value";
}

Entityプロジェクトの修正

次にEntityモデルを修正します。

AccountsTableEntity
package tech.blogenist.service.account.entity.db.account;

import javax.persistence.Embedded;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import tech.blogenist.service.account.value.account.AccountIdValue;
import tech.blogenist.service.account.value.common.datetime.CommonCreatedAtValue;
import tech.blogenist.service.account.value.common.datetime.CommonUpdatedAtValue;

@Builder
@Entity
@Table(
        schema = AccountsTableEntity.SCHEMA_NAME, name = AccountsTableEntity.TABLE_NAME )
@AllArgsConstructor(
        staticName = "of" )
@NoArgsConstructor(
        staticName = "empty" )
@Data
public class AccountsTableEntity
{
    public static final String SCHEMA_NAME = "account";
    public static final String TABLE_NAME  = "accounts";
    @EmbeddedId
    private AccountIdValue     accountId;

    @Embedded
    private CommonCreatedAtValue     createdAt;

    @Embedded
    private CommonUpdatedAtValue     updatedAt;

}

[adsense-responsive]

AccountInfomationsTableEntity
package tech.blogenist.service.account.entity.db.account;

import javax.persistence.Embedded;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import tech.blogenist.service.account.value.account.AccountDisplayNameValue;
import tech.blogenist.service.account.value.account.AccountIdValue;
import tech.blogenist.service.account.value.account.AccountInfomationIdValue;
import tech.blogenist.service.account.value.account.AccountNameValue;
import tech.blogenist.service.account.value.account.AgeValue;
import tech.blogenist.service.account.value.account.SexTypeValue;
import tech.blogenist.service.account.value.common.datetime.CommonCreatedAtValue;
import tech.blogenist.service.account.value.common.datetime.CommonUpdatedAtValue;

@Builder
@Entity
@Table(
        schema = AccountInfomationsTableEntity.SCHEMA_NAME, name = AccountInfomationsTableEntity.TABLE_NAME )
@AllArgsConstructor(
        staticName = "of" )
@NoArgsConstructor(
        staticName = "empty" )
@Data
public class AccountInfomationsTableEntity
{
    public static final String       SCHEMA_NAME = "account";
    public static final String       TABLE_NAME  = "account_infomations";
    @EmbeddedId
    private AccountInfomationIdValue accountInfomationId;

    @ManyToOne
    @JoinColumn(
            name = AccountIdValue.FK_COLUMN_NAME )
    private AccountsTableEntity      accountEntity;

    @Embedded
    private AccountNameValue         name;

    @Embedded
    private AgeValue                 age;

    @ManyToOne
    @JoinColumn(
            name = SexTypeValue.FK_COLUMN_NAME )
    private SexTypesTableEntity      sexType;

    @Embedded
    private AccountDisplayNameValue displayName;

    @Embedded
    private CommonCreatedAtValue             createdAt;

    @Embedded
    private CommonUpdatedAtValue             updatedAt;

}

SexTypesTableEntity
package tech.blogenist.service.account.entity.db.account;

import javax.persistence.Embedded;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import tech.blogenist.service.account.value.account.SexTypeValue;
import tech.blogenist.service.account.value.common.datetime.MasterNameValue;

@Builder
@Entity
@Table(
        schema = SexTypesTableEntity.SCHEMA_NAME, name = SexTypesTableEntity.TABLE_NAME )
@AllArgsConstructor(
        staticName = "of" )
@NoArgsConstructor(
        staticName = "empty" )
@Data
public class SexTypesTableEntity
{
    public static final String SCHEMA_NAME = "account";
    public static final String TABLE_NAME  = "sex_types";
    @EmbeddedId
    private SexTypeValue       type;

    @Embedded
    private MasterNameValue    name;

}

CurrentAccountInfomationsTableEntity
package tech.blogenist.service.account.entity.db.account;

import javax.persistence.AttributeOverride;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import tech.blogenist.service.account.value.account.AccountIdValue;
import tech.blogenist.service.account.value.account.AccountInfomationIdValue;
import tech.blogenist.service.account.value.common.datetime.CommonCreatedAtValue;
import tech.blogenist.service.account.value.common.datetime.CommonUpdatedAtValue;
import tech.blogenist.service.account.value.infrastructure.field.ValueFieldConst;

@Builder
@Entity
@Table(
        schema = CurrentAccountInfomationsTableEntity.SCHEMA_NAME,
        name = CurrentAccountInfomationsTableEntity.TABLE_NAME )
@AllArgsConstructor(
        staticName = "of" )
@NoArgsConstructor(
        staticName = "empty" )
@Data
public class CurrentAccountInfomationsTableEntity
{
    public static final String            SCHEMA_NAME = "account";
    public static final String            TABLE_NAME  = "current_account_infomations";

    @AttributeOverride(
            name = ValueFieldConst.FIELD, column = @Column(
                    name = AccountIdValue.FK_COLUMN_NAME ) )
    @EmbeddedId
    private AccountIdValue                        accountId;

    @OneToOne
    @JoinColumn(
            name = AccountInfomationIdValue.FK_COLUMN_NAME,
            referencedColumnName = AccountInfomationIdValue.COLUMN_NAME )
    private AccountInfomationsTableEntity accountInfomations;

    @Embedded
    private CommonCreatedAtValue                  createdAt;

    @Embedded
    private CommonUpdatedAtValue                  updatedAt;

}

Datasourceプロジェクトの修正

CurrentAccountInfomationsRepository
package tech.blogenist.service.account.datasource.db.current_account.account_infomations;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import tech.blogenist.service.account.entity.db.account.CurrentAccountInfomationsTableEntity;
import tech.blogenist.service.account.value.account.AccountIdValue;

@Repository
public interface CurrentAccountInfomationsRepository
        extends JpaRepository< CurrentAccountInfomationsTableEntity, AccountIdValue >
{
    public List< CurrentAccountInfomationsTableEntity > findAll();
}

APIプロジェクトの修正

modelパッケージ

AccountListingElementModel
package tech.blogenist.service.account.api.model.account.listing.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import tech.blogenist.service.account.value.account.AccountDisplayNameValue;
import tech.blogenist.service.account.value.account.AccountIdValue;
import tech.blogenist.service.account.value.account.AccountInfomationIdValue;
import tech.blogenist.service.account.value.account.AccountNameValue;
import tech.blogenist.service.account.value.account.AgeValue;
import tech.blogenist.service.account.value.common.datetime.MasterNameValue;

@Builder
@AllArgsConstructor(
        staticName = "of" )
@NoArgsConstructor(
        staticName = "empty" )
@Data
public class AccountListingElementModel
{
    private AccountIdValue           accountId;
    private AccountInfomationIdValue accountInfomationId;
    private AccountNameValue         name;
    private AgeValue                 age;
    private MasterNameValue          sex;
    private AccountDisplayNameValue displayName;
}

AccountListingResponseModel
package tech.blogenist.service.account.api.model.account.listing.response;

import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@AllArgsConstructor( staticName = "of" )
@NoArgsConstructor( staticName = "empty" )
@Data
public class AccountListingResponseModel
{
    private List< AccountListingElementModel > accounts;
}

serviceパッケージ

AccountListingService
package tech.blogenist.service.account.api.service.account.listing;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import tech.blogenist.service.account.api.model.account.listing.response.AccountListingElementModel;
import tech.blogenist.service.account.api.model.account.listing.response.AccountListingResponseModel;
import tech.blogenist.service.account.datasource.db.current_account.account_infomations.CurrentAccountInfomationsRepository;
import tech.blogenist.service.account.entity.db.account.AccountInfomationsTableEntity;
import tech.blogenist.service.account.entity.db.account.CurrentAccountInfomationsTableEntity;
import tech.blogenist.service.account.value.common.datetime.MasterNameValue;

@Service
public class AccountListingService
{
    @Autowired
    private CurrentAccountInfomationsRepository currentAccountInfomationsRepository;

    public AccountListingResponseModel listing()
    {

        List< CurrentAccountInfomationsTableEntity > entities = currentAccountInfomationsRepository.findAll();
        List< AccountListingElementModel > elements = entities.stream()
                .map( entity ->
                {
                    AccountInfomationsTableEntity infomation = entity.getAccountInfomations();

                    AccountListingElementModel element = AccountListingElementModel.builder()
                            .accountInfomationId( infomation.getId() )
                            .accountId( entity.getAccountId() )
                            .age( infomation.getAge() )
                            .name( infomation.getName() )
                            .sex(
                                    infomation.getSexType() == null
                                            ? MasterNameValue.empty()
                                            : infomation.getSexType().getName() )
                            .displayName( infomation.getDisplayName() )
                            .build();
                    return element;
                } )
                .collect( Collectors.toList() );

        return AccountListingResponseModel.of( elements );
    }
}

AccountDetailService
package tech.blogenist.service.account.api.service.account.detail;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import tech.blogenist.service.account.api.model.account.detail.response.AccountDetailResponseModel;
import tech.blogenist.service.account.datasource.db.current_account.account_infomations.CurrentAccountInfomationsRepository;
import tech.blogenist.service.account.entity.db.account.CurrentAccountInfomationsTableEntity;
import tech.blogenist.service.account.value.account.AccountIdValue;

@Service
public class AccountDetailService
{
    @Autowired
    private CurrentAccountInfomationsRepository currentAccountInfomationsRepository;

    public AccountDetailResponseModel findBy( AccountIdValue id )
    {
        CurrentAccountInfomationsTableEntity entity = currentAccountInfomationsRepository
                .findByAccountId( id );

        return AccountDetailResponseModel.builder()
                .accountId( entity.getAccountId() )
                .accountInfomationId( entity.getAccountInfomations().getId() )
                .name( entity.getAccountInfomations().getName() )
                .age( entity.getAccountInfomations().getAge() )
                .displayName( entity.getAccountInfomations().getDisplayName() )
                .sex( entity.getAccountInfomations().getSexType().getName() )
                .build();
    }
}

apiパッケージ

AccountListingController
package tech.blogenist.service.account.api.controller.account.listing;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import tech.blogenist.service.account.api.infrastructure.rest.path.RestPathConst;
import tech.blogenist.service.account.api.model.account.listing.response.AccountListingResponseModel;
import tech.blogenist.service.account.api.service.account.listing.AccountListingService;

@RestController
@RequestMapping( RestPathConst.VersionPath.V1 + RestPathConst.CategoryPath.AccountPath.BASE_PATH )
public class AccountListingController
{
    @Autowired
    private AccountListingService service;

    @GetMapping
    public AccountListingResponseModel listing()
    {
        AccountListingResponseModel response = service.listing();
        return response;
    }
}

AccountDetailController
package tech.blogenist.service.account.api.controller.account.detail;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import tech.blogenist.service.account.api.infrastructure.rest.path.RestPathConst;
import tech.blogenist.service.account.api.model.account.detail.response.AccountDetailResponseModel;
import tech.blogenist.service.account.api.service.account.detail.AccountDetailService;
import tech.blogenist.service.account.value.account.AccountIdValue;

@RestController
@RequestMapping( RestPathConst.VersionPath.V1 + RestPathConst.CategoryPath.AccountPath.BASE_PATH )
public class AccountDetailController
{
    @Autowired
    private AccountDetailService service;

    @GetMapping(
            path = RestPathConst.CategoryPath.AccountPath.RESOURCE_PATH )
    public AccountDetailResponseModel detail(
            @PathVariable( RestPathConst.CategoryPath.AccountPath.PATH_VARIABLE ) Integer accountId

    )
    {
        AccountDetailResponseModel response = service.findBy( AccountIdValue.of( accountId ) );

        return response;
    }
}

動作確認

では、実際にエンドポイントを叩いてみましょう!

一覧

[adsense-responsive]

/v1/accounts/
{
    "accounts": [
        {
            "accountId": {
                "value": 10001
            },
            "accountInfomationId": {
                "value": 20002
            },
            "name": {
                "value": "山田 太郎"
            },
            "age": {
                "value": 19
            },
            "sex": {
                "value": "男性"
            },
            "displayName": {
                "value": "山田 ローカル 太郎(新)"
            }
        },
        {
            "accountId": {
                "value": 10002
            },
            "accountInfomationId": {
                "value": 20004
            },
            "name": {
                "value": "田中 花子"
            },
            "age": {
                "value": 20
            },
            "sex": {
                "value": "女性"
            },
            "displayName": {
                "value": "田中 ローカル 花子(新)"
            }
        },
        {
            "accountId": {
                "value": 10003
            },
            "accountInfomationId": {
                "value": 20006
            },
            "name": {
                "value": "山田 二郎"
            },
            "age": {
                "value": 15
            },
            "sex": {
                "value": "未設定"
            },
            "displayName": {
                "value": "山田 ローカル 二郎(新)"
            }
        }
    ]
}

詳細

/v1/accounts/10001/
{
    "accountId": {
        "value": 10001
    },
    "accountInfomationId": {
        "value": 20002
    },
    "name": {
        "value": "山田 太郎"
    },
    "age": {
        "value": 19
    },
    "sex": {
        "value": "男性"
    },
    "displayName": {
        "value": "山田 ローカル 太郎(新)"
    }
}

無事にテーブル結合が行われて、複数テーブルのデータをレスポンスモデルに変換して取得することが出来ました♪

参考

現場至上主義 Spring Boot2 徹底活用

Spring Data JPAプログラミング入門

終わりに

以上のように、JPAの仕組みやマッピング方法や結合方法さえわかれば、SQLを書くことなくデータの取得が可能になります。

他にもJPAには便利な機能が搭載されていますので、是非使ってみてください♪