Showing posts with label PostgreSQL. Show all posts
Showing posts with label PostgreSQL. Show all posts

Saturday, March 16, 2024

Spring Data JPA - Join Fetch, Entity Graphs - to fetch nested levels of OneToMany related data, and JPQL . . .

One-to-many relationship in Databases is quite common. It is also quite cumbersome in terms of how many aspects that need to be considered for getting it correctly implemented. Just to list few aspects - related JPA annotations, relationship keys specified in annotations, fetch modes, fetch types, joins, query types, performance, N+1 data problem, cartesian product, DISTINCT to eliminate duplicates, indexes etc. With Spring Data, JPA and Hibernate as the default implementation provider there are few JPA annotations, Hibernate specific annotations, JPQL queries, Java collection types, all these will get added to the mix.

Environment: Java 21, Spring Boot 3.2.3, PostgreSQL 15.3, maven 3.9.6 on macOS Catalina 10.15.7

If you have JPA entities related through OneToMany relationships two multiple levels, then some special care is required to fetch data in a performant manner by avoiding the classic N+1 query issue, or even multiple queries. Each query is a roundtrip to Database and it adds up its own baggage.

Let's take a simple example of Country has many States, State has many Cities. We want to represent this relationship in JPA Entities and query using Spring Data JPA Repositories.

The entities with relationships look something like:

public abstract class BaseEntity implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false, updatable = false) @ToString.Exclude protected Long id; /** For optimistic locking */ @Version protected Long version; @CreationTimestamp @Column(nullable = false, updatable = false, columnDefinition = "TIMESTAMP WITH TIME ZONE") protected OffsetDateTime dateCreated = OffsetDateTime.now(); @UpdateTimestamp @Column(nullable = false, columnDefinition = "TIMESTAMP WITH TIME ZONE") protected OffsetDateTime lastUpdated = OffsetDateTime.now(); } @Entity public class Country extends BaseEntity { private String name; @OneToMany(mappedBy = "country", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default @ToString.Exclude private Set<State> states = new LinkedHashSet<>(); } @Entity public class State extends BaseEntity { private String name; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "country_id") @ToString.Exclude private Country country; // owning side of the relationship @OneToMany(mappedBy = "state", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default @ToString.Exclude private Set<City> cities = new LinkedHashSet<>(); } @Entity public class City extends BaseEntity { private String name; @ManyToOne @JoinColumn(name = "state_id") @ToString.Exclude private State state; }

And a repository interface like:
@Repository public interface CountryRepository extends JpaRepository<Country, Long> { Optional<State> findByName(String name); }

One way to fetch all related data in a single query is by writing JPQL query with JOIN FETCH. This involves making sure to use all @OneToMany annotated properties to use Set and not List, and not using FetchType.EAGER and FetchMode.JOIN., and by writing a JPQL query with @Query annotation as shown below. Make a note of both DISTINCT and JOIN FETCH. This will result into one query which fetches for a Country all States, for each State all Cities data. If it is huge set of records, your best bet is to use @EntityGraph recommended. Lets say that our data is not huge and we want to use JPQL. In this case, the repository method annotatted with JPQL Query looks like:

@Repository public interface CountryRepository extends JpaRepository { @Query(""" SELECT DISTINCT country from Country country JOIN FETCH country.states state JOIN FETCH state.cities city WHERE country.name = :name """) Optional<Country> findByName(String name); }

JPQL

Java Persistencw Query Language (JPQL) is portable query language to query persistent entities irrespective of the mechanism used to store those entities. Typically in a Java application the entities are Java classes. Similar to SQL, it provides select, update, delete statements, join operations, aggregations, subqueries etc. Hibernate supports both JPQL and HQL.

Spring Data JPA offers different ways to define query methods in Repository interface like: 1) Derived queries for which the query is derived/generated from the name of the method by following conventions 2) Declared Queries by annotating query method with @Query annotation 3) Named Queries etc.

JPQL - Fetch Entities
In the above JPQL declared query in CountryRepository using @Query annotation, a typical JPQL to query entity object (Country) is shown. Typically, entity object is mapped to Database table and entity query results into SQL query generated by the underlying JPA implementation like Hibernate. The query result is Database records fetched from table(s) and the raw data is transformed into Entity objects.

JPQL - Fetch Custom objects
JPQL also supports custom objects through JPQL Constructor expressions. A constructor expression can be specified in JPQL to return a custom object instead of Entity object. Below is an example a code snippet in which a light-weight custom Java record is returned instead of Entity object.

@Repository public interface StateRepository extends JpaRepository<State, Long> { /** * JPQL - Query to fetch specific fields of Entity and return a non-entity custom objects * * @param population the population * @return list of light-weight StatePopulation objects */ @Query(""" SELECT new com.giri.countrystatecity.domain.StatePopulation(state.name, state.population) from State state WHERE state.population > :population """) List<StatePopulation> findAllStatesByPopulationGreaterThan(Long population); }
where StatePopulation is simply a record like: public record StatePopulation(String name, Long population) { }

JPQL - Fetch Raw specified column data 
Similar to the custom object, raw column data can also be fetched and then required object csn be constructed from the returned data. Following is code snippet for fetching raw data and constructing a data record object.

/** * JPQL - Query to fetch specific fields of Entity and return Raw data * * @param population the population * @return list of light-weight StatePopulation objects */ @Query(""" SELECT state.name, state.population from State state WHERE state.population > :population """) List<List<Object>> findAllStatesByPopulationGreaterThanJpqlRaw(Long population);
where raw data fetched can be converted into required objects in a service method like shown below:
public List<StatePopulation> getAllByPopulationGreaterThanJpqlRaw(Long population) { List<List<Object>> states = stateRepository.findAllStatesByPopulationGreaterThanJpqlRaw(population); return states.stream() .map(record -> new StatePopulation((String) record.get(0), (Long)record.get(1))) .toList(); }

Gotcha

  • If you use List instead of Set, you might bump into infamous HIBERNATE concept called bag and an exception like - MultipleBagFetchException: cannot simultaneously fetch multiple bags which will force you to read a whole lot of text in finding the information you need, digging through googled links and StackOverflow without much luck, and eventually breaking your head ;)
  • There are also other ways to tackle this N+1 query problem. Writing native query in the @Query annotation is another way. I wouldn't go that route as I don't want to get sucked by databases. I am sure if you take that route, you will have someone around you ready to argue in favor of Sub Selects, Stored Procedures etc. ;). My personal preference is to stay away from diving deep into database,  avoid abyss ;)

