Java and JDBC for Oracle DBAs – Part 5: Spring-Boot, JdbcTemplate & DB migration (Using FlywayDB)

Today we’ll be looking at Spring-boot and a tools which developers use to migrate databases, FlywayDB. We’ll also look at how you can automate grants towards our two roles (which we created in part 3) whenever new tables are created.

Part 5: Spring-Boot, JdbcTemplate & DB migration (Using FlywayDB)

Spring-Boot: Where do we start?

Disclaimer

First of all, this is not a tutorial on Spring. If you are looking for such a tutorial, you can go to the Official Spring “Getting Started Guides“, or search for other tutorials on internet. This blog is a part of a blog series showing different ways Java Developers work towards an Oracle database. Today we are showing the use of Spring-boot and the JdbcTemplate.

Introduction to the Spring Framework

In a tutorial on tutorialpoint.com I found the following description of Spring:

Spring framework is an open source Java platform that provides comprehensive infrastructure support for developing robust Java applications very easily and very rapidly.

The most important feature, which maybe made Spring as popular as it has become is the Dependency Indjection (DI) and Inversion of Control (IOC). When DI or IOC is used properly, we can develop loosely coupled applications. When creating enterprise application classes should be as independent as possible of other Java classes. This increases the possibility to reuse classes. It also give the developer a better way to unit test classes independently. The DI feature (or sometime called wiring) helps in gluing these classes together and at the same time keeping them independent. Read more about DI and IOC in this tutorial.

One of the other key things about Spring is the use of annotations. This is a way of giving information about your Java classes to the Spring Framework. Spring will then use this information to integrate the different parts of your application. We’ll see the use of annotations when we start to build the todays application.

Another strength with the Spring Framework is the different modules which assist in developing different parts of an application, and the actual application requirement. The picture below (from tutorialpoint.com) shows the different Spring modules and layers:

Spring modules (picture from tutorialpoint.com)

Spring-boot

One of the issues with the Spring Framework has been the amount of configuration (in XML files) and work to glue applications together – the “XML hell”. This has lead to a new framework, on top of the Spring Framework, which is called Spring-boot. The Spring-boot framework defines different application starters. Dependent of the kind of application you are going to use, you choose one or more starter package. The starter package glue together a suitable pre-choosen frameworks (and versions) which you most probably will need for the kind of application you are developing.

For instance, if you are developing a web application or an application to expose restful services, the spring-boot-starter-web would be suitable. If you add this dependency to your maven pom.xml, the following dependencies would be linked to your project:

These (picture above) APIs include the most you would need to developing a web application, including  – for instance – frameworks for different Spring modules, JSON binding, validation (Hibernate validator), application server as Tomcat (Embedded Servlet Container) and logging (logback, slf4j). In addition the Spring-boot frameworks removes a lot of the need for heavy XML configuration.

The todays demo application: Spring-Jdbc

Some preparation using Spring Initializr

Today we’ll start a new application, and we’ll be using the Spring Initializr found at https://start.spring.io/. This web page you can use build a starting point for your application. Now we will use the Spring Initializr to … (Note! Click on the “Switch to the full version”).

Generate a “Maven” project with “Java” and Spring-Boot “2.1.3”:

Group: no.eritec.demo
ArtifactId: spring-jdbc
Name: spring-jdbc
Project: no.eritec.demo
Packaging: jar (default)
Java Version: 8 (default)

Your page should look something like this:

Then click “Generate Project”, and save the file to a know directory (for instance “Downloads”).

Open project in Eclipse

Unzip the file from the Spring Initializr in your “Workspace” directory, and open in Eclipse.

# unzip spring-jdbc.zip
# mv spring-jdbc /Users/lassejenssen/WS/.

Then go to Eclipse, and choose “File -> Open projects from File System …”:

Then open the pom.xml file, and add the Oracle driver dependency:

      <dependency>
        <groupId>com.oracle.jdbc</groupId>
        <artifactId>ojdbc8</artifactId>
        <version>12.2.0.1</version>
      </dependency>

