Access SAP HANA in the NEO environment

In the first part of the blog series I introduced you to Java development based on the Spring framework. Using a simple RestController the basics were shown.

What would an application be without access to a database. In the SAP Cloud context it is obvious that SAP HANA is used as database. In this part of the blog series I show you how to access the database in the NEO environment.

JNDI – The yellow pages

The HANA database is accessed via JNDI, in my opinion one of the most exciting technologies in the Java environment. With a JNDI lookup resources managed by the runtime environment can be loaded. The idea behind it is very simple – the runtime environment takes care of instantiating the required classes and makes them available to the application.

In order for the application presented here to support JNDI, some activities must be carried out. In the first step it is necessary to create a subdirectory called webapp in the directory src > main and in this subdirectory a subdirectory called WEB-INF. In the directory src > main > webapp > WEB-INF it is only necessary to create a file named web.xml.

Folderstructure

Folderstructure für web.xml

Those resources that are managed by JNDI must be listed in the web.xml file as resource reference (resource-ref). The name (res-ref-name) under which the resource is addressed and the underlying Java class (res-type) must be defined.

 

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
 version="3.0">
   <resource-ref>
     <res-ref-name>jdbc/DefaultDB</res-ref-name>
     <res-type>javax.sql.DataSource</res-type>
   </resource-ref>
</web-app>

Declare Spring Data Dependency

The first step has already been completed. Since we do not implement the access to the database with SQL commands ourselves, but use Spring Data, we have to define a corresponding dependency in pom.xml.

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

Since Spring Data supports a variety of databases, it is necessary to tell the runtime environment which database to use. This is done using the application.properties file. If this file does not yet exist, it must be created in the directory src > main > resources. The fully qualified class name of the HANA driver is com.sap.db.jdbc.Driver

spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.HANAColumnStoreDialect
spring.jpa.properties.hibernate.connection.pool_size = 10
spring.datasource.driverClassName=com.sap.db.jdbc.Driver

@Configuration

Spring @Configuration annotation is part of the Spring Core Framework. The Spring annotation indicates that the class has @Bean definition methods. This allows the Spring container to process the class and generate Spring Beans at runtime that can be used in the application.

For use in the Neo Stack, the data source must be loaded using JNDI. A corresponding class must be created for this. The following code snippet shows the complete class.

Neo Datasource Configuration

package at.clouddna.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;

import javax.sql.DataSource;
import java.sql.SQLException;

@Configuration
@Profile({"neo"})
public class NeoConfig 
{	
	@Bean(destroyMethod="")
	public DataSource jndiDataSource() throws IllegalArgumentException, SQLException
	{
		JndiDataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
		DataSource ds = dataSourceLookup.getDataSource("java:comp/env/jdbc/DefaultDB");
		return ds;
	}
}
The JNDI lookup aims at the name java:comp/env/jdbc/DefaultDB. The prefix java:comp/env/ is always the same in the SAP Cloud Platform. The name jdbc/DefaultDB defined behind it corresponds to the res-ref-name in the web.xml

Development of an entity class

The use of Spring-Data allows a very efficient development of the persistence layer. Spring-Data is based on the Hibernate Framework. As soon as we create a class and assign the annotation @Entity to it, a corresponding table is created in the underlying database. In the following code snippet I show you a simple user class.

User.java

package at.clouddna.demo.model;

import javax.persistence.*;

@Entity
public class User {

    @Column(nullable = false)
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Long id;

    private String firstname;
    private String lastname;

    public Long getId() {
        return this.id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }

    public String getFirstname() {
        return this.firstname;
    }

    public void setLastname(String lastname) {
        this.lastname = lastname;
    }

    public String getLastname() {
        return this.lastname;
    }
}

CRUD Methods

The great thing about Spring-Data is the out-of-the-box availability of CRUD methods. All you have to do is create an interface that inherits from the JpaRespository. This is shown for the user entity in the following code snippet.

package at.clouddna.demo.repository;

import at.clouddna.demo.model.User;

