Spring Boot + PostgreSQL + Angular NgZorro Autocomplete

September 27, 2022 No comments autocomplete spring ngzorro form postgresql

Spring Boot + PostgreSQL + Angular NgZorro Autocomplete

1. Introduction

The NgZorro is an Angular library with many valuable components that could be used in web applications. In this tutorial, we will present the NgZorro Autocomplete component powered by data from the Spring Boot application server.

2. Spring Boot Project

The backend site was powered by the Spring framework with several features such as liquibase, Spring Data, Specifications, or Rest Controllers.

2.1. Project Structure

The file structure of the backend project is as follows:

├── main
│   ├── java
│   │   └── com
│   │       └── frontbackend
│   │           └── springboot
│   │               ├── config
│   │               │   └── CorsFilter.java
│   │               ├── dto
│   │               │   └── PersonDto.java
│   │               ├── jpa
│   │               │   ├── model
│   │               │   │   └── PersonEntity.java
│   │               │   ├── repository
│   │               │   │   └── PersonRepository.java
│   │               │   └── utils
│   │               │       └── JpaSpecifications.java
│   │               ├── rest
│   │               │   └── PersonController.java
│   │               ├── service
│   │               │   └── PersonService.java
│   │               └── SpringBootAngularNgZorroAutocomplete.java
│   └── resources
│       ├── application.properties
│       └── db-scripts
│           ├── db-changelog-master.xml
│           └── PERSON-1.0.0.xml

The individual components used on the backend are described below.

2.2. Maven

Maven pom.xml file used to build the project has the following content:

<?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">

    <artifactId>spring-boot-postgresql-angular-ng-zorro-autocomplete</artifactId>

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
        <relativePath/>
    </parent>

    <properties>
        <lombok.version>1.18.24</lombok.version>
        <liquibase-core.version>4.16.0</liquibase-core.version>
    </properties>

    <!-- Add typical dependencies for a web application -->
    <dependencies>
        <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>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
        <dependency>
            <groupId>org.liquibase</groupId>
            <artifactId>liquibase-core</artifactId>
            <version>${liquibase-core.version}</version>
        </dependency>
    </dependencies>

    <!-- Package as an executable jar -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

We used several dependencies:

Notice, that in order to create an executable jar we added a special spring-boot-maven-plugin plugin:

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

2.3. REST API

The application will serve the following services:

URL HTTP Method Action
/api/people?filter={filter} GET Get filtered list of people

2.4. Model, Controller, and Service

The PersonEntity class is a Java representation of tbl_person table in the PostgreSQL database:

package com.frontbackend.springboot.jpa.model;

import java.util.UUID;
import javax.persistence.Entity;
import javax.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.Type;

@Entity(name = "tbl_person")
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
public class PersonEntity {

    @Id
    @Type(type = "pg-uuid")
    private UUID id;

    private String firstName;
    private String lastName;
    private int age;
    private String address;
}

The PersonRepository is our JpaRepository used for CRUD operations on the PersonEntity object. Note that this interface also extends JpaSpecificationExecutor to create queries using Specifications.

package com.frontbackend.springboot.jpa.repository;

import com.frontbackend.springboot.jpa.model.PersonEntity;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface PersonRepository extends JpaRepository<PersonEntity, UUID>, JpaSpecificationExecutor<PersonEntity> {
}

The JpaSpecifications is the utility class used for preparing database queries:

package com.frontbackend.springboot.jpa.utils;

import java.util.List;
import lombok.experimental.UtilityClass;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;

@UtilityClass
public class JpaSpecifications {

    public <T> Specification<T> upperLike(String value, String columnName) {
        return !StringUtils.hasText(value) ?
                null :
                ((root, query, cb) -> cb.like(cb.upper(root.get(columnName)), "%" + value.toUpperCase() + "%"));
    }

    public <T> Specification<T> checkList(List<?> values, String columnName) {
        return (values == null || values.isEmpty()) ?
                null :
                ((root, query, cb) -> root.get(columnName)
                        .in(values));
    }
}

PersonService is responsible for preparing queries and mapping database results into DTO objects:

package com.frontbackend.springboot.service;

