Skip to content

warumono-for-develop/spring-boot-jpa-relationships-tutorial

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

7 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Spring Boot with JPA for Relationships Tutorial

An tutorial application using Spring Boot as JPA Relationships back-end.

More details about the codes, please read the online Spring Boot and Spring Data JPA.

Requirements

Running in

  • JDK 1.8 or newer
  • Spring Boot 1.5.9.RELEASE or newer
  • Gradle 3.5.1 or newer org.projectlombok:lombok

Optional

Dependencies

Latest Update

  • 1.0 (Dec 13, 2017)

Relationships features

1:1 (@OneToOne)

  • Reference Entity Profile
  • Build the project gradle build
  • Run the application ./gradlew bootRun

Look at the following domain model:

	@JsonBackReference
	@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
	@JoinColumn(updatable = false, nullable = false, name = "user_seq", referencedColumnName = "seq", foreignKey = @ForeignKey(name = "FKEY_USER_SEQ_IN_PROFILE"))
	private User host;

With OAuth2 Resource Server

Test accounts

User

username: admin@me.com
password: admin
-- Has authorities: USER, ADMIN

or

username: user@me.com
password: user
-- Has authorities: USER

Client

client id    : oneclient
client secret: onesecret
-- Has scopes: read, write
-- Has grant types: authorization_code, refresh_token, implicit, password, client_credentials

or

client id    : twoclient
client secret: twosecret
-- Has scopes: read
-- Has grant types: authorization_code, client_credentials

or

You can use it that create a new account.

@Bean
public CommandLineRunner commandLineRunner(UserRepository userRepository, ClientRepository clientRepository)
{
  String yourUsername = "your@email.address";
  String yourPassword = new BCryptPasswordEncoder().encode("yourpassword");
 
  String yourClientId = "your_client_id";
  String yourClientSecret = new BCryptPasswordEncoder().encode("your_client_secret");

  return args ->
  {
    userRepository.save(new AppUser(3L, yourUsername, yourPassword, "USER,ADMIN"));
    clientRepository.save(new AppClient(yourClientId, yourClientSecret, "read,write", "authorization_code,refresh_token,implicit,password,client_credentials"));
  };
}

Get a access token

Request command

Template command
  • clientid : id in AppClient entity.
  • clientsecret : secret in AppClient entity.
  • username : username in AppUser entity.
  • password : password in AppUser entity.
$ curl -XPOST "<clientid>:<clientsecret>@localhost:9090/oauth/token" -d "grant_type=password&username=<username>&password=<password>"
via
$ curl -XPOST "oneclient:onesecret@localhost:9090/oauth/token" -d "grant_type=password&username=user@me.com&password=user"

Response

Response template
  • access_token : token in order to access resource in ResourceServer
  • refresh_token : token in order to get a new access_token in AuthorizationServer
{"access_token":"<access_token>","token_type":"bearer","refresh_token":"<refresh_token>","expires_in":43199,"scope":"read write","jti":"ed68363e-2ced-4466-8c07-894a04cd3250"}

via

