Thursday, 7 March 2019

RESTful APIs web services example with Spring Boot, JPA/Hibernate, MySQL, VueJS and Docker

          This tutorial will help you to build a full stack CRUD web application example with Spring Boot,RESTful web services, JPA/Hibernate, MySQL, VueJS and Docker.

Project dependencies

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.learnsoftware.springboot</groupId>
    <artifactId>crud-mysql-vuejs</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Define JPA Entity, Repository and Service

Product.java

package com.learnsoftware.springboot.restful.product;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.math.BigDecimal;
import java.util.Date;

@Entity

@Data
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String description;

    private BigDecimal price;

    @CreationTimestamp
    private Date createdAt;

    @UpdateTimestamp
    private Date updatedAt;
}
ProductRespository.java

package com.learnsoftware.springboot.restful.product;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRespository extends JpaRepository<Product, Long> {
}
ProductService.java

package com.learnsoftware.springboot.restful.product;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service

@RequiredArgsConstructor
public class ProductService {
    private final ProductRespository productRespository;

    public List<Product> findAll() {
        return productRespository.findAll();
    }

    public Optional<Product> findById(Long id) {
        return productRespository.findById(id);
    }

    public Product save(Product stock) {
        return productRespository.save(stock);
    }

    public void deleteById(Long id) {
        productRespository.deleteById(id);
    }
}

Define REST API and Controller

ProductAPI.java

package com.learnsoftware.springboot.restful.product;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;
import java.util.Optional;


@RestController
@RequestMapping("/api/v1/products")
@Slf4j
@RequiredArgsConstructor
public class ProductAPI {
    private final ProductService productService;

    @GetMapping
    public ResponseEntity<List<Product>> findAll() {
        return ResponseEntity.ok(productService.findAll());
    }