Now we need to tell Spring the details about how to connect towards our database. Create a file name “application.properties” under the “src/main/resources” directory with the following content:

spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@//localhost:1521/orcl
spring.datasource.username=demo
spring.datasource.password=demo
spring.datasource.initialize=false
Database: Emp table

In the todays application we’ll be using the famous “Emp” table (from the good old “Scott/tiger” schema) with some adjustments.

The table is created by the following SQL:

create sequence emp_seq start with 1 increment by 1;

create table emp(
  empno    number(4,0) default emp_seq.nextval,
  ename    varchar2(10),
  job      varchar2(9),
  mgr      number(4,0),
  hiredate date,
  sal      number(10,2),
  comm     number(7,2),
  deptno   number(2,0),
  constraint pk_emp primary key (empno)
);

Do not run this against the database yet. We’ll use FlywayDB to do this in a moment.

Domain model: Employee.java

Now we are ready to create an entity class for the employee:

package no.eritec.demo.springjdbc.domain;

import java.sql.Date;

public class Employee {
	
	private int empno;
	private String ename;
	private String job;
	private int managerNo;
	private Date hiredate;
	private int salary;
	private int comm;
	private int deptno;
	
	public Employee() {
		super();
	}
	
	public Employee(
			int empno, 
			String ename,
			String job,
			int managerNo,
			int salary,
			int comm,
			int deptno) {
		this.empno = empno;
		this.ename = ename;
		this.job = job;
		this.managerNo = managerNo;
		this.salary = salary;
		this.comm = comm;
		this.deptno = deptno;
	}

	public Date getHiredate() {
		return hiredate;
	}

	public void setHiredate(Date hiredate) {
		this.hiredate = hiredate;
	}
	
	public int getEmpno() {
		return empno;
	}

	public void setEmpno(int empno) {
		this.empno = empno;
	}

	public String getEname() {
		return ename;
	}

	public void setEname(String ename) {
		this.ename = ename;
	}

	public String getJob() {
		return job;
	}

	public void setJob(String job) {
		this.job = job;
	}

	public int getManagerNo() {
		return managerNo;
	}

	public void setManagerNo(int managerNo) {
		this.managerNo = managerNo;
	}

	public int getSalary() {
		return salary;
	}

	public void setSalary(int salary) {
		this.salary = salary;
	}

	public int getComm() {
		return comm;
	}

	public void setComm(int comm) {
		this.comm = comm;
	}

	public int getDeptno() {
		return deptno;
	}

	public void setDeptno(int deptno) {
		this.deptno = deptno;
	}

}
Repository: EmployeeRepository.java

Then we’ll add a repository class, with methods to find all employees (findAll), to find an employee by id (findEmployeeById) and to create a new employee (create):

package no.eritec.demo.springjdbc.repository;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import no.eritec.demo.springjdbc.domain.Employee;

@Repository
public class EmployeeRepository {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @Transactional(readOnly=true)
    public List<Employee> findAll() {
        return jdbcTemplate.query("select * from emp", new EmployeeRowMapper());
    }
    
    @Transactional(readOnly=true)
    public Employee findEmployeeById(int empno) {
        return jdbcTemplate.queryForObject("select * from emp where empno=?",
                                        new Object[]{empno}, new EmployeeRowMapper());
    }
    
    public Employee create(final Employee emp) {
    	String id_column = "empno";
    	final String sql = 
        	"insert into emp(ename,job,mgr,hiredate,sal,comm,deptno) values(?,?,?,?,?,?,?)";
        KeyHolder holder = new GeneratedKeyHolder();
        jdbcTemplate.update(conn -> { 
        	PreparedStatement ps = conn.prepareStatement(sql, new String[]{id_column});
                    ps.setString(1, emp.getEname());
                    ps.setString(2, emp.getJob());
                    ps.setInt(3, emp.getManagerNo());
                    ps.setDate(4, (java.sql.Date) emp.getHiredate());
                    ps.setInt(5, emp.getSalary());
                    ps.setInt(6, emp.getComm());
                    ps.setInt(7, emp.getDeptno());
                    return ps; 
            }, holder
        );
        int newEmpno = holder.getKey().intValue();
        emp.setEmpno(newEmpno);
        return emp;
    }
}