Sample Spring Boot Application - Country-State-City

Here is the link to a sample Spring Boot 3.2.3 GraphQL application which has both @Query JPQL way and @EntityGraph way of getting the single generated query that is performant in fetching all related data in one roundtrip.

References

Sunday, March 03, 2024

Enums - all the way to persistence (revisited and revised for today's tech stack) . . .

About two years ago I blogged on this combination: Enums - all the way to persistence. Technology is moving at faster pace than ever before. Java's release cadence is moving at rapid 6 months cycle, every March and September. Spring boot catches Java and other technologies and moves along at the same pace as Java, every 6 months in May and November. Of course, the PostgreSQL database, Hibernate and even Maven build system keep moving as well, at their own pace.

The challenge for Java developer is to keep up with all the moving technologies. As software development requires talented developers with years of experience and knowledge, debates go on Artificial Intelligence (AI). Some who have moved away from coding strongly feel that the generative AI which currently is capable of generating even code will replace software developers. I don't believe in that, at least at this time. The add-on 'at least at this time' is only a cautious extension to that non-generative human statement). I have tried CodeGPT lately at work, a couple of times when I was stuck with things not working together as described in documentations and blog posts, asking it's generative trained intelligence to see if it would be able to be my copilot in those development situations. It couldn't really stand up the hype in anyway, and I had to go and figure out myself all those situations.

Enums persistence - is one such problem again I did hit roadblocks lately after two years. The only change is newer versions of all of these technologies. It required additional exploration of few things before arriving at a solution that worked eventually.

Environment: Java 21, Spring Boot 3.2.3, PostgreSQL 15.3, maven 3.9.6 on macOS Catalina 10.15.7

Spring boot 3.2.3 data jpa brings in Hibernate 6.4.4 dependency.

The same persistent model described in my earlier blog post: Enums - all the way to persistence would need the following changes for enums to work.

The DDL script requires an extra PostgreSQL casting as shown below for enums:

-- create enum type genders CREATE TYPE genders AS ENUM( 'MALE', 'FEMALE' ); CREATE CAST (varchar AS genders) WITH INOUT AS IMPLICIT;

In maven pom.xml, the spring boot version is 3.2.3 and the hibernate-types-55 dependency is not needed.

Changes to domain object Person.java are shown below (the @TypeDef annotation is not required):

... import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; ... @NotNull @Enumerated(EnumType.STRING) @JdbcTypeCode(SqlTypes.NAMED_ENUM) Gender gender; ... }

Changes look simple after figuring out and making things to work, but finding things that work required a bit of exploration ;)

References

Monday, January 15, 2024

Spring Boot - Docker Compose - Run init script . . .

Spring Boot 3.1 enhanced docker-compose support, made it lot simpler and better suited for local development. With that we don't need to worry about installing services like database locally and managing them manually, letting Docker do that for us and Spring Boot do the rest of starting and stopping docker container.
 
This post is about details explored on - how to run additional init db script with PostgreSQL service defined in Docker compose file in a Spring Boot application.

Environment: Java 21, Spring Boot 3.2.1, PostgreSQL 16, maven 3.9.6 on macOS Catalina 10.15.7

The Scenario

My Spring Boot 3.2.x application uses PostgreSQL database, a specific version of it. By leveraging Spring Boot support for docker-compose in development, I would like to have a new schema and user  created, granting the user required privileges on the schema.

A typical PostgreSQL Service configuration in docker compose file looks like:
docker-compose.yml
version: '3' services: PostgreSQL16: image: 'postgres:16.1' ports: - '54321:5432' environment: - 'POSTGRES_DB=my_app' - 'POSTGRES_USER=postgres' - 'POSTGRES_PASSWORD=s3cr3t'

In the above docker compose configuration, we have specified database name, user, and password through environment variables in the container, and mapped host port (local port) to container port (default postgres port). With this when the docker-compose command: docker-compose up is run to create and start the container, the my_app database gets created, and PostgreSQL will be up and running in the container. The postgres user created is the superuser with access and ownership to all database objects including the public schema. When the docker container is created for PostgreSQL16 database service, the value of POSTGRES_USER environment variable is used to create the superuser, and public is the default schema created.

PostgreSQL 15 changes to public schema

From version 15 onwards, privileges on public schema are restricted and the schema is accessible to superuser only. So, it is good to create an application specific schema, and application specific database user with all needed privileges granted on the application schema. This requires a way to run a one-time initial databases script for creating application schema and user. The following shell script is an example to do so:
init-database.sh
#!/bin/sh set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL /* Create schema, user and grant permissions */ CREATE SCHEMA my_app_schema; CREATE USER my_app_user_local WITH PASSWORD 'password'; GRANT ALL PRIVILEGES ON SCHEMA my_app_schema TO my_app_user_local; EOSQL

In order to run the above db init script when the container is created, reference the shell script file under volumes: to attach the init file directory (./) to the container directory (/docker-entrypoint-initdb.d/) as shown below:
docker-compose.yml
version: '3' services: PostgreSQL16: image: 'postgres:16.1' ports: - '54321:5432' environment: - 'POSTGRES_DB=my_app' - 'POSTGRES_USER=postgres' - 'POSTGRES_PASSWORD=s3cr3t' volumes: -  ./init-database.sh:/docker-entrypoint-initdb.d/init-database.sh
 
With this, when the docker container is created for PostgreSQL service, the init db script gets executed which results with new schema my_app_schema and user my_app_user_local. with privileges granted.

Gotchas

Auto configured datasource properties
When the application is run, the PostgreSQL container is created and run by Spring Boot. It also auto configures dataSource bean with properties: url, username, and password taking them from docker compose file. The user is superuser created from the POSTGRES_USER container environment variable. If the application has any initialization database scripts within the application under main/resources dir like schema.sql for initial schema or even flyway scripts in flyway enabled application under main/resources/db.migration dir, all the database tables and other objects created are owned by the superuser as the datasource uses superuser for connecting to the database.

If you want the data objects like tables, indices created in the application schema instead of public, you may need to specify it as the prefix in your schema.sql or for an app with flyway support, add the property spring.flyway.schemas appropriately, e.g in application.yml.

TIPS

1. With docker-compose managing the database service, if you need to use psql, the terminal based frontend to PostgreSQL to connect to db and run commands, invoke psql like:

$ # list docker containers running $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7caf031c31a4 postgres:16.0 "docker-entrypoint.s…" 56 minutes ago Up 56 minutes 0.0.0.0:5222->5432/tcp docker-PostgreSQL16-1 45fa1a477ac3 postgres:15.3 "docker-entrypoint.s…" 4 days ago Up 3 days 0.0.0.0:54321->5432/tcp docker-compose-postgres15-1 $ # run psql command to coonect to PostgreSQL16 db and list users $ docker exec -it docker-PostgreSQL16-1 psql -U postgres psql (16.0 (Debian 16.0-1.pgdg120+1)) Type "help" for help. postgres=# postgres=# \? General \bind [PARAM]... set query parameters \copyright show PostgreSQL usage and distribution terms \crosstabview [COLUMNS] execute query and display result in crosstab \errverbose show most recent error message at maximum verbosity \g [(OPTIONS)] [FILE] execute query (and send result to file or |pipe); \g with no arguments is equivalent to a semicolon \gdesc describe result of query, without executing it \gexec execute query, then execute each value in its result \gset [PREFIX] execute query and store result in psql variables \gx [(OPTIONS)] [FILE] as \g, but forces expanded output mode \q quit psql --More-- postgres=# \dn List of schemas Name | Owner --------+------------------- public | pg_database_owner (1 row) postgres=# \du List of roles Role name | Attributes -------------------+------------------------------------------------------------ my_app_user_local | postgres | Superuser, Create role, Create DB, Replication, Bypass RLS postgres=# SELECT version(); version --------------------------------------------------------------------------------------------------------------------- PostgreSQL 16.0 (Debian 16.0-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit (1 row) postgres=# \conninfo You are connected to database "postgres" as user "postgres" via socket in "/var/run/postgresql" at port "5432". postgres=# select current_date; current_date -------------- 2024-01-15 (1 row) postgres=# SHOW search_path; search_path ----------------- "$user", public (1 row) postgres=# \l List of databases Name | Owner | Encoding | Locale Provider | Collate | Ctype | ICU Locale | ICU Rules | Access privileges --------------+----------+----------+-----------------+------------+------------+------------+-----------+----------------------- boot-graalvm | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | postgres | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | template0 | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/postgres + | | | | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/postgres + | | | | | | | | postgres=CTc/postgres (4 rows) postgres=# \c boot-graalvm You are now connected to database "boot-graalvm" as user "postgres". boot-graalvm=# \dt List of relations Schema | Name | Type | Owner --------+-----------------------+-------+---------- public | account_holder | table | postgres public | accounts | table | postgres public | addresses | table | postgres public | flyway_schema_history | table | postgres (4 rows) boot-graalvm=# boot-graalvm=# \c postgres You are now connected to database "postgres" as user "postgres". postgres=# postgres=# \q

References

Monday, January 08, 2024

Spring Boot - Check database connectivity after the application starts up . . .

Database Integration is much simpler with Spring Boot's non-invasive Auto Configuration feature. A typical Spring Boot application is configured to run in multiple environments, a.k.a profiles. However, there are multiple options available when it comes to configuring Database, like Docker Compose, Testcontainers, explicit DataSource profile based properties/yaml, externalized DataSource properties through Vault etc. In any case, it is good to have a database connection check in place to make sure that the database connection looks good once the application boots up and starts to run.

Environment: Java 21, Spring Boot 3.2.1, PostgreSQL 16, maven 3.9.6 on macOS Catalina 10.15.7

The Scenario

The Database is PostgreSQL and we want to run a simple query to make sure that the database connection looks good once the application starts up.

One way to achieve this

One way to achieve this is to execute a simple query after the application starts up. Spring Boot's CommandLineRunner or ApplicationRunner can be leveraged to do this. This is a good place to run specific code after the application has started.

Here is a code snippet for this:
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.jdbc.core.simple.JdbcClient; @SpringBootApplication @Slf4j public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } @Autowired(required = false) JdbcClient jdbcClient; @Bean public CommandLineRunner commandLineRunner() { return args -> { if (jdbcClient != null) { log.info("Database check: {}", jdbcClient.sql("SELECT version()").query(String.class).single()); } }; } }

The above highlighted is the code snippet that gets executed after the application gets started. It just logs the executed query result, nothing but the database version. 

An integration test case can also be put in place as shown below, which makes sure that the database connection and version look good. This kind of testcase is good to have to make sure that the code is tested against the same db version as the production.

import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.test.context.ActiveProfiles; import static org.assertj.core.api.Assertions.*; /** * An integration test to check Database connectivity. */ @ActiveProfiles("test") // We don't want the H2 in-memory database. // We will provide a custom 'test container' as DataSource, so don't replace it. @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @DataJpaTest @Import(TestContainersConfiguration.class) public class DatabaseCheckIT { @Autowired JdbcClient jdbcClient; @Test void database_connection_works_and_version_looks_good() { assertThat(jdbcClient.sql("SELECT version()").query(String.class).single()) .contains("16.0"); } }

The above test case uses Testcontainers and a test configuration as shown below for unit/integration tests:

import lombok.extern.slf4j.Slf4j; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.annotation.Bean; import org.testcontainers.containers.PostgreSQLContainer; /** * Test Configuration for testcontainers. */ @TestConfiguration(proxyBeanMethods = false) @Slf4j public class TestContainersConfiguration { private static final String POSTGRES_IMAGE_TAG = "postgres:16.0"; @Bean @ServiceConnection PostgreSQLContainer postgreSQLContainer() { return new PostgreSQLContainer<>(POSTGRES_IMAGE_TAG) .withDatabaseName("my-application") .withUsername("my-application") .withPassword("s3cr3t") .withReuse(true); } }

Gotcha

Note that in the main application class, for JdbcClient @Autowired annotation, the optional property required is explicitly set to false.  The reason for this is if there are any integration test cases to test specific layers (test slices like @GraphQlTest) that do not auto configure datasource, when the application class is run as part of starting Spring Boot run, the testcase runs into exception as JdbcClient bean is not available for auto wiring. So, in those cases jdbcClient property would be null. So, a non null check is required to safely run the SQL statement.

💡 TIPS

The CommandLineRunner bean in the application class can conditionally be defined on some DataSource related bean/class by annotation it with @ConditionalOnBean or @ConditionalOnClass. I couldn't find a way to get it conditionally defined and be working for all scenarios.

Resources


Thursday, September 22, 2022

Enums - all the way to persistence . . .

In any application there would be a need for pre-defined ordered set of constants. Enum is a data type good for such cases. Java has enum since 1.5. Databases do support enum type and PostgreSQL has a special support for this since release 8.3. JPA and Spring Data is a good match to use in modern Java applications for persistence, especially in Spring Boot applications. 

