Showing posts with label repository. Show all posts
Showing posts with label repository. Show all posts

Tuesday, March 26, 2019

Spring Boot Data JPA (with REST)

0. Intro 

It's getting more and more easier to create rest services with spring boot : now It's possible to create them with just several annotations! Let's see :)

1. Project structure

I created simple gradle project with some tests:



2.  Dependencies (build.gradle)

We need:
- spring-data-jpa   - to use auto-generated by Spring repositories
- sparing-data-rest - to expose these repositories by REST services
- lombok - to reduce code for entities classes
- h2 - as database

plugins {
   id 'org.springframework.boot' version '2.1.3.RELEASE'   id 'java'}

apply plugin: 'io.spring.dependency-management'
group = 'com.demien'version = '0.0.1-SNAPSHOT'sourceCompatibility = '1.8'
repositories {
   mavenCentral()
}

dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-data-jpa'   implementation 'org.springframework.boot:spring-boot-starter-data-rest'   runtimeOnly 'com.h2database:h2'   compileOnly('org.projectlombok:lombok')
   testImplementation 'org.springframework.boot:spring-boot-starter-test'}


3. Main runner class 

Nothing interesting is here - just spring boot runner:

package com.demien.sprdata;
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplicationpublic class SprdataApplication {

   public static void main(String[] args) {
      final SpringApplication app = new SpringApplication(SprdataApplication.class);      app.run(args);   }

}



4. Domain entities 

I create only 2 entities: "parent" : UserGroup and "child": User.

4.1. UserGroup entity 

Thanks to lombok, it's very simple, we just have to list properties. Also we have to annotate it as "@Entity" and also I'm defining named query "UserGroup.namedQueryByName"| :

package com.demien.sprdata.domain;
import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import javax.persistence.NamedQuery;
import lombok.Getter;import lombok.Setter;
@Entity@Getter@Setterpublic class UserGroup {

   @Id   @GeneratedValue(strategy = GenerationType.AUTO)
   private Long id;   private String name;   private String description;
   public UserGroup() {

   }

}



4.2. User entity

It's little bit more complicated: we have to define relationship with parent: we will be joining by field groupId. And I'm also defining named query.

package com.demien.sprdata.domain;
import javax.persistence.Entity;import javax.persistence.FetchType;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import javax.persistence.JoinColumn;import javax.persistence.ManyToOne;import javax.persistence.NamedQuery;
import lombok.Getter;import lombok.Setter;
@Getter@Setter@Entity@NamedQuery(name = "User.namedQueryByName", query = "SELECT u FROM User u WHERE u.name = :name ")
public class User {

   @Id   @GeneratedValue(strategy = GenerationType.AUTO)
   private Long id;   @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "groupId")
   private UserGroup group;   private String name;   private Integer salary;
   public User() {
   }

}


5. Repositories

The magic is happening here. We just have to define interface ..... and that's it! We don't have to create the implementation - it will be created by Spring!!!

5.1. UserGroup repository 

I made it very simple: we're just extending CrudRepository and adding several methods.
"Crud" means Create, Update and Delete - so all these methods will be available in implementation which will be generated by Spring. And also 2 more methods added: findAll and count;

package com.demien.sprdata.repository;
import org.springframework.data.repository.CrudRepository;
import com.demien.sprdata.domain.UserGroup;
public interface UserGroupRepository extends CrudRepository<UserGroup, Long> {

   Iterable<UserGroup> findAll();
   long count();
}


5.2. UserRepository 

It's more complicated. First of all, we're annotation it with @RepositoryRestResource to expose methods as rest services.  Next - I'm using PagingAndSorting repository, so paging and sorting features will be available. Also I'm adding a lot of methods which will be generated by Spring:
- we can use patter [find | count] By [fieldName].
- we can even use fields of parent entity (UserGroup which is accessed by "group" field in User entity)
- we can define our own queries


package com.demien.sprdata.repository;
import java.util.List;
import org.springframework.data.jpa.repository.Query;import org.springframework.data.repository.PagingAndSortingRepository;import org.springframework.data.repository.query.Param;import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import com.demien.sprdata.domain.User;
//http://localhost:8080/users/@RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
   Iterable<User> findAll();
   long count();
   List<User> findByName(String name);
   Long deleteByName(String name);
   Long countByGroupName(String groupName);
   // find by parent entity : Group   List<User> findByGroupName(String name);
   // defining custom query   @Query("SELECT u FROM User u WHERE u.name LIKE CONCAT('%', :name, '%') ")
   List<User> queryByName(@Param("name") String name);
   // using named query defined in entity class   List<User> namedQueryByName(@Param("name") String name);
   @Query(value = "SELECT * FROM User WHERE name = :name ", nativeQuery = true)
   List<User> nativeQueryByName(@Param("name") String name);
}

6. Testing 

And now let's test how this magic works

6.1 UserGroupRepository -  test

Here I'm testing just CRUD operations:


package com.demien.sprdata;
import static org.junit.Assert.assertEquals;import static org.junit.Assert.assertFalse;import static org.junit.Assert.assertTrue;
import java.util.ArrayList;import java.util.List;import java.util.Optional;
import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;import org.springframework.test.context.junit4.SpringRunner;
import com.demien.sprdata.domain.UserGroup;import com.demien.sprdata.repository.UserGroupRepository;
@DataJpaTest@RunWith(SpringRunner.class)
public class UserGroupRepositoryTest {

