tjholmes66
tjholmes66

Reputation: 2008

Record Class with findAll with Spring Data JPA

I am using SpringBoot 3.4.2 (latest as of right now) I can create RESTful controllers that access the Service, and then we make a Repository call to get data from the database which all works 100%. I completely understand in why we should NOT, in most cases, return the Entity to whatever is calling the RESTful API. In some cases we simply do not want data to be returned, and in other cases we want to aggregate some data or do computations on data, and so a DTO would be used for this. I wholly embrace the Entity <> DTO mapping.

Now, I see that a Record exists, and this with SMALL data can be used as a DTO. From what I have read that Records are immutable. I have read a lot of literature on DTO (with Lombok) is different than Records. Now if I really want to keep my Repositories as they are, I can simply write for myself the code to map from Entities to Records, but do we want to do this? From other examples on the Internet, I can see we can use Spring Data JPA to load data directly into Records, and if the fields are exactly the same then the mapping is quite easy. If in some reasons the data is different, then there is a way to fix that also, but I'm not there yet. So, I have a question, and it's a small issue, but maybe I am missing something. So, let me put the code out there.

@Entity
@Table(name = "company")
public class CompanyEntity implements Serializable
{
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   @Column(name = "company_id")
   private long companyId;
   @Column(name = "active")
   private boolean active;
   @Column(name = "code")
   private String companyCode;
   @Column(name = "name")
   private String companyName;
   @Column(name = "description")
   private String description;
   @Column(name = "address1")
   private String address1;
   @Column(name = "address2")
   private String address2;
   @Column(name = "city")
   private String city;
   @Column(name = "state")
   private String state;
   @Column(name = "zip")
   private String zip;
}

And now we have the Record class with the same exact fields:

public record CompanyRecord(long companyId, boolean active, 
String companyCode, 
String companyName, String description, String address1, 
String address2, String city, String state, String zip)
implements Serializable {}

And now I have the Repository code:

@Repository("CompanyRepository")
public interface CompanyRepository extends JpaRepository<CompanyEntity, Long>
{
    // This works great!  No issus here!
    public CompanyRecord findByCompanyId(long companyId);

    // This works great!  No issus here!
    public List<CompanyEntity> findAll();

    // This does NOT work!
    // IDE Reported Error:
    // The return type is incompatible with ListCrudRepository<CompanyEntity,Long>.findAll()
    public List<CompanyRecord> findAll();

   // This works great!  No issus here!
   List<CompanyRecord> findByCompanyCode(String companyCode);
}

So, I don't know if this is more of a JPARepository issue, or how to fix this. What is even more confusing is the JUnit Testing.

@Test
public void testFindById_Entity()
{
    // works as expected, no problems
    // surprising because the Repo wanted a CompanyRecord, and not an entity
    long companyId = 1;
    CompanyEntity companyEntity = companyRepository.findById(companyId).orElse(null);
    assertNotNull(companyEntity);
}

@Disabled
@Test
public void testFindById_Record()
{
    long companyId = 1;
    // doesn't work, and gives an IDE error
    // Type mismatch: cannot convert from CompanyEntity to CompanyRecord
    // I would expect this to work since the interface specifically asks for a Record 
    CompanyRecord companyRecord = companyRepository.findById(companyId).orElse(null);
    assertNotNull(companyRecord);
}

@Test
public void testFindByCode_Entity()
{
    String companyCode = "IBM";
    // does not work, AS EXPECTED since the interface wants a record
    List<CompanyEntity> companyEntityList = companyRepository.findByCompanyCode(companyCode);
    assertNotNull(companyEntityList);
}

@Test
public void testFindByCode()
{
    String companyCode = "IBM";
    // this works as expected, the interface asks for a record
    List<CompanyRecord> companyRecordList = companyRepository.findByCompanyCode(companyCode);
    assertNotNull(companyRecordList);
}