{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTMxNTczNzksInVzZXJfbmFtZSI6InVzZXJAbWUuY29tIiwiYXV0aG9yaXRpZXMiOlsiVVNFUiJdLCJqdGkiOiJlZDY4MzYzZS0yY2VkLTQ0NjYtOGMwNy04OTRhMDRjZDMyNTAiLCJjbGllbnRfaWQiOiJvbmVjbGllbnQiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXX0.ZFxOMfjVy-z4QkLy20LWvmsClgqpCtIuhlzM9pyw6YUDGgWrIn6QfKFi5OMOmrKFuJvk_IA57aRa27PMAQuHKWKtHryWj71BUqQbWIVt0Cc04ZfBuey5Xy6qIHHvEy-LhaAt4KiX4JnySoLspiuBMgRs0-OCFvAhrO5vEG-Q2svlkivMMEMl3qDgosh4S4IBmmJ-WKckJTOQQ9Zwr3yrSJoNXPDPI_1Nik4jzP2I0rs8jYGuFVG-nst9xd8PRA9JtblAcCjjSwPhV6U72Ue5MdP_vsGXTdSmdlidNeclWqkCYiW3FJQ23LyIo9wT8-ouf9xOXuHn67Tj6C87tV46Ng","token_type":"bearer","refresh_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyQG1lLmNvbSIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiJlZDY4MzYzZS0yY2VkLTQ0NjYtOGMwNy04OTRhMDRjZDMyNTAiLCJleHAiOjE0OTU3MDYxNzksImF1dGhvcml0aWVzIjpbIlVTRVIiXSwianRpIjoiYWIyZTVkNTYtZjQxYi00Zjc2LThjMDktN2Y2NTE0NTc3ODRkIiwiY2xpZW50X2lkIjoib25lY2xpZW50In0.AOtrqPxVmGe0zSkJcDP3-yrYydHLjEkLaJoR47VtfpH2Qhjhf9VhB5r9oF4pAYh9KnSvep5C1BoAIoQslE53DZELLzM4nkxEKY4arGtZkxAjjQWPvdJT5UC8xMVCD8RSmhnB5t0wap5TLr8G78_7uQRLeAxmzwdTtJVBQRUNz_LLU_iokkWZaTbwOlnDLhbAQcR5ZFArwvsxBNlw2YNYOhWhk1jibzBMZvkfv4IP5L_bZyVEKEeCoucJLad_mZvWI9b-6PNTZlzZ3OLxRdRcB6IsKIKWSwP0m9SuQ2tx2MWLeL3b8wCxUAnzjA7ye1LfColsnW2EqY8m3_lMIEoNuw","expires_in":43199,"scope":"read write","jti":"ed68363e-2ced-4466-8c07-894a04cd3250"}

API

Configuration

By default Spring Boot applications run on port 9090. But may vary depending on what ports are in use on your machine (check the terminal after entering the ./gradlew bootRun command). If you require to change which port the application runs on by default, add the following to:

application.yml

server:
    port: 9090 # --> change other port via. 9999

Conclusion

Domain Configurations JsonObjectMapper JsonManagedReference JsonBackReference ToStringBuilder

Dependencies

Latest Update

  • 1.0 (Dec 13, 2017)

Domain ER(Entity-Relation)

Code tables

ROLE (κΆŒν•œ)

A credential table that is given to a user

μ‚¬μš©μžμ—κ²Œ λΆ€μ—¬λ˜λŠ” κΆŒν•œ 정보 ν…Œμ΄λΈ”

ACCEPTANCE (λ™μ˜μ‚¬ν•­)

The table of consent information required for the user

μ‚¬μš©μžμ—κ²Œ μš”κ΅¬λ˜λŠ” λ™μ˜μ‚¬ν•­ 정보 ν…Œμ΄λΈ”

Custom tables

USER (μ‚¬μš©μž)

User information table that is the basis of data

λ°μ΄ν„°μ˜ κΈ°μ΄ˆκ°€ λ˜λŠ” μ‚¬μš©μž 정보 ν…Œμ΄λΈ”

PROFILE (ν”„λ‘œν•„)

User's detailed information table

μ‚¬μš©μžμ˜ 상세 정보 ν…Œμ΄λΈ”

MOBILE (νœ΄λŒ€μ „ν™”)

User's phone information table

μ‚¬μš©μžμ˜ νœ΄λŒ€μ „ν™” 정보 ν…Œμ΄λΈ”

Generated tables

PROFILE_ACCEPTANCE (μ‚¬μš©μžμ˜ λ™μ˜μ‚¬ν•­)

Information table that user agrees

μ‚¬μš©μžκ°€ λ™μ˜ν•œ 사항 정보 ν…Œμ΄λΈ”

USER_ROLE (μ‚¬μš©μžμ˜ κΆŒν•œ)

Authorization information table granted to user

μ‚¬μš©μžμ—κ²Œ λΆ€μ—¬λœ κΆŒν•œ 정보 ν…Œμ΄λΈ”

Relationship features

USER (1) --- (1) PROFILE

PROFILE (1) --- (N) MOBILE

USER (N) --- (M) ROLE

PROFILE (N) --- (M) ACCEPTANCE

Case 1 : 1 ( @OneToOne )

The forward part of the reference

USER

  @JsonManagedReference
  @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "host")
  @JoinColumn(name = "user_seq", referencedColumnName = "seq")
  private Profile profile;
The back part of the reference

