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

Saturday, September 17, 2022

Do more with less - the way to write code, what Groovy taught me . . .

I was not happy at all going back to verbose native Java, leaving my years of happy Groovy and Grails development on JVM. The shrinking market for Groovy pushed me out, but I am not away from it. I still stay with it and write Groovy code on any given day. At least on the side, when I explore and experience some of new Java language features along the way. One question always comes to my mind is - how did we do it in Groovy, why didn't we have to worry about this obvious expectations. The comparison always pleasantly proves that Groovy stood by it's literal word-meaning- very pleasant, to work with, and a very well thought-out and super consistent language on JVM right from day-1.

Environment: Java 17, Groovy 4.0.2 on macOS Catalina 10.15.7

In my recent Java code, I was dealing with a list of request objects that come in a specific order, and had to give the response objects back in the same order after doing some internal processing that included database query for the list by ids with Spring Data's findAllBy conventional interface. In between I built a map from the request list for faster lookup of specific object's request details. I wanted to retain the order in the map and then the Java's functional feature threw me out to know some internals and come back before take things granted for to use it.

The following is a simple code snippet comparing both Java and Groovy features in the same Groovy script. That's another beauty of Groovy, you can write both Java and Groovy code in one class file.

import java.util.stream.Collectors // a data item record Item(Integer id, String name){} //groovy List items = [ new Item(1, 'One'), new Item(2, 'Two'), new Item(3, 'Three'), new Item(4, 'Four'), new Item(5, 'Five'), new Item(6, 'Six'), new Item(7, 'Seven'), new Item(8, 'Eight'), new Item(9, 'Nine'), new Item(10, 'Ten'), ] println "Groovy\n======" println "Items[id:Integer, name:String]: ${items}" def itemsMap = items.collectEntries{[it.name(), it.id()]} println "Items map by name (order preserved): ${itemsMap}" println "Items map keys (order preserved): ${itemsMap.keySet()}" // java List itemsJava = List.of( new Item(1, "One"), new Item(2, "Two"), new Item(3, "Three"), new Item(4, "Four"), new Item(5, "Five"), new Item(6, "Six"), new Item(7, "Seven"), new Item(8, "Eight"), new Item(9, "Nine"), new Item(10, "Ten") ); System.out.println("Java\n===="); System.out.println("Items[id:Integer, name:String]:" + itemsJava); var itemsMapJava = itemsJava.stream() .collect( Collectors.toMap( item -> item.name, item -> item.id ) ); System.out.println("Items map by name (order NOT preserved): " + itemsMapJava); System.out.println("Items map keys (order NOT preserved): " + itemsMapJava.keySet().stream().toList()); var itemsMapOrderPreserved = itemsJava.stream() .collect( Collectors.toMap( item -> item.name, item -> item.id, (key1, key2) -> key1, // key conflict resolver LinkedHashMap::new // pass the underlying map implementation you want, to preserve the order, defaults to HashMap ) ); System.out.println("Items map by name (order preserved): " + itemsMapOrderPreserved); System.out.println("Items map keys (order preserved): " + itemsMapOrderPreserved.keySet().stream().toList());

The output:

Groovy ====== Items[id:Integer, name:String]: [Item[id=1, name=One], Item[id=2, name=Two], Item[id=3, name=Three], Item[id=4, name=Four], Item[id=5, name=Five], Item[id=6, name=Six], Item[id=7, name=Seven], Item[id=8, name=Eight], Item[id=9, name=Nine], Item[id=10, name=Ten]] Items map by name (order preserved): [One:1, Two:2, Three:3, Four:4, Five:5, Six:6, Seven:7, Eight:8, Nine:9, Ten:10] Items map keys (order preserved): [One, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten] Java ==== Items[id:Integer, name:String]:[Item[id=1, name=One], Item[id=2, name=Two], Item[id=3, name=Three], Item[id=4, name=Four], Item[id=5, name=Five], Item[id=6, name=Six], Item[id=7, name=Seven], Item[id=8, name=Eight], Item[id=9, name=Nine], Item[id=10, name=Ten]] Items map by name (order NOT preserved): [Eight:8, Five:5, Six:6, One:1, Four:4, Nine:9, Seven:7, Ten:10, Two:2, Three:3] Items map keys (order NOT preserved): [Eight, Five, Six, One, Four, Nine, Seven, Ten, Two, Three] Items map by name (order preserved): [One:1, Two:2, Three:3, Four:4, Five:5, Six:6, Seven:7, Eight:8, Nine:9, Ten:10] Items map keys (order preserved): [One, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten]

Conclusion

Java started to evolve, changing faster than it used to be and faster than the community can catch up- all for good. It's slowly getting to be less verbose. Still, certain obvious expectations are not so obvious in Java.

Groovy shined next to legacy Java, still shines next to modern Java, on the JVM.