    @PostMapping
    public ResponseEntity create(@Valid @RequestBody Product product) {
        return ResponseEntity.ok(productService.save(product));
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> findById(@PathVariable Long id) {
        Optional<Product> stock = productService.findById(id);
        if (!stock.isPresent()) {
            log.error("Id " + id + " is not existed");
            ResponseEntity.badRequest().build();
        }

        return ResponseEntity.ok(stock.get());
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> update(@PathVariable Long id, @Valid @RequestBody Product product) {
        if (!productService.findById(id).isPresent()) {
            log.error("Id " + id + " is not existed");
            ResponseEntity.badRequest().build();
        }

        return ResponseEntity.ok(productService.save(product));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity delete(@PathVariable Long id) {
        if (!productService.findById(id).isPresent()) {
            log.error("Id " + id + " is not existed");
            ResponseEntity.badRequest().build();
        }

        productService.deleteById(id);

        return ResponseEntity.ok().build();
    }
}
ProductController.java

package com.learnsoftware.springboot.restful.product;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ProductController {
    @GetMapping("/")
    public String list(){
        return "products";
    }
}

FreeMarker/HTML View Template

products.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="description" content="">
  <meta name="author" content="">
  <title>Full stack CRUD Example with Spring Boot, MySQL and VueJS</title>
  <link href="https://unpkg.com/bootstrap@3.4.0/dist/css/bootstrap.min.css" rel="stylesheet">
  <link href="/products.css" rel="stylesheet"/>
</head>
<body>
  <div class="container">
    <h1>Product CRUD</h1>
    <main id="app">
      <router-view></router-view>
    </main>
  </div>

  <template id="product">
    <div>
      <h2>{{ product.name }}</h2>
      <b>Description: </b>
      <div>{{ product.description }}</div>
      <b>Price:</b>
      <div>{{ product.price }}<span class="glyphicon glyphicon-euro"></span></div>
      <br/>
      <span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span>
      <a>
        <router-link to="/">Back to product list</router-link>
      </a>
    </div>
  </template>

  <template id="product-list">
    <div>
      <div class="actions">
        <a class="btn btn-default">
          <router-link :to="{path: '/add-product'}">
            <span class="glyphicon glyphicon-plus"></span>
            Add product
          </router-link>
        </a>
      </div>
      <div class="filters row">
        <div class="form-group col-sm-3">
          <input placeholder="Search" v-model="searchKey" class="form-control" id="search-element" requred/>
        </div>
      </div>
      <table class="table">
        <thead>
        <tr>
          <th>Name</th>
          <th>Description</th>
          <th>Price</th>
          <th class="col-sm-2">Actions</th>
        </tr>
        </thead>
        <tbody>
        <tr v-for="product in filteredProducts">
          <!-- tr v-for="product in products" -->
          <!-- tr v-for="product in products | filterBy searchKey in 'name'" -->
          <td>
            <a>
              <router-link :to="{name: 'product', params: {product_id: product.id}}">{{ product.name }}</router-link>
            </a>
          </td>
          <td>{{ product.description }}</td>
          <td>
            {{ product.price }}
            <span class="glyphicon glyphicon-euro" aria-hidden="true"></span>
          </td>
          <td>
            <a class="btn btn-warning btn-xs">
              <router-link :to="{name: 'product-edit', params: {product_id: product.id}}">Edit</router-link>
            </a>
            <a class="btn btn-danger btn-xs">
              <router-link :to="{name: 'product-delete', params: {product_id: product.id}}">Delete</router-link>
            </a>
          </td>
        </tr>
        </tbody>
      </table>
    </div>
  </template>


  <template id="add-product">
    <div>
      <h2>Add new product</h2>
      <form @submit="createProduct">
        <div class="form-group">
          <label for="add-name">Name</label>
          <input class="form-control" id="add-name" v-model="product.name" required/>
        </div>
        <div class="form-group">
          <label for="add-description">Description</label>
          <textarea class="form-control" id="add-description" rows="10" v-model="product.description"></textarea>
        </div>
        <div class="form-group">
          <label for="add-price">Price, <span class="glyphicon glyphicon-euro"></span></label>
          <input type="number" class="form-control" id="add-price" v-model="product.price"/>
        </div>
        <button type="submit" class="btn btn-primary">Create</button>
        <a class="btn btn-default">
          <router-link to="/">Cancel</router-link>
        </a>
      </form>
    </div>
  </template>

  <template id="product-edit">
    <div>
      <h2>Edit product</h2>
      <form @submit="updateProduct">
        <div class="form-group">
          <label for="edit-name">Name</label>
          <input class="form-control" id="edit-name" v-model="product.name" required/>
        </div>
        <div class="form-group">
          <label for="edit-description">Description</label>
          <textarea class="form-control" id="edit-description" rows="3" v-model="product.description"></textarea>
        </div>
        <div class="form-group">
          <label for="edit-price">Price, <span class="glyphicon glyphicon-euro"></span></label>
          <input type="number" class="form-control" id="edit-price" v-model="product.price"/>
        </div>
        <button type="submit" class="btn btn-primary">Save</button>
        <a class="btn btn-default">
          <router-link to="/">Cancel</router-link>
        </a>
      </form>
    </div>
  </template>

  <template id="product-delete">
    <div>
      <h2>Delete product {{ product.name }}</h2>
      <form @submit="deleteProduct">
        <p>The action cannot be undone.</p>
        <button type="submit" class="btn btn-danger">Delete</button>
        <a class="btn btn-default">
          <router-link to="/">Cancel</router-link>
        </a>
      </form>
    </div>
  </template>

  <script src="https://unpkg.com/vue@2.5.22/dist/vue.js"></script>
  <script src="https://unpkg.com/vue-router@3.0.2/dist/vue-router.js"></script>
  <script src="https://unpkg.com/axios@0.18.0/dist/axios.min.js"></script>
  <script src="/products.js"></script>

</body>
</html>

Static Files

products.js

var products = [];

function findProduct (productId) {
  return products[findProductKey(productId)];
}

function findProductKey (productId) {
  for (var key = 0; key < products.length; key++) {
    if (products[key].id == productId) {
      return key;
    }
  }
}

var productService = {
  findAll(fn) {
    axios
      .get('/api/v1/products')
      .then(response => fn(response))
      .catch(error => console.log(error))
  },

  findById(id, fn) {
    axios
      .get('/api/v1/products/' + id)
      .then(response => fn(response))
      .catch(error => console.log(error))
  },

  create(product, fn) {
    axios
      .post('/api/v1/products', product)
      .then(response => fn(response))
      .catch(error => console.log(error))
  },

  update(id, product, fn) {
    axios
      .put('/api/v1/products/' + id, product)
      .then(response => fn(response))
      .catch(error => console.log(error))
  },

  deleteProduct(id, fn) {
    axios
      .delete('/api/v1/products/' + id)
      .then(response => fn(response))
      .catch(error => console.log(error))
  }
}

var List = Vue.extend({
  template: '#product-list',
  data: function () {
    return {products: [], searchKey: ''};
  },
  computed: {
    filteredProducts() {
      return this.products.filter((product) => {
       return product.name.indexOf(this.searchKey) > -1
         || product.description.indexOf(this.searchKey) > -1
         || product.price.toString().indexOf(this.searchKey) > -1
      })
    }
  },
  mounted() {
    productService.findAll(r => {this.products = r.data; products = r.data})
  }
});

var Product = Vue.extend({
  template: '#product',
  data: function () {
    return {product: findProduct(this.$route.params.product_id)};
  }
});

var ProductEdit = Vue.extend({
  template: '#product-edit',
  data: function () {
    return {product: findProduct(this.$route.params.product_id)};
  },
  methods: {
    updateProduct: function () {
      productService.update(this.product.id, this.product, r => router.push('/'))
    }
  }
});

var ProductDelete = Vue.extend({
  template: '#product-delete',
  data: function () {
    return {product: findProduct(this.$route.params.product_id)};
  },
  methods: {
    deleteProduct: function () {
      productService.deleteProduct(this.product.id, r => router.push('/'))
    }
  }
});

var AddProduct = Vue.extend({
  template: '#add-product',
  data() {
    return {
      product: {name: '', description: '', price: 0}
    }
  },
  methods: {
    createProduct() {
      productService.create(this.product, r => router.push('/'))
    }
  }
});

var router = new VueRouter({
 routes: [
  {path: '/', component: List},
  {path: '/product/:product_id', component: Product, name: 'product'},
  {path: '/add-product', component: AddProduct},
  {path: '/product/:product_id/edit', component: ProductEdit, name: 'product-edit'},
  {path: '/product/:product_id/delete', component: ProductDelete, name: 'product-delete'}
 ]
});

new Vue({
  router
}).$mount('#app')
products.css

.actions {
  margin-bottom: 20px;
  margin-top: 20px;
}

Application Configurations

Application.java

package com.learnsoftware.springboot.restful;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
application.properties

spring.datasource.url=jdbc:mysql://hk-mysql:3306/test?useSSL=false
spring.datasource.username=root
spring.datasource.password=learnsoftware
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.jpa.database-platform=org.hibernate.dialect.MySQL57Dialect
spring.jpa.generate-ddl=true
spring.jpa.show-sql=true

spring.freemarker.suffix=.html

Run with Docker

Prepare Dockerfile for Java/Spring Boot application and docker-compose.yml for MySQL Server
Dockerfile

FROM maven:3.5-jdk-8
docker-compose.yml

version: '3'
services:
  hk-mysql:
    container_name: hk-mysql
    image: mysql/mysql-server:5.7
    environment:
      MYSQL_DATABASE: test
      MYSQL_ROOT_PASSWORD: learnsoftware
      MYSQL_ROOT_HOST: '%'
    ports:
    - "3306:3306"
    restart: always

  crud-mysql-vuejs:
    build: .
    volumes:
    - .:/app
    - ~/.m2:/root/.m2
    working_dir: /app
    ports:
    - 8080:8080
    command: mvn clean spring-boot:run
    depends_on:
    - hk-mysql
Type the below command at the project root directory, make sure your local Docker is running

docker-compose up

Run with JDK/OpenJDK, Maven and MySQL Server local

Update hk-mysql  on application.properties to localhost and type the below command at the project root directory

mvn clean spring-boot:run

No comments:

Post a Comment

Unity Top Download

Latest post

An Introduction to Hybris from basics

An Introduction to Hybris from basics:  -- ecommerce site and PCM(Product content Management) solutions. eg. croma website.  -- having sear...

Popular posts