@Test
public void testFindAll_Entity()
{
    // works as expected
    List<CompanyEntity> companyEntityList = companyRepository.findAll();
    assertNotNull(companyEntityList);
}

I absolutely see some very confusing points trying to do this this way. Sometimes thing work as I would expect them to, and sometimes they don't. Makes me feel like 'Record' isn't quite ready for prime time. Or, I could go back to the manual way of using an Entity for all my repository work, and manually ceate records in the Service, but I can see how using a Record in the Spring Data JPA would save boilerplate code.

Anyone have similar experiemces, or could shed more light on Java Record in Spring Data JPA. Tha Baeldung page is great information, but in their examples they are defintely using criteriaQuery, and I'm not going down that road. I usually use HQL for my specific requests and that might be what I have to do.

UPDATE #1

So, I followed the comments on here, and I completely understand the requirements of Hibernate Entities and Records, I get it. I also followed the other comments about using JPQL, and although it seems to compile completely failed my JUnit Test because the converter didn't exist, so I got that error.

So, it seems, in the Repository Interface:

// gives Convert error
@Query(value = "SELECT c from CompanyEntity c")
List<CompanyRecord> companyRecordList findAllCustomerRecords();

// I think this might work but didn't test it
@Query(value = "SELECT new CompanyRecord( c.id, c.name, etc) from CompanyEntity c")
List<CompanyRecord> companyRecordList findAllCustomerRecords();

So, I know Records have been out since Java14, and more officially on Java16, but in all the places I've worked, we've never used the Record object. This whole Record thing came to me when I was working on a personal project to use GraphQL, and I was surprised to see they used Record. I was trying to use Record in a way that seemed to be the industry standard way of doing things, but I don't think that's quite been established yet.

At this point, I understand why Record exists, but I don't think I need to replace DTOs with Records. We have various tools that convert Entity to DTO and vice-versa, but it doesn't seem to have caught up to Records yet. I was hoping to leverage Spring Data JPA for that mapping, but it seems like that take some effort, and IMHO, that's not the right place to be doing that.

So, Thanks for all the answers, I look forward to more thoughts on this, but I'm going to keep my Repository strictly with Entity and go back to using DTO's (with Lombok) and using MapStruct or other tools to do my mapping. Seems like a cleaner solution with less boiler-plate code.

Upvotes: 1

Views: 95

Answers (1)

AbstractKamen
AbstractKamen

Reputation: 66

Records cannot be entities. It's either or. Apparently, spring-data-jpa does indeed map by itself entities to records if the fields are matching. However, then you have nonsense like this

    ...
    // This works great!  No issus here!
    public List<CompanyEntity> findAll();

    // This does NOT work!
    // IDE Reported Error:
    // The return type is incompatible with ListCrudRepository<CompanyEntity,Long>.findAll()
    public List<CompanyRecord> findAll();
    ...

In order to get around this you can rename the method to something else like findAllCompanyRecords or something in order to avoid the conflict.

Another thing you can do is implement the repository yourself and handle the mapping between entity and record. This way you won't have methods in your repository which return an entity and other methods which return a record which is quite bizare in my opinion. Baeldung has a simple example with a custom repo https://www.baeldung.com/spring-jpa-java-records but going this route means you make a custom repository interface and implementation and then you have to change all your repositories which you said you didn't want.

There's always the giga hack way which you make findAll() return a List without its type.

Disgusting example:

interface TestInterface {
    List<Number> findAll();
}

interface TestInterface2 extends TestInterface {
    List findAll();
}

class TestImpl implements TestInterface2 {

    @Override
    public List findAll() {
        return List.of("not", "expected");
    }
}

    ...
    public static void main(String[] args) {
        TestInterface test = new TestImpl();

        List<Number> all = test.findAll(); // nope does not contain any Number
        System.out.println(all);
    }
    ...

Do what you think is best. In my experience repositories tend to multiply quite a lot and changing things in them can lead to many many changes, so getting them right is important.

Upvotes: 1

Related Questions