class EmployeeRowMapper implements RowMapper<Employee>
{
    @Override
    public Employee mapRow(ResultSet rs, int rowNum) throws SQLException {
    	Employee e = new Employee();
        e.setEmpno(rs.getInt("empno"));
        e.setEname(rs.getString("ename"));
        e.setJob(rs.getString("job"));
        e.setHiredate(rs.getDate("hiredate"));
        e.setSalary(rs.getInt("sal"));
        e.setComm(rs.getInt("comm"));
        e.setDeptno(rs.getInt("deptno"));
               
        return e;
    }
}

JUnit test: SpringJdbcApplicationTests.java

At last we create a test class to test our code. Under the “src/test/java” you’ll find the “SpringJdbcApplicationTests” class. Add the following to the code:

package no.eritec.demo.springjdbc;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.sql.Date;
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.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import no.eritec.demo.springjdbc.domain.Employee;
import no.eritec.demo.springjdbc.repository.EmployeeRepository;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringJdbcApplicationTests {

   @Autowired
   private EmployeeRepository empRepo;
	
   @Test
   public void findAllUsers()  {
      List<Employee> employees = empRepo.findAll();
      assertNotNull(employees);
      assertTrue(!employees.isEmpty());
   }
    
   @Test
   public void findUserById()  {
      Employee emp = empRepo.findEmployeeById(1000);
      assertNotNull(emp);
   }
    
   @Test
   public void createEmployee() {       
    	Employee emp = new Employee();
    	emp.setEname("JOHNNY");
    	emp.setJob("Sales");
    	emp.setHiredate(new Date(new java.util.Date().getTime()));
    	emp.setManagerNo(1000);
    	emp.setSalary(5000);
    	Employee savedEmp = empRepo.create(emp);
    	Employee newEmp = empRepo.findEmployeeById(savedEmp.getEmpno());
        assertNotNull(newEmp);
        assertEquals(savedEmp.getEname(), newEmp.getEname());
        assertEquals(savedEmp.getSalary(), newEmp.getSalary());
   }

}

Right click inside the “SpringJdbcApplicationTests.java” file, and choose “Run As … -> JUnit Test”. You will now hopefully see the following output:

/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.5.RELEASE)

2018-10-06 23:59:56.871  INFO 57907 --- [           main] n.e.d.s.SpringJdbcApplicationTests       : Starting SpringJdbcApplicationTests on Lasses-MacBook-Pro-5.local with PID 57907 (started by lassejenssen in /Users/lassejenssen/WS/spring-jdbc)
2018-10-06 23:59:56.872  INFO 57907 --- [           main] n.e.d.s.SpringJdbcApplicationTests       : No active profile set, falling back to default profiles: default
2018-10-06 23:59:56.874  INFO 57907 --- [           main] o.s.w.c.s.GenericWebApplicationContext   : Refreshing org.springframework.web.context.support.GenericWebApplicationContext@6331250e: startup date [Sat Oct 06 23:59:56 CEST 2018]; root of context hierarchy
2018-10-06 23:59:57.157  WARN 57907 --- [           main] o.s.w.c.s.GenericWebApplicationContext   : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'employeeRepository': Unsatisfied dependency expressed through field 'jdbcTemplate'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flyway' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.flywaydb.core.Flyway]: Factory method 'flyway' threw exception; nested exception is java.lang.IllegalStateException: Cannot find migrations location in: [classpath:db/migration] (please add migrations or check your Flyway configuration)
2018-10-06 23:59:57.159  INFO 57907 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2018-10-06 23:59:57.161 ERROR 57907 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'employeeRepository': Unsatisfied dependency expressed through field 'jdbcTemplate'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flyway' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.flywaydb.core.Flyway]: Factory method 'flyway' threw exception; nested exception is java.lang.IllegalStateException: Cannot find migrations location in: [classpath:db/migration] (please add migrations or check your Flyway configuration)