PROFILE

  @JsonBackReference
  @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
  @JoinColumn(updatable = false, nullable = false, name = "user_seq", referencedColumnName = "seq", foreignKey = @ForeignKey(name = "FKEY_USER_SEQ_IN_PROFILE"))
  private User host;

Case 1 : N ( @OneToMany )

The forward part of the reference

PROFILE

  @JsonManagedReference
  @Default
  @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "holder")
  private Collection<Mobile> mobiles = Sets.newHashSet();
The back part of the reference

MOBILE

  @JsonBackReference
  @ManyToOne
  @JoinColumn(updatable = false, nullable = false, name = "profile_seq", referencedColumnName = "seq", foreignKey = @ForeignKey(name = "FKEY_PROFILE_SEQ_IN_MOBILE"))
  private Profile holder;

Case N : M ( @ManyToMany ) [Use the '@JoinTable']

The forward part of the reference

USER

  @Default
  @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
  @JoinTable
  (
    name = "USER_ROLE", 
    joinColumns = @JoinColumn(name = "user_seq", referencedColumnName = "seq", foreignKey = @ForeignKey(name = "FKEY_USER_SEQ_IN_USER")), 
    inverseJoinColumns = @JoinColumn(name = "role_seq", referencedColumnName = "seq", foreignKey = @ForeignKey(name = "FKEY_ROLE_SEQ_IN_ROLE")), 
    foreignKey = @ForeignKey(name = "FKEY_USER_SEQ__ROLE_SEQ_IN_USER_ROLE")
  )
  private Collection<Role> roles = Sets.newHashSet();
The back part of the reference

ROLE

No attribute, only specified in USER.

Instead, a new table(**USER_ROLE**) is created that connects the two tables.

Check your DBMS.

Case N : M ( @ManyToMany ) [Use the '@EmbeddedId']

It is actually 1 : N ( @OneToMany ), not N : M ( @ManyToMany ).

However, since they have the same characteristics as N : M ( @ManyToMany ), they are titled N : M ( @ManyToMany ).

You can also add columns by extending entity.

@Embeddable

ProfileAcceptanceId.java

A new ID table that connects two tables.

  @Embeddable // <--- annotation
  public class ProfileAcceptanceId implements Serializable // <--- implements Serializable
  {
    private static final long serialVersionUID = 1L;

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(updatable = false, nullable = false, name = "profile_seq", referencedColumnName = "seq", foreignKey = @ForeignKey(name = "FKEY_PROFILE_SEQ_IN_PROFILE"))
    private Profile profile;

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(updatable = false, nullable = false, name = "acceptance_seq", referencedColumnName = "seq", foreignKey = @ForeignKey(name = "FKEY_ACCEPTANCE_SEQ_IN_ACCEPTANCE"))
    private Acceptance acceptance;
  }

A new Properties(Columns) table.

@Entity

PROFILE_ACCEPTANCE

  @Entity
  public class ProfileAcceptance
  {
    @NonNull
    @EmbeddedId // <--- annotation
    private ProfileAcceptanceId primaryKey; // <--- ID

    /* extra properties(columns) */
    @Default
    @Column(updatable = true, nullable = false, columnDefinition = "BOOLEAN DEFAULT 0 COMMENT 'μˆ˜μ‹ λ™μ˜ μ—¬λΆ€'")
    private Boolean accepted = Boolean.FALSE;
    ...
  }

No attribute, only specified in USER.

Super - Common properties(columns) and toString

Properties(Columns)

The following properties(columns) are automatically assigned when saving, even if you do not specify them directly.

  • seq - auto_increment by DBMS
  • identity - generate by UUID
  • updatedAt - apply by Spring Data Annotation
  • registedAt - apply by Spring Data Annotation

Case 1 :

With identity property(column)

Case 2 :

Without identity property(column)

Override toString to ToStringBuilder

Usage

  public class WithIdentity extends AuditingEntity // <--- extends

or

  public class WithoutIdentity extends AuditingWithoutIdentityEntity // <--- extends

ID with Custom key

seq

Unique Identification Keys in the Server

μ„œλ²„λ‚΄μ˜ 고유 식별 ν‚€

μ„œλ²„λ‚΄μ—μ„œ μ‚¬μš©ν•˜λŠ” Primary Key