Environment: Java 17, Spring Boot 2.6,7 on macOS Catalina 10.15.7

Example Scenario - A persistable entity object in a Spring Boot micro-service application with JPA and PostgreSQL DB.

DDL Script
-- create enum type genders CREATE TYPE genders AS ENUM( 'MALE', 'FEMALE' ); -- create people table CREATE TABLE people( id VARCHAR(36) PRIMARY KEY, first_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL, gender genders NOT NULL ); -- Unique Constraints ALTER TABLE people ADD CONSTRAINT people_fname_lname_uk UNIQUE (first_name, last_name);

Maven dependencies: pom.xml
... <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.vladmihalcea</groupId> <artifactId>hibernate-types-55</artifactId> <version>2.16.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> <scope>provided</scope> </dependency> ... </dependencies> ...

Enum: Gender.java
import lombok.AllArgsConstructor; @AllArgsConstructor public enum Gender { MALE("Male"), FEMALE("Female"); String genderName; }

Domain Object: Person.java
import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import javax.validation.constraints.NotNull; import java.util.UUID; @Entity @Table( name = "people", uniqueConstraints = { @UniqueConstraint( columnNames = {"firstName", "lastName"}, name = "people_fname_lname_uk" ) } ) @TypeDef( name = "pgsql_enum", typeClass = PostgreSQLEnumType.class ) @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Person { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(length = 36, nullable = false, updatable = false) @Type(type="org.hibernate.type.UUIDCharType") private UUID id; @NotNull String firstName; @NotNull String lastName; @NotNull @Enumerated(EnumType.STRING) @Type(type = "pgsql_enum") Gender gender; }

JPA Repository: PersonRepository.java
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import java.util.UUID; public interface PersonRepository extends JpaRepository<Person, UUID> { Person findByFirstNameAndLastNameAndGender(String firstName, String lastName, Gender gender); @Query(value = "SELECT * FROM people WHERE first_name = :firstName AND last_name = :lastName AND gender = CAST(:#{#gender.name()} as genders)", nativeQuery = true) Person findByFirstNameAndLastNameAndGenderNQ(String firstName, String lastName, Gender gender); }