The test fails with the following error:

Cannot find migrations location in: [classpath:db/migration]

Remember we told Spring-Boot we wanted to use “FlywayDB” (when we checked the flyway box when using Spring Initalizr)? Now Flyway expects to find the “db/migration” directories under the resources location. So let’s focus on the migration tool, FlywayDB, for a while.

Database Migration using FlywayDB

Developers use different tools to migrate changes in the data model into their database. In my last Java projects the developers have been using either FlywayDB or Liquibase, but there are other utilities on the market. In this blog post we’ll be using FlywayDB (also called Flyway), which is very simple and straight forward.

If you go the the Flyway download page, you’ll see that there is both an community edition, an pro edition and an enterprise edition. For instance, the community edition, which we will be using, does not have a UNDO migration feature. Further Flyway has both a command line utility, a maven (or gradle) plugin and a Java API. In this blog we’ll be using the maven plugin, and therefor you don’t need to download and install anything on your computer. Maven will do this for us. If you want to read more about how flyway works you can go to https://flywaydb.org/getstarted/how. In this blog post we will see how we can easily get flyway up and running in a maven project, and get a small feeling of what a migration utility does for the developers.

Note! This is not a tutorial in FlywayDB, but an example of how developers can use a tool like Flyway in their daily work to keep their database schemas up to date.

Ok. Let’s get going, and start to create our database changes. First we need to create the directories where Flyway expects to find the migration scripts:

# cd src/main/resources
# mkdir -p db/migration
# cd db/migration
# vi V1__init.sql

Copy the following to the V1__table_emp.sql script (note! important to use two underscores in the name):

create table emp(
  empno    number(4,0) GENERATED BY DEFAULT ON NULL AS IDENTITY START WITH 1000 INCREMENT BY 1,
  ename    varchar2(10),
  job      varchar2(9),
  mgr      number(4,0),
  hiredate date,
  sal      number(10,2),
  comm     number(7,2),
  deptno   number(2,0),
  constraint pk_emp primary key (empno)
);

insert into emp (ename, job, mgr, hiredate, sal, comm, deptno) values ('LARRY', 'CEO', null, sysdate-4000, 10000, null, 10);
insert into emp (ename, job, mgr, hiredate, sal, comm, deptno) values ('KING', 'Sales', 1000, sysdate-3000, 5000, null, 30);
commit;

grant select, insert, update, delete on emp to demo_rw;
grant select on emp to demo_ro;

Note! As an observant reader, you now might want to ask why I include the grants. Because in part 3 we created a DB_GRANT package to take care of these. The reason I do include these grants, is because I soon want to test and show you the flyway CLEAN command. This command will wipe away (drop) all objects belonging to the DEMODATA user, including the DB_GRANT package.

If we only were using one single Oracle user (to both store data and access data from the application) then Spring-boot would take care of Flyway for us. But because we do it the right way, and have both an data owner (DEMODATA) and application user (DEMO), we need a little more configuration.

Open the “application.properties” file, and add the following:

spring.flyway.user=demodata
spring.flyway.password=demo
spring.flyway.baselineOnMigrate=true
spring.flyway.table=SCHEMA_VERSION

Here we are telling Spring which user to use when migrating database with Flyway. By setting “baselineOnMigration=true” we are telling FLyway to go on with the migration if the schema in question is not empty. Flyway will then create a baseline for our migrations.

mvn flyway:clean

Sometimes it is very nice to be able to start all over from scratch. This is also true for our database schema. This way we can repeat the same test over and over again, expecting the same result every time. Flyway let us do this by using the “clean” command.

One way to do this is by using the maven command “mvn flyway:clean”. But then we need to tell Maven the same things which we told Spring (in the application.properties file).

