class-based Projections

Class-based projections use class-based DTOs (Data Transfer Objects) which have all the required attributes of the entity class. these access modifier getters can be used the same way as projection interface methods.

The Spring Data Jpa query execution engine will not create a proxy instance of the class-based projections DTO. Class-based projections also don't support nested projections.

drop database if exists AppleDb;
create database AppleDb;
use AppleDb;
create TABLE apple ( apple_id BIGINT NOT NULL, apple_name VARCHAR(255) DEFAULT NULL, taste VARCHAR(255) DEFAULT NULL, PRIMARY KEY (apple_id)) ENGINE=INNODB;
create TABLE hibernate_sequence ( next_val BIGINT) ENGINE=INNODB;
insert into hibernate_sequence values ( 1 );
insert into apple (apple_name, apple_id, taste) values ('Macintosh', 1, 'tangy');
insert into apple (apple_name, apple_id, taste) values ('Fuji', 2, 'sweet');
insert into apple (apple_name, apple_id, taste) values ('Gala', 3, 'tangy');
insert into apple (apple_name, apple_id, taste) values ('Jonagold', 4, 'sweet');
insert into apple (apple_name, apple_id, taste) values ('GrannySmith', 5, 'tangy');
package com.sujan.example.jpa.controller;

import com.sujan.example.jpa.entity.AppleSummary;
import com.sujan.example.jpa.repository.AppleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class AppleController {
    @Autowired
    private AppleRepository appleRepository;

    @GetMapping
    void projection() {
        List<AppleSummary> findByTasteProjection = appleRepository.findByTaste("tangy");
        for (AppleSummary appleSummary : findByTasteProjection) {
            System.out.println("Apple Name And Taste- " + appleSummary.toString());
        }
    }
}
package com.sujan.example.jpa.entity;

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

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity
@ToString
@NoArgsConstructor
@AllArgsConstructor()
public class Apple {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long appleId;
    private String appleName;
    private String taste;
}
package com.sujan.example.jpa.entity;

import java.util.Objects;

public class AppleSummary {
    public AppleSummary(String appleName, String taste) {
        this.appleName = appleName;
        this.taste = taste;
    }

    private String appleName;
    private String taste;

    public String getAppleName() {
        return appleName;
    }

    public String getTaste() {
        return taste;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof AppleSummary)) return false;
        AppleSummary that = (AppleSummary) o;
        return Objects.equals(appleName, that.appleName) && Objects.equals(taste, that.taste);
    }

    @Override
    public int hashCode() {
        return Objects.hash(appleName, taste);
    }

    @Override
    public String toString() {
        System.out.println("AppleSummary.toString");
        return "AppleSummary{" +
                "appleName='" + appleName + '\'' +
                ", taste='" + taste + '\'' +
                '}';
    }
}
package com.sujan.example.jpa.repository;

import com.sujan.example.jpa.entity.Apple;
import com.sujan.example.jpa.entity.AppleSummary;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface AppleRepository extends JpaRepository<Apple, Long> {
    List<AppleSummary> findByTaste(String taste);
}
package com.sujan.example.jpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(JpaApplication.class, args);
    }
}
spring.datasource.url=jdbc:mysql://localhost:3306/AppleDb?autoReconnect=true&useSSL=false&createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.datasource.initialization-mode=always
plugins {
    id 'org.springframework.boot' version '2.3.3.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}
group = 'com.sujan'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
repositories {
    mavenCentral()
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    annotationProcessor 'org.projectlombok:lombok'
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}
test {
    useJUnitPlatform()
}
curl --location --request GET 'http://localhost:8080/'
select apple0_.apple_name as col_0_0_, apple0_.taste as col_1_0_ from apple apple0_ where apple0_.taste=?
AppleSummary.toString
Apple Name And Taste- AppleSummary{appleName='Macintosh', taste='tangy'}
AppleSummary.toString
Apple Name And Taste- AppleSummary{appleName='Gala', taste='tangy'}
AppleSummary.toString
Apple Name And Taste- AppleSummary{appleName='GrannySmith', taste='tangy'}

 

Avoid boilerplate code for projection DTOs

Class based projection DTOs have a lot of boiler plate codes. Project Lombok provides @Data, @Entity, @ToString, @NoArgsConstructor, @AllArgsConstructor annotations to simplyfy the code.

drop database if exists AppleDb;
create database AppleDb;
use AppleDb;
create TABLE apple ( apple_id BIGINT NOT NULL, apple_name VARCHAR(255) DEFAULT NULL, taste VARCHAR(255) DEFAULT NULL, PRIMARY KEY (apple_id)) ENGINE=INNODB;
create TABLE hibernate_sequence ( next_val BIGINT) ENGINE=INNODB;
insert into hibernate_sequence values ( 1 );
insert into apple (apple_name, apple_id, taste) values ('Macintosh', 1, 'tangy');
insert into apple (apple_name, apple_id, taste) values ('Fuji', 2, 'sweet');
insert into apple (apple_name, apple_id, taste) values ('Gala', 3, 'tangy');
insert into apple (apple_name, apple_id, taste) values ('Jonagold', 4, 'sweet');
insert into apple (apple_name, apple_id, taste) values ('GrannySmith', 5, 'tangy');
package com.sujan.example.jpa.controller;

import com.sujan.example.jpa.entity.AppleSummary;
import com.sujan.example.jpa.repository.AppleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class AppleController {
    @Autowired
    private AppleRepository appleRepository;

    @GetMapping
    void projection() {
        List<AppleSummary> findByTasteProjection = appleRepository.findByTaste("tangy");
        for (AppleSummary appleSummary : findByTasteProjection) {
            System.out.println("Apple Name And Taste- " + appleSummary.toString());
        }
    }
}
package com.sujan.example.jpa.entity;

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

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Apple {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long appleId;
    private String appleName;
    private String taste;
}
package com.sujan.example.jpa.entity;

import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
@EqualsAndHashCode
@AllArgsConstructor
public class AppleSummary {
    private String appleName;
    private String taste;
}
package com.sujan.example.jpa.repository;

import com.sujan.example.jpa.entity.Apple;
import com.sujan.example.jpa.entity.AppleSummary;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface AppleRepository extends JpaRepository<Apple, Long> {
    List<AppleSummary> findByTaste(String taste);
}
package com.sujan.example.jpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(JpaApplication.class, args);
    }
}
spring.datasource.url=jdbc:mysql://localhost:3306/AppleDb?autoReconnect=true&useSSL=false&createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.datasource.initialization-mode=always
plugins {
    id 'org.springframework.boot' version '2.3.3.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}
group = 'com.sujan'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
repositories {
    mavenCentral()
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    annotationProcessor 'org.projectlombok:lombok'
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}
test {
    useJUnitPlatform()
}
curl --location --request GET 'http://localhost:8080/'
select apple0_.apple_name as col_0_0_, apple0_.taste as col_1_0_ from apple apple0_ where apple0_.taste=?
Apple Name And Taste- AppleSummary(appleName=Macintosh, taste=tangy)
Apple Name And Taste- AppleSummary(appleName=Gala, taste=tangy)
Apple Name And Taste- AppleSummary(appleName=GrannySmith, taste=tangy)

 

above annotation provides by Project Lombok still have a lot of annotations, Project Lombok provides @Value annotation which is equivelent of @Getter @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) @AllArgsConstructor @ToString @EqualsAndHashCode.