import static org.springframework.data.jpa.domain.Specification.where;

import com.frontbackend.springboot.dto.PersonDto;
import com.frontbackend.springboot.jpa.model.PersonEntity;
import com.frontbackend.springboot.jpa.repository.PersonRepository;
import com.frontbackend.springboot.jpa.utils.JpaSpecifications;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestParam;

@RequiredArgsConstructor
@Service
public class PersonService {

    private static final int PAGE_SIZE = 10;
    private static final int PAGE_NUMBER = 0;

    private final PersonRepository personRepository;

    public List<PersonDto> findPerson(@RequestParam String filter) {
        return personRepository.findAll(applyWhere(filter), PageRequest.of(PAGE_NUMBER, PAGE_SIZE))
                .stream()
                .map(this::toPersonDto)
                .collect(Collectors.toList());
    }

    private Specification<PersonEntity> applyWhere(String filter) {
        Specification<PersonEntity> firstName = JpaSpecifications.upperLike(filter, "firstName");
        Specification<PersonEntity> lastName = JpaSpecifications.upperLike(filter, "lastName");

        return where(firstName).or(lastName);
    }

    private PersonDto toPersonDto(PersonEntity entity) {
        return PersonDto.builder()
                .id(entity.getId())
                .name(entity.getFirstName() + " " + entity.getLastName())
                .age(entity.getAge())
                .address(entity.getAddress())
                .build();
    }
}

The PersonController is our REST controller used to handle communication with the frontend site:

package com.frontbackend.springboot.rest;

import com.frontbackend.springboot.dto.PersonDto;
import com.frontbackend.springboot.service.PersonService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/people")
public class PersonController {

    private final PersonService personService;

    @GetMapping
    public List<PersonDto> findPerson(@RequestParam("filter") String filter) {
        return personService.findPerson(filter);
    }
}

The PersonDto is our Data Transfer Object used by REST controller:

package com.frontbackend.springboot.dto;

import java.util.UUID;
import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class PersonDto {
    private UUID id;
    private String name;
    private int age;
    private String address;
}

2.5. Spring Boot Configuration

The Spring Boot configuration file contains Datasource and liquibase properties:

spring.datasource.url=jdbc:postgresql://localhost:5432/db
spring.datasource.username=username
spring.datasource.password=password

spring.liquibase.change-log=classpath:/db-scripts/db-changelog-master.xml

The /db-scripts/db-changelog-master.xml file has the following content:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd">

    <include file="db-scripts/PERSON-1.0.0.xml"/>

</databaseChangeLog>

The PERSON-1.0.0.xml stores information about the structure of the tbl_person table and inserts used to fill that table on startup:

?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd">

    <changeSet author="frontbackend" id="1.0.0-1">
        <createTable tableName="tbl_person">
            <column name="id" type="uuid">
                <constraints nullable="false"
                             primaryKey="true"
                             unique="true"/>
            </column>
            <column name="first_name" type="VARCHAR2(30)"/>
            <column name="last_name" type="VARCHAR2(30)"/>
            <column name="age" type="VARCHAR2(500)"/>
            <column name="address" type="VARCHAR2(200)"/>
        </createTable>
    </changeSet>

    <changeSet author="frontbackend" id="1.0.0-2">
        <sql>
            insert into tbl_person (id, first_name, last_name, age, address) values ('597f4585-aa32-4606-bcc5-ae070616de3b', 'Worden', 'Handes', 29, '4566 Di Loreto Park');
            insert into tbl_person (id, first_name, last_name, age, address) values ('fb3013fb-5343-4832-ac95-1ee6f0fa264d', 'Eddie', 'Kivits', 48, '97525 Oakridge Trail');
...
        </sql>
    </changeSet>

</databaseChangeLog>

3. Angular Project

The frontend site is an Angular application that communicates with the backend using REST API.

3.1. Project Structure

The file structure of the frontend project is as follows:

├── app.component.html
├── app.component.scss
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── app-routing.module.ts
├── icons-provider.module.ts
├── model
│   └── person.ts
├── pages
│   ├── autocomplete
│   │   ├── autocomplete.component.html
│   │   ├── autocomplete.component.scss
│   │   ├── autocomplete.component.ts
│   │   ├── autocomplete.module.ts
│   │   └── autocomplete-routing.module.ts
│   └── welcome
│       ├── welcome.component.html
│       ├── welcome.component.scss
│       ├── welcome.component.ts
│       ├── welcome.module.ts
│       └── welcome-routing.module.ts
└── services
    └── person-http.service.ts

3.2. Creating Angular Project and Components

To create an Angular project with components and services we used Angular-CLI tool:

3.2.1. Create Angular Project

The following command creates a new Angular project called autocomplete:

ng new autocomplete

The output:

? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS   [ https://sass-lang.com/documentation/syntax#scss                ]
CREATE autocomplete/README.md (1058 bytes)
CREATE autocomplete/.editorconfig (274 bytes)
CREATE autocomplete/.gitignore (548 bytes)
CREATE autocomplete/angular.json (3243 bytes)
CREATE autocomplete/package.json (1075 bytes)
CREATE autocomplete/tsconfig.json (863 bytes)
CREATE autocomplete/.browserslistrc (600 bytes)
CREATE autocomplete/karma.conf.js (1429 bytes)
CREATE autocomplete/tsconfig.app.json (287 bytes)
CREATE autocomplete/tsconfig.spec.json (333 bytes)
CREATE autocomplete/.vscode/extensions.json (130 bytes)
CREATE autocomplete/.vscode/launch.json (474 bytes)
CREATE autocomplete/.vscode/tasks.json (938 bytes)
CREATE autocomplete/src/favicon.ico (948 bytes)
CREATE autocomplete/src/index.html (298 bytes)
CREATE autocomplete/src/main.ts (372 bytes)
CREATE autocomplete/src/polyfills.ts (2338 bytes)
CREATE autocomplete/src/styles.scss (80 bytes)
CREATE autocomplete/src/test.ts (745 bytes)
CREATE autocomplete/src/assets/.gitkeep (0 bytes)
CREATE autocomplete/src/environments/environment.prod.ts (51 bytes)
CREATE autocomplete/src/environments/environment.ts (658 bytes)
CREATE autocomplete/src/app/app-routing.module.ts (245 bytes)
CREATE autocomplete/src/app/app.module.ts (393 bytes)
CREATE autocomplete/src/app/app.component.scss (0 bytes)
CREATE autocomplete/src/app/app.component.html (23364 bytes)
CREATE autocomplete/src/app/app.component.spec.ts (1091 bytes)
CREATE autocomplete/src/app/app.component.ts (217 bytes)
✔ Packages installed successfully.
    Directory is already under version control. Skipping initialization of git.
3.2.2. Add ng-zorro-antd library

To add the ng-zorro-antd library we need to run the following command:

ng add ng-zorro-antd

The output:

ℹ Using package manager: npm
✔ Found compatible package version: ng-zorro-antd@13.4.0.
✔ Package information loaded.

The package ng-zorro-antd@13.4.0 will be installed and executed.
Would you like to proceed? Yes
✔ Package successfully installed.
? Enable icon dynamic loading [ Detail: https://ng.ant.design/components/icon/en ] Yes
? Set up custom theme file [ Detail: https://ng.ant.design/docs/customize-theme/en ] No
? Choose your locale code: en_US
? Choose template to create project: sidemenu
CREATE src/app/icons-provider.module.ts (482 bytes)
CREATE src/app/pages/welcome/welcome-routing.module.ts (352 bytes)
CREATE src/app/pages/welcome/welcome.component.scss (0 bytes)
CREATE src/app/pages/welcome/welcome.component.html (0 bytes)
CREATE src/app/pages/welcome/welcome.component.ts (274 bytes)
CREATE src/app/pages/welcome/welcome.module.ts (314 bytes)
UPDATE package.json (1106 bytes)
UPDATE src/app/app.module.ts (1127 bytes)
UPDATE angular.json (3566 bytes)
UPDATE src/app/app-routing.module.ts (416 bytes)
UPDATE src/app/app.component.scss (1245 bytes)
UPDATE src/app/app.component.html (1592 bytes)
UPDATE src/app/app.component.ts (214 bytes)
✔ Packages installed successfully.
3.2.3. Add modules, components, and services

Next single-line command creates the autocomplete module, routing module, and component:

ng g m pages/autocomplete --routing=true && ng g c pages/autocomplete --skip-tests=true -m=pages/autocomplete

The output:

CREATE src/app/pages/autocomplete/autocomplete-routing.module.ts (255 bytes)
CREATE src/app/pages/autocomplete/autocomplete.module.ts (304 bytes)
CREATE src/app/pages/autocomplete/autocomplete.component.scss (0 bytes)
CREATE src/app/pages/autocomplete/autocomplete.component.html (27 bytes)
CREATE src/app/pages/autocomplete/autocomplete.component.ts (300 bytes)
UPDATE src/app/pages/autocomplete/autocomplete.module.ts (399 bytes)

3.3. Model, Components, and Services

The Person interface is a TypeScript representation of the PersonDto object:

export interface Person {
  id: string;
  name: string;
  age: number;
  address: string;
}

The PersonHttpService class is responsible for communication with /api/people endpoint:

import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Person } from '../model/person';
import { environment } from '../../environments/environment';

const BASE_URL = environment.baseUrl;

@Injectable({
  providedIn: 'root'
})
export class PersonHttpService {

  constructor(private http: HttpClient) {
  }

  filter(name: string): Observable<Person[]> {
    let params = new HttpParams().set('filter', name);
    return this.http.get<Person[]>(`${BASE_URL}/api/people`, {params: params});
  }
}

The AutocompleteComponent class handle our NgZorro Autocomplete component:

import { Component, OnInit } from '@angular/core';
import { PersonHttpService } from '../../services/person-http.service';

@Component({
  selector: 'app-autocomplete',
  templateUrl: './autocomplete.component.html',
  styleUrls: ['./autocomplete.component.scss']
})
export class AutocompleteComponent implements OnInit {

  inputValue: string = '';
  filteredOptions: string[] = [];

  constructor(private personHttpService: PersonHttpService) {
  }

  ngOnInit(): void {
  }

  onChange(value: string): void {
    this.personHttpService.filter(value).subscribe(list => {
      this.filteredOptions = list.map(people => people.name);
    });
  }
}

The HTML use by AutocompleteComponent:

<div class="example-input">
  <input
    placeholder='Search people...'
    nz-input
    [(ngModel)]="inputValue"
    (ngModelChange)="onChange($event)"
    [nzAutocomplete]="auto"
  />

  <nz-autocomplete [nzDataSource]="filteredOptions" #auto></nz-autocomplete>
</div>

In AutocompleteModule we imported the necessary modules from Angular and NgZorro library:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { AutocompleteRoutingModule } from './autocomplete-routing.module';
import { AutocompleteComponent } from './autocomplete.component';
import { FormsModule } from '@angular/forms';
import { NzAutocompleteModule } from 'ng-zorro-antd/auto-complete';


@NgModule({
  declarations: [
    AutocompleteComponent
  ],
  imports: [
    FormsModule,
    CommonModule,
    AutocompleteRoutingModule,
    NzAutocompleteModule
  ]
})
export class AutocompleteModule { }

The AutocompleteRoutingModule contains simple routing to AutocompleteComponent:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AutocompleteComponent } from './autocomplete.component';

const routes: Routes = [
  {path: '', component: AutocompleteComponent},
];


@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class AutocompleteRoutingModule {
}

In the main AppRoutingModule we used lazy loading of child components:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', pathMatch: 'full', redirectTo: '/welcome' },
  { path: 'welcome', loadChildren: () => import('./pages/welcome/welcome.module').then(m => m.WelcomeModule) },
  { path: 'autocomplete', loadChildren: () => import('./pages/autocomplete/autocomplete.module').then(m => m.AutocompleteModule) }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

4. Demo

Spring boot angular ng autocomplete demo

5. Conclusion

In this tutorial, we presented how to handle the NgZorro Autocomplete component powered by data from the Spring Boot application.

{{ message }}

{{ 'Comments are closed.' | trans }}