Add the following plugin information to your build section in the pom.xml file (Note! I have included the whole build section below):

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
         <plugin>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-maven-plugin</artifactId>
            <configuration>
               <url>jdbc:oracle:thin:@localhost:151/orcl</url>
               <user>demodata</user>
               <password>demo</password>
               <locations>db/migration</locations>
               <table>SCHEMA_VERSION</table>
            </configuration>
            <dependencies>
               <dependency>
                  <groupId>com.oracle.jdbc</groupId>
                  <artifactId>ojdbc8</artifactId>
                  <version>12.2.0.1</version>
               </dependency>
            </dependencies>
         </plugin>
      </plugins>
   </build>

By the way, we do not really need to set the name for the flyway versioning table. But if we don’t flyway will make a table called “flyway_schema_version” – in lowercase! This does not work very well in Oracle. So I specify the table name in UPPERCASE, and also remove the “flyway_” prefix.

Ok. Now we are ready to do some cleaning. But before we go ahead … do you remember in the last blog (part 4), we created a PERSON table in our DEMODATA schema. What happens if we run …

# mvn flyway:clean
[INFO] ---------------------< no.eritec.demo:spring-jdbc >---------------------
[INFO] Building spring-jdbc 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- flyway-maven-plugin:5.0.7:clean (default-cli) @ spring-jdbc ---
[INFO] Flyway Community Edition 5.0.7 by Boxfuse
[INFO] Database: jdbc:oracle:thin:@localhost:1521/orcl (Oracle 12.2)
[INFO] Successfully cleaned schema "DEMODATA" (execution time 00:00.119s)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.362 s
[INFO] Finished at: 2018-10-07T11:04:46+02:00
[INFO] ------------------------------------------------------------------------

Let’s now go to the database and see what has happened to our DEMODATA schema:

$ sqlplus demodata/demo@orcl

SQL*Plus: Release 12.2.0.1.0 Production on Tue Jul 24 13:23:37 2018

Copyright (c) 1982, 2016, Oracle.  All rights reserved.

Last Successful login time: Tue Jul 24 2018 13:20:30 +00:00

Connected to:
Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production

SQL> select * from tab;

no rows selected

As you see – all the tables (including our PERSON table) were dropped! Clean is a great help in development and test, but is of course not to be ran against production. The project I have been working have only been using Flyway in test and development. Here it will effectively give you a fresh start, by wiping your configured schemas completely clean – including all objects. If you remember, when we created the DEMODATA schema in part 3, a DEMO_GRANT package was also created in the schema. This is now wiped away. But don’t worry. We’ll add this a little later. Let’s continue …

mvn flyway:baseline

Now we could also use maven to  initialize or set a baseline for Flyway. We could let Spring and flyway take care of this for us, but I just wanted to show you the Maven way of doing this.

To do this we run the “mvn flyway:baseline”. This will create the flyway versioning (or Schema History) table (which we choose to call “SCHEMA_VERSION”):

$ mvn flyway:baseline
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< no.eritec.demo:spring-jdbc >---------------------
[INFO] Building spring-jdbc 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- flyway-maven-plugin:5.0.7:baseline (default-cli) @ spring-jdbc ---
[INFO] Flyway Community Edition 5.0.7 by Boxfuse
[INFO] Database: jdbc:oracle:thin:@localhost:1521/orcl (Oracle 12.2)
[INFO] Creating Schema History table: "DEMODATA"."SCHEMA_VERSION"
[INFO] Successfully baselined schema with version: 1
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.810 s
[INFO] Finished at: 2018-10-07T11:17:56+02:00
[INFO] ------------------------------------------------------------------------

If we go to our database again we see maven and flyway have created the SCHEMA_VERSION table, and inserted one baseline row:

SQL> col tname for a40
SQL> set lines 200
SQL> select * from tab

TNAME					 TABTYPE  CLUSTERID
---------------------------------------- ------- ----------
SCHEMA_VERSION				 TABLE

SQL> select * from schema_version;

The baseline command we would use if we already had a data model which we wanted to keep, and build on top of.  This is not the case. We are starting from fresh, so just do a “mvn flyway:clean” again. This will remove the “SCHEMA_VERSION” table for now.

mvn flyway:migrate