JPA Repository Test: PersonRepositoryIT.java
import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @ActiveProfiles("test") @RunWith(SpringRunner.class) @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public class PersonRepositoryIT { @Autowired TestEntityManager testEntityManager; @Autowired PersonRepository personRepository; @Test public void findBy_returns_expected() { // given: data in DB List.of( Person.builder().firstName("Giri").lastName("Potte").gender(Gender.MALE), Person.builder().firstName("Boo").lastName("Potte").gender(Gender.MALE) ).forEach(testEntityManager::persist); testEntityManager.flush(); // expect: assertThat( personRepository.findByFirstNameAndLastNameAndGender("Giri", "Potte", Gender.MALE) ).isNotNull() .extracting("firstName", "lastName", "gender") .isEqualTo(List.of("Giri", "Potte", Gender.MALE)); // expect: assertThat( personRepository.findByFirstNameAndLastNameAndGenderNQ("Boo", "Potte", Gender.MALE) ).isNotNull() .extracting("firstName", "lastName", "gender") .isEqualTo(List.of("Boo", "Potte", Gender.MALE)); } }

TIP

From Database side, sometimes, it's handy to have some SQLs dealing with enum type to create, drop, list values, add a new value, modify existing value etc.

I was just fiddling with PostgreSQL enum type and the following is the link:

References

Thursday, May 13, 2021

PostgreSQL - JSON - Gotchas . . .

These days, JSON is widely used for data-interchange mainly because it's light-weight, simple and readable. Many programming languages have a very good support for JSON, either a built-in support or through libraries.

Databases also added support for JSON in recent years. PostgreSQL is no exception in this space. However, each Database adds its own set of JSON functions and specific syntax for working with JSON data stored in its database. There is no standard and one has to become familiar with the database specific syntax and function to save and retrieve JSON data in and out of database. This post mainly focuses on gotchas dealing with JSON in PostgreSQL Database.

PostgreSQL database Version:12.x

Gotcha-1: Updating JSON data

PostgreSQL documentation lists all JSON functions. However, updating JSON data has a Gotcha.  It only works if the JSON column has JSON data. If the JSON column is NULL, then the function jsonb_set() doesn't work. It's bit frustrating as it doesn't fail with an error either. It misleads by returning the number of records updated.

Click on the DEMO to check it out.

Given below is the SQL from the above DEMO to try out:
-- check version SELECT version(); -- create test table CREATE TABLE test_json ( id varchar(36) NOT NULL, profile json NULL, -- age: generated column, value derived from profile json age smallint GENERATED ALWAYS AS ((profile ->> 'age')::smallint) STORED, CONSTRAINT pk_giri_test PRIMARY KEY (id) ); SELECT * FROM test_json; -- insert a record INSERT INTO test_json (id, profile) VALUES('2eab2d99-167d-41b7-8227-d89ae45d3801', '{"fname":"Giri", "lname":"Pottepalem", "age":30}'); -- check data SELECT * FROM test_json; -- insert another record with no profile INSERT INTO test_json (id) VALUES('2eab2d99-167d-41b7-8227-d89ae45d3802'); -- check data SELECT * FROM test_json; -- try to update profile using json_set UPDATE test_json SET profile = jsonb_set(profile::jsonb, '{fname}', '"boo"'::jsonb) WHERE id = '2eab2d99-167d-41b7-8227-d89ae45d3802'; -- check data, notice that previous update did not work though it returned 1 rows affected SELECT * FROM test_json; -- lets update using regular UPDATE test_json SET profile = '{"fname":"boo", "lname":"pottepalem"}'::jsonb WHERE id = '2eab2d99-167d-41b7-8227-d89ae45d3802'; -- check data, notice that previous update actually updated profile SELECT * FROM test_json; -- update one property using jsonb_set UPDATE test_json SET profile = jsonb_set(profile::jsonb, '{age}', '15'::jsonb) WHERE id = '2eab2d99-167d-41b7-8227-d89ae45d3802'; -- check data, notice that jsonb_set now updated profile SELECT * FROM test_json; -- update multiple properties using jsonb_set UPDATE test_json SET profile = jsonb_set( jsonb_set( jsonb_set( profile::jsonb, '{age}', '18'::jsonb )::jsonb, '{fname}', '"Bhuvan"'::jsonb )::jsonb, '{lname}', '"Pottepalem"'::jsonb ) WHERE id = '2eab2d99-167d-41b7-8227-d89ae45d3802'; -- check data, notice that jsonb_set now updated profile with multiple properties SELECT * FROM test_json; -- update json by concatening with ||, properties that exists get updated, properties that don't exist get added UPDATE test_json SET profile = profile::jsonb || '{"fname" : "boo", "lname" : "potte", "address" : "dummy address"}' WHERE id = '2eab2d99-167d-41b7-8227-d89ae45d3802'; -- check data, notice that existing properties got updated and new properties got added SELECT * FROM test_json; -- remove address property from json UPDATE test_json SET profile = profile::jsonb - 'address' WHERE id = '2eab2d99-167d-41b7-8227-d89ae45d3802'; -- check data, notice that address property is removed from the json SELECT * FROM test_json; -- update json by concatening with ||, properties that exists get updated, properties that do not exist get added, integer age UPDATE test_json SET profile = profile::jsonb || '{"fname" : "Bhuvan"}' || '{"lname" : "Pottepalem"}' || '{"age" : 19}' || '{"address" : "dummy address"}' WHERE id = '2eab2d99-167d-41b7-8227-d89ae45d3802'; -- check data, notice that existing properties got updated and new properties got added, including generated column age SELECT * FROM test_json; -- append another column data to a JSON column SELECT profile :: jsonb || jsonb_build_object('id', id) as profile_json_with_id_added FROM test_json;


References

Thursday, October 22, 2020

UUID support in PostgreSQL . . .

UUID (universally unique identifier) is used for unique identifiers in and across systems. It is widely supported with algorithm(s) implemented and available as library/utility in programming  languages. Also, databases have it implemented and made available in queries via DB function(s). There are 4 different version of uuid.

I recently attempted to migrate a Spring Boot application from MySQL database to PostgreSQL database. The application was written in Plain-Old-JDBC-DAO (Data Access Object interface/implementation pattern) style with hand-coded SQLs mixed and tightly coupled with Java code. Luckily there were fairly decent number of Integration Test-cases in place already. Otherwise, it would have been very challenging and nasty to identify all the SQLs mixed with code that needed migration.

In my attempt, I learned the fact that both MySQL and PostgreSQL databases have functions to generate random UUID. However, Postgres versions prior to 13 require little more effort to get access to those functions. This post is to share some details on that. I am not a database guy and would only love  touching the surface ;) 

Environment: Java 13, Spring Boot 2.2.4.RELEASE, PostgreSQL 11.8, MySQL 5.7.31 on macOS Catalina 10.15.6

After migrating schema using AWS Schema Conversion Tool (AWS SCT) and non-transactional data as SQL exports into baseline Flyway scripts, the next step was to identify code changes.

Most of integration test cases failed with org.springframework.jdbc.BadSqlGrammarException revealing all inline SQLs that needed migration. One such SQL was using MySQL uuid() function which failed with the following error, indicating that uunid() was MySQL specific:

org.postgresql.util.PSQLException: ERROR: function uuid() does not exist

MySQL - uuid support

MySql has uuid() function generates uuid of Version 1 algorithm which involves the MAC address of the computer and timestamp.

-- generates a random uuid of version 1 SELECT uuid(); 635ef48c-1498-11eb-a38e-4bb3a0084954

PostgreSQL

PostgreSQL distribution comes with additional supplied modules but are not installed by default. Any user account with CREATE privilege can install module(s). There is a uuid generation function (gen_random_uuid()) available in pgcrypto module which generates uuid of Version 4 algorithm which is derived entirely from random hexadecimal numbers.

Without having pgcrypto module installed, the SQL:
SELECT gen_random_uuid(); -- genertes random UUID

would result with the following error:
SQL Error [42883]: ERROR: function gen_random_uuid() does not exist Hint: No function matches the given name and argument types. You might need to add explicit type casts.

The following SQLs are handy to find out all available extensions and see the list of extensions installed.
SELECT * FROM pg_avilable_extensions ORDER BY name; -- list available extensions SELECT * FROM pg_extension; -- list installed extensions

To install pgcrypto extension, use the following command.
This can go into application's Flyway script to get the extension installed into the database as needed.
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- creates extension

Once, pgcrypto extension is installed, the following returns a random UUID generated:
SELECT gen_random_uuid(); -- generate a random uuid of version 4 842d3fae-7788-4ecb-b441-7c7e8130b8bf

NOTE
In PostgreSQL 13 this function is made available in the core. There is no need for installing pgcrypto module in this case.

TIP

Finding version number of a given uuid string is no big deal.
The M in uuid format: xxxxxxxx-xxxx-Mxxx-xxxx-xxxxxxxxxxxx tells the version number.

Groovy script to find out the version number of a given uuid string:
println "uuid version: ${UUID.fromString('842d3fae-7788-4ecb-b441-7c7e8130b8bf').version()}" // 4, PostgreSQL gen_random_uuid() generated println "uuid version: ${UUID.fromString('635ef48c-1498-11eb-a38e-4bb3a0084954').version()}" // 1, MySQL uuid() generated

References



Sunday, January 28, 2018

Add time-zone sense . . .

As today's applications run from the Cloud hardware and serve requests coming in from time-zones spread across the globe, date-time-zone handling becomes more sensitive than ever. Applications need to be designed and built with this sensible capability and need to have not just Date-Time sense but also added Zone sense.

Recently, I had to work on adding Timestamp and Zone capability to a Grails3 application. The initial implementation & business-logic around a couple of Date-Time sensitive fields was written just to track Date in MM/dd/yyyy format using java.util.Date. The new requirement was to capture date, timestamp and timezone. Sounds simple, but not really ;). It actually involved a bit of exploration on many fronts including: Java, Hibernate, Grails and PostgreSQL database.

Environment: Grails 3.2.3, Java 8, PostgreSQL on MacOS High Sierra 10.13.2

First Steps - Towards the right direction (Java 8 Date-Time classes)

Java 8 Date Time classes

I did some first-hand reading on Java 8 date-time classes from java.time package and an up-front research on the support for these classes in PostgreaSQL, Hibernate, Spring Boot and Grails.

Java 8 (at last) added fluent date-time-zone API classes that are immutable, thread-safe and based on ISO 8601 calendar. I looked into details of few classes like: Instant, LocalDateTime, ZonedDateTime and OffsetDateTime. From Java docs, OffsetDateTime is the class recommended for modeling date-time concepts in more detail and when communicating with database/networking. After experimenting little bit all these classes, without further exploration I chose java.time.OffsetDateTime class over java.util.Date.