drop database if exists AppleDb;
create database AppleDb;
use AppleDb;
create TABLE apple ( apple_id BIGINT NOT NULL, apple_name VARCHAR(255) DEFAULT NULL, taste VARCHAR(255) DEFAULT NULL, PRIMARY KEY (apple_id)) ENGINE=INNODB;
create TABLE hibernate_sequence ( next_val BIGINT) ENGINE=INNODB;
insert into hibernate_sequence values ( 1 );
insert into apple (apple_name, apple_id, taste) values ('Macintosh', 1, 'tangy');
insert into apple (apple_name, apple_id, taste) values ('Fuji', 2, 'sweet');
insert into apple (apple_name, apple_id, taste) values ('Gala', 3, 'tangy');
insert into apple (apple_name, apple_id, taste) values ('Jonagold', 4, 'sweet');
insert into apple (apple_name, apple_id, taste) values ('GrannySmith', 5, 'tangy');
package com.sujan.example.jpa.controller;

import com.sujan.example.jpa.entity.AppleSummary;
import com.sujan.example.jpa.repository.AppleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class AppleController {
    @Autowired
    private AppleRepository appleRepository;

    @GetMapping
    void projection() {
        List<AppleSummary> findByTasteProjection = appleRepository.findByTaste("tangy");
        for (AppleSummary appleSummary : findByTasteProjection) {
            System.out.println("Apple Name And Taste- " + appleSummary.toString());
        }
    }
}
package com.sujan.example.jpa.entity;

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

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity
@ToString
@NoArgsConstructor
@AllArgsConstructor()
public class Apple {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long appleId;
    private String appleName;
    private String taste;
}
package com.sujan.example.jpa.entity;

import lombok.Value;

@Value
public class AppleSummary {
    private String appleName;
    private String taste;
}
package com.sujan.example.jpa.repository;

import com.sujan.example.jpa.entity.Apple;
import com.sujan.example.jpa.entity.AppleSummary;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface AppleRepository extends JpaRepository<Apple, Long> {
    List<AppleSummary> findByTaste(String taste);
}
package com.sujan.example.jpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(JpaApplication.class, args);
    }
}
spring.datasource.url=jdbc:mysql://localhost:3306/AppleDb?autoReconnect=true&useSSL=false&createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.datasource.initialization-mode=always
plugins {
    id 'org.springframework.boot' version '2.3.3.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}
group = 'com.sujan'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
repositories {
    mavenCentral()
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    annotationProcessor 'org.projectlombok:lombok'
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}
test {
    useJUnitPlatform()
}
curl --location --request GET 'http://localhost:8080/'
select apple0_.apple_name as col_0_0_, apple0_.taste as col_1_0_ from apple apple0_ where apple0_.taste=?
Apple Name And Taste- AppleSummary(appleName=Macintosh, taste=tangy)
Apple Name And Taste- AppleSummary(appleName=Gala, taste=tangy)
Apple Name And Taste- AppleSummary(appleName=GrannySmith, taste=tangy)

follow us on