Just as with the “baseline” command, you can also use Maven to migrate your database (by running “mvn flyway:migrate”). We’ll use Spring to do this instead, but we’ll also test the maven command in a second. Just keep on going.

Flyway and our DB_GRANT package

Remember the DEMO_GRANT.grantToRoles procedure we need to run to grant privileges on new objects to the DEMO_RO and DEMO_RW roles (see part 3). As we talked about earlier in this post, the flyway clean operation dropped this package and package body. We therefore now need to recreate it. It also would be very nice if this procedure could be triggered automatically every time we created a new object. So let’s also add a DDL trigger. Since both the package and the trigger could be defined as “CREATE OR REPLACE”, this is a repeatable migration. Therefore we name this file “R__add_pkg_demo_grant.sql”. These files (with prefix “R__” will be ran every time we do a migrate). Because of the dependency (the trigger is dependent of the package) we put both the package and the trigger in the same migration script.

Create the following file in the “src/main/resource/db/migration” directory:

CREATE OR REPLACE PACKAGE DEMO_GRANT AS 
    APP_NAME       CONSTANT  varchar2(4)    :='DEMO';
    SUPP_USR       CONSTANT  varchar2(8)    :='DEMOSUPP';
    DATA_USR       CONSTANT  varchar2(8)    :='DEMODATA';
    ROLE_NAME_RW   CONSTANT  varchar2(7)    :='DEMO_RW';
    ROLE_NAME_RO   CONSTANT  varchar2(7)    :='DEMO_RO';

    procedure grantToRoles;
END;
/

CREATE OR REPLACE PACKAGE BODY DEMO_GRANT AS 
    PROCEDURE log(txt_i IN varchar2) AS
    BEGIN
       dbms_output.put_line(txt_i);
    END;

    PROCEDURE grant_to_roles(obj_name_i IN varchar2, obj_type_i IN varchar2) AS 
       p_sql varchar2(200);
    BEGIN
       -- Grant to RW role
       p_sql := 'GRANT ' || case obj_type_i when 'TABLE'     then 'SELECT, INSERT, UPDATE, DELETE'
                                            when 'VIEW'      then 'SELECT'
                                            when 'SEQUENCE'  then 'SELECT'
                                                             else 'EXECUTE' end || 
                        ' ON ' || obj_name_i || ' TO ' || ROLE_NAME_RW; 
       begin
          execute immediate p_sql;
          log('Grant towards ' || obj_name_i || ' to ' || ROLE_NAME_RW || ' completed successfully.');
       exception when others then
          log('ERROR: Grant towards' || obj_name_i || ' to ' || ROLE_NAME_RW || ' failed: ' || SQLERRM);
       end;

       -- Grant to RO role if table or view
       if obj_type_i in ('TABLE','VIEW') then 
          p_sql := 'GRANT SELECT ON ' || obj_name_i || ' TO ' || ROLE_NAME_RO; 
          begin
             execute immediate p_sql;
             log('Grant towards ' || obj_name_i || ' to ' || ROLE_NAME_RO || ' completed successfully.');
          exception when others then
             log('ERROR: Grant towards' || obj_name_i || ' to ' || ROLE_NAME_RO || ' failed: ' || SQLERRM);
          end;
       end if;
    END;

    PROCEDURE grantToRoles is
    BEGIN 
       dbms_output.enable(1000000);
       FOR rec IN ( SELECT object_name,  object_type  FROM user_objects
                    WHERE object_type IN ('TABLE','PACKAGE','PROCEDURE','FUNCTION','SEQUENCE','VIEW','TYPE')
                      AND NOT (object_type like '%PACKAGE%' and object_name='DEMO_GRANT'))
       LOOP
          BEGIN
             grant_to_roles(rec.object_name, rec.object_type);
          EXCEPTION WHEN others THEN
             dbms_output.   put_line('Bad object_name='  || rec.object_name);
          END;
       END LOOP;
    END;
END;
/

CREATE OR REPLACE TRIGGER trg_after_create AFTER CREATE ON SCHEMA
DECLARE
  pragma autonomous_transaction;
  l_jobno PLS_INTEGER;