java.time.Instant - an instantaneous point on the time line in UTC zone
e.g 2018-01-21T12:21:12.122Z indicating Jan 21, 2018 12:21 pm 12 seconds and 122 milliseconds, Z stands for UTC timezone)

java.time.LocalDateTime - date-time with no timezone information
e.g. 2018-01-21T07:21:12.122 indicating Sun Jan 21, 2018 07:21 am 12 seconds and 122 milliseconds with no timezone, as my system clock is set to EST, this is the time in EST America/New_York zone

java.time.ZonedDateTime - current date time in the current timezone with offset from UTC
e.g. 2018-01-21T07:21:12.122-05:00[America/New_York] indicating Sun Jan 21, 2018 07:21 am 12 seconds and 122 milliseconds with timezone offset and timezone. As my system clock is set to EST, this is the time in EST America/New_York zone. It's more like LocalDateTime with offset from UTC timezone but with local timezone)

java.time.OffsetDateTime - current date time in the current timezone with offset from UTC
e.g. 2018-01-21T07:21:12.122-05:00 indicating Sun Jan 21, 2018 07:21 am 12 seconds and 122 milliseconds with timezone offset. As my system clock is set to EST, this is the time in EST with -05:00 offset that indicates the zone. It's more like LocalDateTime with offset from UTC timezone. Offset indicates the zone.)

Other useful classes

Next Steps - Along the path (PostgreSQL)

PostgreSQL

It is quite common to leverage Grails Database Migration Plugin in a Grails application to manage database changes. When model changes, running the plugin provided dbm-gorm-diff command generates schema changes as a groovy script. For java.util.Date and java.time Date classes the plugin generated change-log script will have column types set to: TIMESTAMP WITHOUT TIME ZONE for PostgreSQL.

For instance, if a domain class had two new properties:
Agreement.groovy
class Agreement { ... Date signedDate Date expirationDate ... }

Then running dbm-gorm-diff
e.g. grails -Dgrails.env=development dbm-gorm-diff agreement-changes.groovy

would have change-log script generated for the model changes as follows:
databaseChangeLog = { changeSet(author: "Gpottepalem (generated)", id: "1234567890123-1") { createTable(tableName: "agreement") { ... column(name: "signed_date", type: "TIMESTAMP WITHOUT TIME ZONE") { constraints(nullable: "false") } column(name: "expiration_date", type: "TIMESTAMP WITHOUT TIME ZONE") { constraints(nullable: "false") } ... } } }

That clearly indicates NO time-zone details are stored in the database.

Move on - Application Code Changes (Grails)

Grails application code

Step-1 Dependencies - Add compile-time and run-time dependencies to gradle build script.

PostgreSQL jdbc driver
- Use the correct PostgreSQL JDBC driver for Java 8 date time classes support. Make sure you are using JRE 8+ driver. If not, change it:

Hibernate - Next add hibernate Java8 compile time dependency.

Grails - Then add grails Java8 dependency plugin