public interface IUserRepository extends JpaRepository<User, Long> {

}
The repository can now be used directly in the controller by accessing it via autowiring. However, my company refrains from this. We always create an associated DTO (Data Transfer Object) for each entity class and additionally create a service class that is provided with the @Service Annotation, which encapsulates the use of the repository. The service class can be injected in the controller via the @Autowired Annotation.

Of course I will show you how it works.

ServiceBase Classs and Model Mapper

The mapping of the Entity class to the DTO and vice versa is not done manually but with ModelMapper. The modelmapper must be included in pom.xml as a dependency.

<dependency>
   <groupId>org.modelmapper</groupId>
   <artifactId>modelmapper</artifactId>
   <version>2.3.5</version>
</dependency>

ServiceBase.java

package at.clouddna.codegenerator.service.da;

import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

public abstract class ServiceBase {

    private ModelMapper modelMapper;

    public ServiceBase(){
        this.modelMapper = new ModelMapper();
        this.modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STANDARD);
    }

    public <D, T> D map(T entity, Class<D> outClass) {
        return modelMapper.map(entity, outClass);
    }

    public <D, T> List<D> mapAll(Collection<T> entityList, Class<D> outCLass) {
        return entityList.stream()
                .map(entity -> map(entity, outCLass))
                .collect(Collectors.toList());
    }

    protected void writeToFile(String fileName, String content) throws IOException {
        FileOutputStream outputStream = new FileOutputStream(fileName);
        byte[] strToBytes = content.getBytes();
        outputStream.write(strToBytes);
        outputStream.close();
    }

    protected void writeToFile(String fileName, byte[] content) throws IOException {
        FileOutputStream outputStream = new FileOutputStream(fileName);
        outputStream.write(content);
        outputStream.close();
    }
}

Service Class

The Service class inherits from the ServiceBase class and encapsulates access to the database. The following code snippet shows the UserService class. It is important that the class is provided with the annotation @Service. This allows it to be used in the controller via autowiring.

UserService.java

package at.clouddna.demo.service;

import at.clouddna.demo.dto.UserDto;
import at.clouddna.demo.model.User;
import at.clouddna.demo.respository.IUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

@Service
public class UserService extends ServiceBase {

    @Autowired
    private IUserRepository repository;

    public UserDto create(UserDto userDto) {
        return map(repository.save(map(userDto, User.class)), UserDto.class);
    }

    public UserDto update(UserDto userDto) {
        return map(repository.save(map(userDto, User.class)), UserDto.class);
    }

    public boolean delete(Long id) {
        repository.deleteById(id);
        return true;
    }

    public UserDto findById(Long id) {
        Optional<User> userOptional = repository.findById(id);
        if(!userOptional.isPresent()) {
            return null;
        }
        return map(userOptional.get(), UserDto.class);
    }

    public List<UserDto> findAll() {
        return mapAll(repository.findAll(), UserDto.class);
    }
}

RestController

Finally, I will show you how the previously developed service in the RestController can be fully used for all CRUD methods. You will be surprised how easy it is!

UserController.java

package at.clouddna.demo.controller;

import at.clouddna.demo.dto.UserDto;
import at.clouddna.demo.model.User;
import at.clouddna.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping
    public ResponseEntity getAll() {
        return ResponseEntity.ok(userService.findAll());
    }

    @GetMapping("/{id}")
    public ResponseEntity getById(@PathVariable("id") Long id) {
        return ResponseEntity.ok(userService.findById(id));
    }

    @PostMapping
    public ResponseEntity create(@RequestBody UserDto body) {
        return ResponseEntity.ok(userService.create(body));
    }

    @PutMapping("/{id}")
    public ResponseEntity update(@PathVariable("id") Long id,
                                 @RequestBody UserDto body) {
        body.setId(id);
        return ResponseEntity.ok(userService.update(body));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity delete(@PathVariable("id") Long id) {
        return ResponseEntity.ok(userService.delete(id));
    }
}

Conclusion

I hope that in this part I have made Java development in the SAP Cloud Platform appealing to you. As you have hopefully realized, this is not witchcraft and not space science. I would like to give you one more tip – already during the planning of the project, make sure that everything is clearly structured and that you define your own package for entity, DTO, repository, service and controller.