BEGIN
  dbms_job.submit( l_jobno, 'BEGIN demo_grant.granttoroles; END;',  sysdate + interval '10' second );
  commit;
END trg_after_create;
/
mvn flyway:info

Instead of applying these changes right away, let’s look at another flyway command – the “info” command:

$ mvn flyway:info
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< no.eritec.demo:spring-jdbc >---------------------
[INFO] Building spring-jdbc 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- flyway-maven-plugin:5.0.7:info (default-cli) @ spring-jdbc ---
[INFO] Flyway Community Edition 5.0.7 by Boxfuse
[INFO] Database: jdbc:oracle:thin:@localhost:1521/orcl (Oracle 12.2)
[INFO] Schema version: << Empty Schema >>
[INFO]
[INFO] +------------+---------+------------------+------+--------------+---------+
| Category   | Version | Description      | Type | Installed On | State   |
+------------+---------+------------------+------+--------------+---------+
| Versioned  | 1       | table emp        | SQL  |              | Pending |
| Repeatable |         | add pkg db grant | SQL  |              | Pending |
+------------+---------+------------------+------+--------------+---------+

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.604 s
[INFO] Finished at: 2018-10-07T11:42:05+02:00
[INFO] ------------------------------------------------------------------------

The info command compare the SCHEMA_VERSION table to the available scripts. Then we could easily know the state of our database before running migration. Above we see that all scripts are “Pending”, and not applied yet. So, let’s finally do a migrate:

$ mvn flyway:migrate
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< no.eritec.demo:spring-jdbc >---------------------
[INFO] Building spring-jdbc 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- flyway-maven-plugin:5.0.7:migrate (default-cli) @ spring-jdbc ---
[INFO] Flyway Community Edition 5.0.7 by Boxfuse
[INFO] Database: jdbc:oracle:thin:@localhost:1521/orcl (Oracle 12.2)
[INFO] Successfully validated 2 migrations (execution time 00:00.013s)
[INFO] Creating Schema History table: "DEMODATA"."SCHEMA_VERSION"
[INFO] Current version of schema "DEMODATA": << Empty Schema >>
[INFO] Migrating schema "DEMODATA" to version 1 - init
[INFO] Migrating schema "DEMODATA" with repeatable migration add pkg db grant
[INFO] Successfully applied 2 migrations to schema "DEMODATA" (execution time 00:00.245s)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.174 s
[INFO] Finished at: 2018-10-07T11:43:42+02:00
[INFO] ------------------------------------------------------------------------

The migrate was successful. Let’s look at the INFO again (only copied the schema table):

+------------+---------+------------------+------+---------------------+---------+
| Category   | Version | Description      | Type | Installed On        | State   |
+------------+---------+------------------+------+---------------------+---------+
| Versioned  | 1       | table emp        | SQL  | 2018-10-07 11:43:42 | Success |
| Repeatable |         | add pkg db grant | SQL  | 2018-10-07 11:43:42 | Success |
+------------+---------+------------------+------+---------------------+---------+

Now we see that both scripts are ran “Success”-fully. It seems like everything went ok, but you can not really trust this output. This only tell you that the scripts were ran ok against the database. So to be 100% sure that the package, package body and trigger is compiled successfully in the database, you need to login to the database and check for errors.

Test of trigger

Note! The DEMODATA need EXECUTE privilege on DBMS_JOB package. If not allready granted this, run the following as SYSDBA: “grant EXECUTE on sys.DBMS_JOB to DEMODATA”. If your trigger compiled ok in the database, the DEMODATA user already have this privilege.

As we see above, the repeatable migration was run. If we now create another table in the DEMODATA schema, the application schema, DEMO, should get access to it in about 10 seconds. Let’s just try to create a dummy table manually through SqlPlus:

$ sqlplus demodata/demo@orcl

SQL*Plus: Release 12.2.0.1.0 Production on Wed Jul 25 13:14:57 2018

Copyright (c) 1982, 2016, Oracle.  All rights reserved.

