This is a reprint of the README of this repository.
This is an example of a business application that uses Spring Boot on the server side and AngularJS on the client side to access the backend MongoDB.
The database has a collection of brand, model, car and salesperformance. It will have the ability to add, update, delete, and search for these collections, as well as the ability to graph sales performance.
This document is a note on the key technical themes needed to understand the application. To speed up your understanding, it's a good idea to go through Spring's Guide Page Tutorial.
*Caution Not all features work properly at this time as this project has not finished developing features. *
Install MongoDB and start mongod in advance. Leave the authentication function of mongodb unconfigured. (It is in a state where it is installed and started without setting anything)
Git clone this project. Create an appropriate directory and execute the following command in that directory.
$ git clone https://github.com/kazz12211/simple-mongo-crud-app.git
Execute the following command in the same directory to start the application.
$ ./mvnw spring-boot:run
Access the following URL from your browser.
http://localhost:8080
This application is a Spring Boot Maven project. See pom.xml for dependent libraries.
RestController
Generate RestController by @RestController annotation. It seems common in CRUD applications to map data inserts, updates, deletes, and searches to HTTP requests POST, PUT, DELETE, and GET, respectively.
package jp.tsubakicraft.mongocrud.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import jp.tsubakicraft.mongocrud.model.Brand;
import jp.tsubakicraft.mongocrud.service.BrandRepository;
@RestController
public class BrandController {
@Autowired
private BrandRepository repo;
@RequestMapping(value = "/api/brands/listAll", method = RequestMethod.GET)
public List<Brand> listAll() {
Sort sort = new Sort(Sort.Direction.ASC, "name");
return repo.findAll(sort);
}
@RequestMapping(value = "/api/brands", method = RequestMethod.GET)
public Page<?> listBrands(@RequestParam(value = "page", required = true) int page,
@RequestParam(value = "limit", required = true) int limit,
@RequestParam(value = "sortColumn", required = true) String column,
@RequestParam(value = "sortDir", required = true) String dir) {
Sort sort = new Sort(
new Sort.Order("asc".equalsIgnoreCase(dir) ? Sort.Direction.ASC : Sort.Direction.DESC, column));
Pageable pageRequest = new PageRequest(page, limit, sort);
Page<Brand> p = repo.findAll(pageRequest);
return p;
}
@RequestMapping(value = "/api/brands", method = RequestMethod.PUT)
public Brand updateBrand(@RequestBody Brand brand) {
Brand b = repo.findOne(brand.id);
if (b != null) {
b.name = brand.name;
repo.save(b);
}
return b;
}
@RequestMapping(value = "/api/brands", method = RequestMethod.DELETE)
public Brand deleteBrand(@RequestBody Brand brand) {
repo.delete(brand.id);
return brand;
}
@RequestMapping(value = "/api/brands", method = RequestMethod.POST)
public Brand createBrand(@RequestBody Brand brand) {
Brand b = new Brand();
b.name = brand.name;
repo.save(b);
return b;
}
}
This application implements pagination function using ui-bootstrap in UI, but to search for objects on a page-by-page basis, use PageRequest. For example, to search the 11th to 20th Brand objects, call findAll () of PagingAndSortingRepository with PageRequest as an argument as follows.
int page = 1;
int size = 10;
Pageable pageRequest = new PageRequest(page, size);
Page<Brand> page = repo.findAll(pageRequest);
package jp.tsubakicraft.mongocrud.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class ErrorHandler {
@RequestMapping(value = "/{[path:[^\\.]*}")
public String redirect() {
return "forward:/";
}
}
in app.js
var app = angular.module("app", ['ngRoute', 'ngDialog', 'ui.bootstrap', 'chart.js']);
app.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when("/", {
controller: 'home_controller',
templateUrl: 'views/home.html'
})
.when("/brand/", {
controller: 'brand_controller',
templateUrl: 'views/brands.html'
})
.when("/newbrand/", {
controller: 'brand_controller',
templateUrl: 'views/newBrand.html'
})
.when("/model/", {
controller: 'model_controller',
templateUrl: 'views/models.html'
})
.when("/newmodel/", {
controller: 'model_controller',
templateUrl: 'views/newModel.html'
})
.when("/car/", {
controller: 'car_controller',
templateUrl: 'views/cars.html'
})
.when("/newcar/", {
controller: 'car_controller',
templateUrl: 'views/newCar.html'
})
.when("/sales/", {
controller: 'sales_controller',
templateUrl: 'views/sales.html'
})
.when("/newsales/", {
controller: 'sales_controller',
templateUrl: 'views/newSales.html'
})
.otherwise({
redirectTo: "/"
});
}]);
app.config(['$locationProvider', function($locationProvider) {
$locationProvider.html5Mode(true);
}]);
For example, if you want to make a GET request to the / api / brands root of BrandController (REST Controller), use $ http.get (). (See brand_controller.js)
app.controller("brand_controller", function($scope, $http, $location, $q, ngDialog) {
$scope.brands = [];
....
....
$scope.listBrands = function() {
$http.get("/api/brands", {params: {page: $scope.page, limit: $scope.limit, sortColumn: $scope.sortColumn, sortDir: $scope.sortDir}}).then(function(response) {
//I was able to receive the data normally
$scope.brands = response.data;
....
....
}, function(error) {
//HTTP request failed
....
});
};
....
....
$scope.listBrands();
});
Validation can be done in the HTML template, but in this application it is done in the controller. (See brand_controller.js)
....
....
$scope.createBrand = function() {
if(!$scope.validateForm()) {
$http.post("/api/brands", $scope.brand).then(function(response) {
$scope.show = true;
$scope.hide = true;
$scope.hideObj = false;
$scope.showObj = false;
$scope.brandId = "";
$location.path("/brand");
}, function(error) {
$scope.error = error;
});
}
};
....
....
$scope.validateForm = function() {
$scope.validationMessages = [];
if($scope.brand.name == null || $scope.brand.name == null) {
$scope.validationMessages.push("Name is required.");
}
$scope.hasErrors = $scope.validationMessages.length > 0;
return $scope.hasErrors;
};
On the HTML template side, prepare a block to be displayed when there is an error (hasErrors of $ scope is true).
<div class="panel panel-default">
<div class="panel-heading">ADD A BRAND</div>
<form name="brand-form">
<div ng-show="hasErrors">
<div class="alert alert-danger" role="alert">
<div ng-repeat="message in validationMessages">
<strong>{{message}}</strong></br/>
</div>
</div>
</div>
<div class="form-group">
<label for="brand-name">Name</label> <input name="brand-name"
type="text" class="form-control" ng-model="brand.name" required>
</div>
<button class="btn btn-primary" type="submit" ng-click="createBrand()">
<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
Save
</button>
<button class="btn btn-default" ng-click="linkTo('/brand/')">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
Cancel
</button>
</form>
</div>
The ui-bootstrap page (https://angular-ui.github.io/bootstrap/) explains how to do this.
Code on the controller side. Specify the name of the column to sort and the sorting method. (For this application, there are two sorting methods, ASC and DESC) The sort () function is called from the HTML template with the column name as an argument. If the column passed as an argument is the same column as the previous one, switch between ascending and descending order, and if the column is different from the previous one, set it in ascending order and update the column name. Finally, search the DB again. (Since it is a paginated search, the traffic to the database generated by one search is small, so we are re-searching, but if the number of searches is large, it may be better to sort in memory)
app.controller("model_controller", function($scope, $http, $location, $q, ngDialog) {
....
....
$scope.sortColumn = "name";
$scope.sortDir = "ASC";
....
....
$scope.sort = function(column) {
if($scope.sortColumn == column) {
$scope.sortDir = $scope.sortDir == "ASC" ? "DESC" : "ASC";
} else {
$scope.sortDir = "ASC";
$scope.sortColumn = column;
}
$scope.listModels();
};
});
On the HTML template side, specify the controller function to be executed when the mouse click is performed on the column header.
<th class="col-xs-3 col-ms-3 col-md-3 col-lg-4 sortableTableColumn" ng-click="sort('name')">Name</th>
If you use a library such as DataTable.js, which has functions such as pagination and sorting of data to be displayed in the table, it can be done more easily, so when developing an application. Please consider.
It's not limited to Bootstrap CSS, but it's a way to override CSS. The following code overrides Bootstrap's navigation bar style. Set the part of the HTML file that is loading CSS to load after Bootstrap CSS.
.navbar {
margin-bottom: 1px;
border-radius: 0px;
}
.navbar-inverse {
background-color: rgb(12, 140, 213);
border-color: rgb(12,140,213);
}
.navbar-inverse .navbar-brand {
color: #fff;
}
.navbar-inverse .navbar-nav>li>a {
color: #fff;
}
MongoRepository is a sub-interface of PagingAndSortingRepository that has data insertion, update, deletion, search and pagination functions.
The code that sets the MongoDB database selection.
package jp.tsubakicraft.mongocrud.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import com.mongodb.Mongo;
import com.mongodb.MongoClient;
@Configuration
public class MongoConfiguration extends AbstractMongoConfiguration {
@Override
protected String getDatabaseName() {
return "simplecrud";
}
@Override
public Mongo mongo() throws Exception {
return new MongoClient("127.0.0.1", 27017);
}
}
Examples of Brand and Model entities. The Model entity references the Brand entity with the @DBRef annotation. In this case, searching for Model will also search for and combine the referenced Brands.
jp.tsubakicraft.mongocrud.model.Brand.java
package jp.tsubakicraft.mongocrud.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection="brand")
public class Brand {
@Id public String id;
public String name;
public Brand(String id) {
this.id = id;
}
public Brand() {
}
}
jp.tsubakicraft.mongocrud.model.Model.java
package jp.tsubakicraft.mongocrud.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection="model")
public class Model {
@Id public String id;
public String name;
@DBRef public Brand brand;
public Model() {
}
public Model(String id) {
this.id = id;
}
}
How to specify DBRef properties in search criteria.
@Query(value="{brand.$id : ?0}")
public List<Model> findByBrandId(ObjectId brandId);
How to get the number of collections. Set count = true as a parameter of @Query annotation.
@Query(value="{brand.$id : ?0}", count=true)
public Long countBrandModel(ObjectId brandId);
Call a method of MongoRepository using PageRequest.
@Autowired
private BrandRepository repo;
...
@RequestMapping(value="/api/brands", method=RequestMethod.GET)
public Page<Brand> listBrands(@RequestParam(value="page", required=true) int page, @RequestParam(value="limit", required=true) int limit) {
Pageable pageRequest = new PageRequest(page, limit);
return repo.findAll(pageRequest);
}