identity

  private String identity = UUID.randomUUID().toString();

Unique identification key between client and server

ν΄λΌμ΄μ–ΈνŠΈμ™€ μ„œλ²„κ°„μ˜ 고유 식별 ν‚€

ν΄λΌμ΄μ–ΈνŠΈμΈ‘μ—μ„œ μž„μ˜λ‘œ seq λ₯Ό κ°„λ‹¨νžˆ λ³€κ²½(μžλ™μ¦κ°€ 숫자λ₯Ό 증감 λ˜λŠ” μž…λ ₯)ν•˜μ—¬ μ„œλ²„μΈ‘μœΌλ‘œ μš”μ²­ν•¨μœΌλ‘œμ¨ λ°œμƒν•˜λŠ” λ³΄μ•ˆμ·¨μ•½μ‚¬ν•­μ„ λ°©μ§€ν•˜κΈ° μœ„ν•œ ν‚€

H2 DBMS
  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  @Column(updatable = false, nullable = false, columnDefinition = "LONG COMMENT 'μ„œλ²„λ‚΄μ˜ 고유 식별 ν‚€'")
  private Long seq;
  ...
MySQL DMBS
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(updatable = false, nullable = false, columnDefinition = "BIGINT(10) UNSIGNED COMMENT 'μ„œλ²„λ‚΄μ˜ 고유 식별 ν‚€'")
  private Long seq;
  ...

Infinite Recursion with Jackson JSON and Hibernate JPA issue

Solution

  • Hibernate5Module
  • ToStringBuilder
  • @JsonIdentityInfo
  • @JsonManagedReference

How to

Step 1 :

Add customized ObjectMaper to MappingJackson2HttpMessageConverter

WebMvcConfiguration.java

  @Bean
  public ObjectMapper jsonObjectMapper()
  {
      Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder = Jackson2ObjectMapperBuilder.json();
      ...
      return jackson2ObjectMapperBuilder.build().registerModule(new Hibernate5Module()); // <--- module
  }

and

WebMvcConfiguration.java

  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters)
  {
      MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new Jackson2HttpMessageConverter(jsonObjectMapper()).getJackson2HttpMessageConverter();

      converters.add(jackson2HttpMessageConverter);
  }

Step 2 :

Override toString to ToStringBuilder

This project only applies to common AuditingWithoutIdentityEntity.java.

AuditingWithoutIdentityEntity.java

  public class AuditingWithoutIdentityEntity
  {
      ...
      @Override
      public String toString()
      {
          return new ToStringBuilder(this, ToStringStyle.JSON_STYLE).appendSuper(super.toString()).toString();
      }
  }

Step 3 :

Add @JsonIdentityInfo annotation to Entity class

This project only applies to common AuditingWithoutIdentityEntity.java.

AuditingWithoutIdentityEntity.java

  @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "seq") // <--- annotation
  ...
  public class AuditingWithoutIdentityEntity
  {
      ...
  }

Step 4 :

Add @JsonManagedReference annotation to Entity properties
The forward part of the reference

USER

  @JsonManagedReference
  @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "host")
  @JoinColumn(name = "user_seq", referencedColumnName = "seq")
  private Profile profile;
The back part of the reference

PROFILE

  @JsonBackReference
  @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
  @JoinColumn(updatable = false, nullable = false, name = "user_seq", referencedColumnName = "seq", foreignKey = @ForeignKey(name = "FKEY_USER_SEQ_IN_PROFILE"))
  private User host;

Configuration features

Annotations

Test datas

API

Configuration

By default Spring Boot applications run on port 8080. But may vary depending on what ports are in use on your machine (check the terminal after entering the ./gradlew bootRun command). If you require to change which port the application runs on by default, add the following to:

server: 
  context-path: /
  port: 8080 # ---> change other port via. 9999

Swagger

http://localhost:8080/swagger-ui.html

DBMS

spring: 
  h2: 
    console: 
      path: /h2-console
      enabled: true

Console

http://localhost:8080/h2-console

Conclusion

Configurations

JsonObjectMapper

ToStringBuilder

JsonManagedReference with JsonBackReference

Author

warumono - warumono.for.develop@gmail.com

Releases

No releases published

Packages

No packages published

Languages