Last Successful login time: Wed Jul 25 2018 13:12:00 +00:00

Connected to:
Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production

SQL> create table dummy(id number, some_text varchar2(10));

Table created.

SQL> insert into dummy values (1,'Some');

1 row created.

SQL> commit;

Commit complete.

SQL> conn demo/demo@orcl
Connected.
SQL> select * from dummy;

	ID SOME_TEXT
---------- ----------
	 1 Some

By the time we login as the demo user, the necessary grants towards the new table is given, and we can select towards the new DUMMY table. Ok – Just drop the DUMMY table again:

SQL> conn demodata/demo@orcl
Connected.

SQL> drop table dummy purge;

Table dropped.

There are two flyway commands which we haven’t talked about yet or shown. This is the “validate” and the “repair” functions.

mvn flyway:validate

The validate function verify if your database (schema) is in sync with your local script repository.

mvn flyway:repair

The repair function you can use if one of your migration failed. If you have rolled back (or cleaned up) after the failed attempt, and you want to move the failed migration from your SCHEMA_VERSION table, you can run a “mvn flyway:repair”.

Back to our Spring-Jdbc application

So … finally let’s get back to our Spring-Jdbc application. Now we actually should be ready to run our test again. But before you do, you should run the “mvn flyway:clean”.

Then (in Eclipse) right click inside your “SpringJdbcApplicationTests.java” class, and choose “Run as … -> JUnit Test”. (Note! There are many other ways you could trigger this. For instance – on Mac you have a shortcut SHIFT-CMD-F11. Or you could also click on the green “Play” button in the menu bar. Use the main menu “Run -> Run as ..:” etc.)

In the “Console” window you should now see the test start to run. When it completes go to the “JUnit” tab, and hopefully you’ll see all your test succeeded:

Configuring Flyway with Java Config class

The next thing I want to do, is to make Flyway automatically clean before running migrate (which is triggered when I run my test class). Now I can NOT use any configuration in the Maven file (because I’m only using maven through the “spring-boot-maven-plugin”), and there is no defined parameter to set in the “application.properties” file to do this. My only option now is to use JavaConfig, which is exactly what it sounds like: Configuring by using Java code.

To do this I introduce a new class “FlywayConfig”, which we mark as a Spring configuration class by using the @Configuration annotation. We also use the @PropertySource annotation to point to the “application.properties” file. Now we can introduce a new property “flyway-clean-on-migrate” in the “application.properties” file (which will be read by the code). The parameter is going to be used to turn the clean on or off:

flyway-clean-on-migrate=true

And finally we can write our new “FlywayConfig” class:

package no.eritec.demo.springjdbc.config;

import org.flywaydb.core.Flyway;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

@Configuration
@PropertySource(value = { "classpath:application.properties" })
public class FlywayConfig {
	
   @Autowired
   private Environment env;
	
   static Logger logger = LoggerFactory.getLogger(FlywayConfig.class);
 
   boolean clean = true;
    
   @Bean
   public FlywayMigrationStrategy cleanMigrateStrategy() {

      FlywayMigrationStrategy strategy = new FlywayMigrationStrategy() {

            @Override
            public void migrate(Flyway flyway) {
        	
               if (Boolean.parseBoolean(env.getProperty("flyway-clean-on-migrate","true"))) {
                  logger.info("Cleaning DB with Flyway");
                  flyway.clean();
               } else {
                  logger.info("Skipping cleaning (FlywayDB)");
               }

               flyway.migrate();
            }
         };

      return strategy;
   }

   public boolean isClean() {
      return clean;
   }

   public void setClean(boolean clean) {
      this.clean = clean;
   }

}

If we now create a new DUMMY table in the DEMODATA schema and run the test again, we’ll see that the DUMMY class has been dropped when the test finishes. This is because Spring now have made Flyway run clean before doing a migration.

You will find all code generated today in my GitHub repository: https://github.com/lasjen/JavaForOracleDBAs

That was all for today 🙂

Post a Comment

Your email is never published nor shared. Required fields are marked *