Following is the Gradle build script with these dependencies added:
build.gradle
dependencies { ... runtime "org.postgresql:postgresql:42.1.4" //Java JDBC 4.2 (JRE 8+) driver for PostgreSQL database compile 'org.hibernate:hibernate-java8 //hibernate java 8 java.time classes support compile "org.grails.plugins:grails-java8" //grails java 8 java.time classes support ... }

Step-2 Domain Model - Change Date types to OffsetDateTime

Agreement.groovy
class Agreement { ... OffsetDateTime signedDate OffsetDateTime expirationDate ... }

Assuming that the model had Date types earlier and the generated dbm change-log script: agreement-changes.groovy was already applied to database, now as the domain object properties are changed from java.util.Date to java.time.OffsetDateTime, we need to generate change-log script for these changes and apply them to database. But, the command dbm-gorm-diff will not generate the change-log script to have the right PostgreSQL type: TYPE TIMESTAMP WITH TIMEZONE. So, it requires a manual dbm change-log like:

e.g. agreement-date-type-changes.groovy
databaseChangeLog = { changeSet(author: 'gpottepalem', id: 'alter signed_date to store time zone') { grailsChange { change { sql.execute('ALTER TABLE myschema.my_table ALTER COLUMN signed_date TYPE TIMESTAMP WITH TIME ZONE') } } } changeSet(author: 'gpottepalem', id: 'alter expiration_date to store time zone') { grailsChange { change { sql.execute('ALTER TABLE myschema.my_table ALTER COLUMN expiration_date TYPE TIMESTAMP WITH TIME ZONE') } } } }

With this schema change applied to database, when the domain object is saved, OffsetDate gets stored in PostgreSQL database with timestamp and offset indicating the time zone.

Step-3 Views - JSON views

If you have JSON views in the application, you need to make sure that the OffsetDateTime fields' data looks good and is as expected in the JSON output produced. Unlike java.util.Date fields, java.time.OffsetDateTime fields output data won't get formatted. The JSON output for OffsetDateTime fields not only contain all of it's direct properties but also recursive properties of all of it's object references accessible via getXXX() methods. e.g. OffsetDateTime's getOffset() method's returned ZoneOffset object's getAvailableZoneIds() method's returned all zoneIds will show up as well.

For instance, if you have a view under grails-app/views/agreement/_agreement.gson like:
model { Agreement agreement } json.agreement { signedOn agreement.signedDate expiresOn agreement.expirationDate }

You will have it rendered in JSON as:
{ "agreement": { "signedOn": { "dayOfMonth": 27, "dayOfWeek": "SATURDAY", "dayOfYear": 27, "hour": 12, "minute": 44, "month": "JANUARY", "monthValue": 1, "nano": 356000000, "offset": { "availableZoneIds": [ "Asia/Aden", "America/Cuiaba", "Etc/GMT+9", ... ], "id": "-05:00", "rules": { "fixedOffset": true, "transitionRules": [], "transitions": [] }, "totalSeconds": -18000 }, "second": 35, "year": 2018 }, "expiresOn": { "dayOfMonth": 27, ... } } }

A quick solution is to simply convert java.time.OffsetDateTime to java.util.Date or simply stringify using toString() depending on your needs. The following gson template:
import java.time.Instant import java.time.OffsetDateTime model { Agreement agreement } json.agreement { signedOn agreement.signedDate.toString() //current zone date-time with offset from UTC expiresOn Date.from(agreement.expirationDate.toInstant()) //OffsetDateTime to Date in UTC timezone }
will produce the output as:
{ "agreement": { "signedOn": "2018-01-27T17:10:57.443-05:00", "expiresOn": "2018-01-27T22:10:57Z" } }

Gotcha

PostgreSQL stores offset when OffsetDateTime field is persisted. But when queried, will not give stored offset back. Instead, gives data in the timezone the database is set (for instance UTC) with no offset. Databases add their own steep learning curve to deal with date-time-zone data ;)

Tip

Grails console is the handy tool for explorations with changes like this in a Grails application.
e.g. A simple Grails script to persist and retrive Agreement object
import com.my.app.Agreement import java.time.OffsetDateTime println Agreement.count() Agreement agreement = new Agreement( signedDate: OffsetDateTime.now(), expirationDate: OffsetDateTime.now().plusYears(1) ).save(flush: true, failOnError: true) println Agreement.count() Agreement.all.each { println "id:${it.id}, signedDate:${it.signedDate} expirationDate:${it.expirationDate}" }

Summary

Date - is the central issue of Y2K problem which costed IT industry billions of dollars and isn't trivial yet. Even in modern languages and frameworks, it hasn't become as primitive as it can become, and dealing with it requires a special care and attention from all levels of the application right from presentation, business logic, and up to the persistence.

In modern Java applications, Java 8's added date.time classes have become better if not any simpler, but is a definitive way to go with, when dealing with date, time and zone in present times!

References


Sunday, October 16, 2016

Migrate Grails 2.2.1 application data from Oracle to PostgreSQL (Part-2 of 2) . . .

Phase-1 Database Migration

Part -2: Migrate Data from Oracle to PostgreSQL

In Part-1, I covered migrating Grails Application from Oracle to PostgreSQL.

In this Part-2, I cover Data Migration to conclude this two-part series. This is not a comprehensive post by any means. I only cover what I experienced in this process.

After migrating Grails application, my next step was to migrate data. I knew this is where things would become bit tricky. I always feel that Software development is interesting with many competing technologies and products, but is made messy with only few similarities but many differences. Competing technologies start to emerge with little or no standards and deviate so much. Databases are no exception.

During application development, the debate of application logic getting buried in Stored Procedures as opposed to opening up in the application's business/service layer gets to the question of "What if we change the database?". The answer, "We won't change our database" often wins the debate and takes the roots of business-logic deeper and deeper into the database. Lately, PostgreSQL has been making many to rethink of investing in expensive databases. Change is inevitable. The time to "change your database" has come- to some, at least!

My first effort

My first effort was to turn to Google and find any tool(s) that could help migrate data from Oracle to PostgreSQL. After checking several commercial tools, an open-source tool (Ora2Pg), and trying out one tool (Navicat), it was an easy decision for me: decided not to use any tool.

Most of the tools migrate both schema & data, often resulting with several datatype mismatches between Grails application's domain-model and Database schema, especially with fields like boolean that has a close matching database equivalent boolean type in PostgreSQL. It's not easy for the tools to infer these details through database schema/metadata.

The Approach

I took the well-known approach in Software Engineering: ETL - a 3-Step process of migrating data.
  1. Extraction- I leveraged SQL Developer tool to extract data out into delimited files.
  2. Transformation- I wrote Groovy scripts to transform data by transforming data of only those data types that needed special care.
  3. Load- I wrote groovy scripts to load transformed data files into PostgreSQL using its COPY command which is very cool for this task.

Data Migration Step-by-Step

Step-0: Create PostgreSQL database and generate schema

Create PostgreSQL database and generate schema by leveraging Grails and dbm-plugin. I covered this in Part-1.

Step-1: Extract data from Oracle

There are many possible ways to extract/export data from Oracle. I used SQL Developer tool to extract data into delimited multiple files. Each file contains exported data of a specific table.

Certain data types can be transformed during data export by setting preferences. Default Date format is one such that differs form database to database. With this ETL approach, dates certainly need to go through transformations due to default-format differences between databases. This transformation can easily be avoided by taking it into this step of Extraction/Export. SQL Developer offers to set data preferences for a session. With this feature, we can set preferences for Date/Timestamp fields and change the format to default PostgreSQL date format.

Set Data Preferences

Open SQL Developer and connect to database. To check current preferences, go to Preferences > Database > NLS for formats from Oracle SQL Developer main menu item.

Execute the following SQL to alter date format for that particular session:

alter SESSION set NLS_DATE_FORMAT = 'MM/DD/YYYY HH24:MI:SS';
alter SESSION set NLS_TIMESTAMP_FORMAT = 'MM/DD/YYYY HH24:MI:SSXFF';

Export Data

Go to Tools > Database Export and follow the following steps:
Select Connection
Uncheck Export DDL
Format: delimited
Delimiter: % (your preferred char, I chose % as many of special characters are found in the actual data in some column or other)
Left Enclosure: "
Right Enclosure: "
Save as: Separate Files
File: enter dir name (where the exported data to be saved)

Simply go through next steps to finish. You will have data exported into multiple files in the directory specified with each file name as <TABLE_NAME>_DATA_TABLE.dsv.  For instance if you have a table MY_TABLE, it's exported delimited data file would be: MY_TABLE_DATA_TABLE.dsv.

Step-2: Transform Data

This step requires, finding all data types that need data transformation. Once data type Date is taken care during the Extraction process, I was only left to deal with Boolean/boolean data types, transforming 1/0 to true/false. This requires identifying all such data columns in the entire schema. There are two choices for finding those columns: 1) Database metadata, 2) Grails domain model. I chose Grails domain-model and wrote a simple script to find the list of tables and columns that are mapped to Boolean/boolean properties of domain-model. I had covered this in my previous post: The power of Grails Metadata is there when you need it. Once I found those details, I got everything that I needed to transform data.

I wrote Groovy script for transforming data. Following is the Groovy script:

import groovy.json.JsonSlurper /** * oracle-to-postgres-data-mIgrator.groovy * This groovy script migrates/transforms delimited data-files exported from Oracle to data-files that are PostgreSQL * compliant. It takes care of special data migration needed for data-column types like boolean, and nullable FK columns * that get outputted as "" for nulls when left enclosure(") and right enclosure (") characters are used during data * export. * * Certain dependencies like: base directory the data-files are located in, data-file extension etc. are externalized * into postgres-migration-config.groovy file for making this script little flexible. * * Prerequisites * 1. Run script: boolean-columns-finder.groovy from the application's home dir. * 2. Export all data from Oracle database into delimited files from a tool like: SqlDeveloper. * 3. Adjust any configurations needed in postgres-data-migration-config.groovy file. * Especially config properties like: delimiter and baseDir * * Dependencies (input) * 1. Requires myAppTablesBooleanColumns.json to exist in the application home directory. This is the file that is * result of executing boolean-columns-finder.groovy that contains a map of table names and corresponding list of * boolean columns. * 2. A set of delimited data files exported using a tool like SqlDeveloper. * * Assumptions for Data Export (based on the tool used SqlDeveloper to export oracle data). * . Each table's data is exported into it's own data file with the filename like: _DATA_TABLE.dsv * . To simplify parsing, used % as the delimiter because it's one unused character found in application's data. * . Each data file contains header as the first line which is nothing but column names in capital letters. * . Used left enclosure(") and right enclosure (") characters during data export which encloses column-data like * long text that contains embedded new lines, and nullable FK's that are null. * . Also, default date format is different between oracle and PostgreSQL. The data migration date columns has been * eliminated by leveraging SqlDeveloper to export data in the format that PostgreSQL can import. * This can be achieved by running the following SQLs for the session in SqlDeveloper before data export. * alter SESSION set NLS_DATE_FORMAT = 'MM/DD/YYYY HH24:MI:SS'; * alter SESSION set NLS_TIMESTAMP_FORMAT = 'MM/DD/YYYY HH24:MI:SSXFF'; * * Result (output) * 1. Migrated data files. Each data file (_DATA_TABLE.dsv) will have a corresponding migrated data file * (_DATA_TABLE.dsv.migrated) * 2. Only data that needs to be migrated (for boolean data) goes through data migration. * The input json file: myAppTablesBooleanColumns.json contains all tables that need data migration. For every * table that needs data migration, the corresponding input data-file name is derived by the above mentioned * filename assumptions. * e.g. table: my_table, data-file: MY_TABLE_DATA_TABLE.dsv, migrated data-file: MY_TABLE_DATA_TABLE.dsv.migrated * 3. Rest of the tables' data that do not need data migration (do not have boolean columns) is a simple passthrough * with just the header migration. The header migration is simply converting column names to lowercase. * * @see postgres-data-migration-config.groovy * @see boolean-columns-finder.groovy * * @author Giri Pottepalem */ def config = new ConfigSlurper().parse(new File('postgres-data-migration-config.groovy').toURI().toURL()) def dataDelimiter = config.data.delimiter def datafileBaseDir = config.data.file.baseDir def datafileExtension = config.data.file.extension def migratedDatafileExtension = config.data.migrated.file.extension def tablesBooleanColumnsFile = config.tables.boolean.columns.file if(!new File(tablesBooleanColumnsFile).exists()){ println "Error: Missing required file ${tablesBooleanColumnsFile} for data migration." System.exit(1) } Map tablesBooleanColumnsMap = new JsonSlurper().parseText( new FileReader(tablesBooleanColumnsFile).text ) def dataFilesNeedDataMigraton = tablesBooleanColumnsMap.keySet().collect { "${it.toUpperCase()}${datafileExtension}" } def allDataFiles = new File(datafileBaseDir).listFiles().name.findAll { it.endsWith(datafileExtension) } List dataFilesJustNeedHeaderMigration = allDataFiles - dataFilesNeedDataMigraton println "Migrating just header for all data files that don't need data migration..." dataFilesJustNeedHeaderMigration.each { dataFilename -> String absoluteDataFilename = "${datafileBaseDir}/$dataFilename" print "Reading data file: $absoluteDataFilename..." File dataFile = new File(absoluteDataFilename) File migratedDataFile = new File("${absoluteDataFilename}${migratedDatafileExtension}") migratedDataFile.withWriter { fileWriter -> dataFile.eachLine { line, lineNumber -> if (lineNumber == 1) { fileWriter.writeLine(line.toLowerCase()) //migrate header } else { fileWriter.writeLine(line) //take data as is } } } println "Migrated" } println "Migrating data..." tablesBooleanColumnsMap.each { String tableName, List booleanColumns -> String dataFilename = "${datafileBaseDir}/${tableName.toUpperCase()}${datafileExtension}" print "Reading data file: $dataFilename..." File dataFile = new File(dataFilename) File migratedDataFile = new File("${dataFilename}${migratedDatafileExtension}") String header def columnNames migratedDataFile.withWriter { fileWriter -> dataFile.eachLine { String line, int lineNumber -> if (lineNumber == 1) { header = line.toLowerCase() //migrate header columnNames = header.split(dataDelimiter) fileWriter.writeLine(header) } else { //migrate data String[] columnData = line.split(dataDelimiter, -1) def booleanColumnIndexes = booleanColumns.collect { columnName -> columnNames.findIndexOf { it.equalsIgnoreCase(columnName) } } booleanColumnIndexes.each { booleanColumnIndex -> if (booleanColumnIndex < columnData.length) { switch (columnData[booleanColumnIndex]) { case null: columnData[booleanColumnIndex] = ''; break case '1': columnData[booleanColumnIndex] = 't'; break case '0': columnData[booleanColumnIndex] = 'f'; break } } } //update data that ended up as "" due to blank to '' in data exoport columnData.eachWithIndex { data, i -> if (data == '""') { columnData[i] = '' } } fileWriter.writeLine(columnData.join(dataDelimiter)) } } } println "Migrated" } println "Done"

Following is a sample of externalized configuration properties as Groovy script in order to make the above script bit flexible:
/** * postgres-data-migration-config.groovy * Configuration properties as groovy script * @author Giri Pottepalem */ data { delimiter = '%' file { baseDir = './export' //dir in which delimited data files exist extension = '_DATA_TABLE.dsv' } migrated { file { extension = '.migrated' } //need to be loaded in specific order due to foreign key constraints files = [ 'MY_TABLE1_DATA_TABLE.dsv.migrated', 'MY_TABLE2_DATA_TABLE.dsv.migrated', ... ] } } appHome = '/gateway' //myApp application home dir //file that contains list of tables and boolean columns for each table. Result of running boolean-columns-finder.groovy tables.boolean.columns.file = "${appHome}/myAppTablesBooleanColumns.json" postgres { dataSource { url = 'jdbc:postgresql://localhost:5432/myApp' user = 'postgres' //superuser for running COPY password = 'password' driverClassName = 'org.postgresql.Driver' } //Adjust sequences with values taken from oracle db by: select sequence_name, last_number from user_sequences; sequences = [ 'hibernate_sequence': 150461, 'table1_sequence': 4041 ] }

Step-3: Load Data

Once data is migrated, find out data dependencies order and load data in the required order that satisfies foreign-key constraints. I covered this in my previous post: Groovy Script to load CSV data files into PostgreSQL database onto Amazon RDS.

References