   @Autowired   private UserGroupRepository groupRepository;
   @Autowired   private TestEntityManager em;
   @Test   public void findAllTest() {
      final List<UserGroup> groups = new ArrayList<>();      groupRepository.findAll().forEach(group -> groups.add(group));      assertEquals(4, groups.size());   }

   @Test   public void checkUserGroupCount() {
      assertEquals(4, groupRepository.count());
   }

   @Test   public void findOne() {
      Optional<UserGroup> opGroup = groupRepository.findById(1001L);      assertTrue(opGroup.isPresent());      assertEquals("ADM", opGroup.get().getName());
      groupRepository.deleteById(1001L);      opGroup = groupRepository.findById(1001L);      assertFalse(opGroup.isPresent());
   }

   @Test   public void createNewTest() {
      final UserGroup newGroup = new UserGroup();      newGroup.setDescription("Created");      groupRepository.save(newGroup);      assertTrue(newGroup.getId() != null);
      em.flush();      final Optional<UserGroup> loaded = groupRepository.findById(newGroup.getId());      assertTrue(loaded.isPresent());      assertTrue(loaded.get().getDescription().equals("Created"));
      groupRepository.deleteById(newGroup.getId());
   }

}



6.2. UserRepository - test

And most interesting is happening here: I'm testing paging, sorting, named queries, native queries....

package com.demien.sprdata;
import static org.junit.Assert.assertTrue;
import java.util.List;
import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;import org.springframework.data.domain.Page;import org.springframework.data.domain.PageRequest;import org.springframework.data.domain.Sort;import org.springframework.test.context.junit4.SpringRunner;
import com.demien.sprdata.domain.User;import com.demien.sprdata.repository.UserRepository;
@DataJpaTest@RunWith(SpringRunner.class)
public class UserRepositoryTest {

   @Autowired   UserRepository userRepository;
   @Autowired   TestEntityManager em;
   @Test   public void sortTest() {
      Sort sort = new Sort(Sort.Direction.ASC, "group_id").and(new Sort(Sort.Direction.DESC, "name"));      Iterable<User> users = userRepository.findAll(sort);      User first = users.iterator().next();      assertTrue(first.getGroup().getId() == 1001L);      assertTrue(first.getName().equals("Victor"));   }

   @Test   public void pagingTest() {
      final PageRequest pageRequest = PageRequest.of(0, 2);      final Page<User> userPage = userRepository.findAll(pageRequest);      assertTrue(userPage.getNumberOfElements() == 2);      assertTrue(userPage.getTotalPages() == 4);
   }

   @Test   public void findTest() {
      List<User> users = userRepository.findByName("Joe");      assertTrue(users.size() == 1);      assertTrue(users.get(0).getName().equals("Joe"));
      Long countByGroupName = userRepository.countByGroupName("ADM");      assertTrue(countByGroupName == 3L);
      users = userRepository.findByGroupName("ADM");      assertTrue(users.size() == 3);
      users = userRepository.queryByName("a");      assertTrue(users.size() == 5);      assertTrue(users.get(0).getName().contains("a"));
      users = userRepository.namedQueryByName("Charles");      assertTrue(users.size() == 1);      assertTrue(users.get(0).getId() == 104L);
      users = userRepository.nativeQueryByName("Mario");      assertTrue(users.size() == 1);      assertTrue(users.get(0).getId() == 105L);
   }

}


7. Rest services

And final thing: let's now run our application runner and open in browser: http://localhost:8080/users/

It should be something like :


{
  "_embedded" : {
    "users" : [ {
      "name" : "Joe",
      "salary" : 100,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/101"
        },
        "user" : {
          "href" : "http://localhost:8080/users/101"
        },
        "group" : {
          "href" : "http://localhost:8080/users/101/group"
        }
      }
    }, {
      "name" : "Huan",
      "salary" : 200,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/102"
        },
        "user" : {
          "href" : "http://localhost:8080/users/102"
        },
        "group" : {
          "href" : "http://localhost:8080/users/102/group"
        }
      }
    }, {
      "name" : "Michael",
      "salary" : 300,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/103"
        },
        "user" : {
          "href" : "http://localhost:8080/users/103"
        },
        "group" : {
          "href" : "http://localhost:8080/users/103/group"
        }
      }
    }, {
      "name" : "Charles",
      "salary" : 100,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/104"
        },
        "user" : {
          "href" : "http://localhost:8080/users/104"
        },
        "group" : {
          "href" : "http://localhost:8080/users/104/group"
        }
      }
    }, {
      "name" : "Mario",
      "salary" : 200,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/105"
        },
        "user" : {
          "href" : "http://localhost:8080/users/105"
        },
        "group" : {
          "href" : "http://localhost:8080/users/105/group"
        }
      }
    }, {
      "name" : "Jan",
      "salary" : 300,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/106"
        },
        "user" : {
          "href" : "http://localhost:8080/users/106"
        },
        "group" : {
          "href" : "http://localhost:8080/users/106/group"
        }
      }
    }, {
      "name" : "Victor",
      "salary" : 500,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/107"
        },
        "user" : {
          "href" : "http://localhost:8080/users/107"
        },
        "group" : {
          "href" : "http://localhost:8080/users/107/group"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/users{?page,size,sort}",
      "templated" : true
    },
    "profile" : {
      "href" : "http://localhost:8080/profile/users"
    },
    "search" : {
      "href" : "http://localhost:8080/users/search"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 7,
    "totalPages" : 1,
    "number" : 0
  }
}

- HATEOAS is in place  !

8. The end

As I mentioned at the beginning, with spring-boot stack we can create rest services with DB repositories by just adding few annotations! Full source code can be downloaded from here