Blog-Archiv

Dienstag, 29. August 2023

Java Functional flatMap Example

Java version 8 (1.8) got a functional extension. That means you can pass around a "function" as method- or even constructor-parameter. The "function" always is a method of an object (or class), and it stays connected to the fields of its object (or class) when passed around.

This is very useful in relation with collections and their predefined functions, but you need to understand the meaning (semantic) of the function to be able to apply it. How would you understand a function named flatMap()? Here is its JavaDoc:

Returns a stream consisting of the results of replacing each element of this stream with the contents of a mapped stream produced by applying the provided mapping function to each element. Each mapped stream is closed after its contents have been placed into this stream. (If a mapped stream is null an empty stream is used, instead.)

An API note tells us a little more:

The flatMap() operation has the effect of applying a one-to-many transformation to the elements of the stream, and then flattening the resulting elements into a new stream.

The function should not be confused with map(), which would not alter the number of elements in the stream, but flatMap() would.

Clarification: both functions do not handle key-value pairs, like java.util.Map does.

Example Code

The flatMap() function is useful wherever a parent-object contains a collection of child-objects. You can turn the stream of parent-objects into a stream of child-objects by flat-mapping. For that, the the flatMap() function requires a parameter that is a function (or lambda) returning the child collection's stream (see line 39).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class FlatMapExample
{
    private static class Room
    {
    }
    
    private static class House
    {
        private final List<Room> rooms = new ArrayList<>();
        
        public List<Room> getRooms() {
            return Collections.unmodifiableList(rooms);
        }
        public void addRoom(Room room) {
            rooms.add(room);
        }
    }
    
    public static void main(String[] args) {
        final House house1 = new House();
        house1.addRoom(new Room());
        
        final House house2 = new House();
        house2.addRoom(new Room());
        house2.addRoom(new Room());
        
        final House house3 = new House();
        house3.addRoom(new Room());
        house3.addRoom(new Room());
        house3.addRoom(new Room());
        
        final List<House> houses = List.of(
                house1, 
                house2, 
                house3);
        
        final long numberOfRooms = houses
                .stream()
                .flatMap(house -> house.getRooms().stream())
                .count();
        
        System.out.println("numberOfRooms = "+numberOfRooms);
    }
}

Line 3 defines a child class definition Room, line 7 defines a parent class definition House. The example uses houses as parent- and rooms as child-objects, a house can contain 0..n rooms.

From line 20 on, some houses are built. The instance house1 contains one room, house2 contains two rooms, house3 contains three rooms (to make it easy:-). All houses are collected into a list of houses on line 32.

Now how can we find out the number of rooms in all houses? The answer is on line 37 and 39. We need to flat-map the house-stream to a room-stream and then count it.

Output of this application is:

numberOfRooms = 6

If you count all rooms between line 21 and 30, you will find out that there are 6, so the result given by the application is correct.

Hope this was helpful!




Firefox File URL Cross-Origin Request Blocked

If you use a web-page that loads resources by file:/// URLs, you may get this error message from Firefox console (open the debugger with F12 key):

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at file:///....... (Reason: CORS request not http)

You can circumvent this in Firefox 112.0.2 by following actions:

  • Open a new tab in your Firefox
  • In the address line, enter "about:config"
  • In the search field on top, enter "security.fileuri.strict_origin_policy"
  • Set it to false by using the toggle-button on the right side

Now your page should work. If not, you may have an older Firefox where this was called "privacy.file_unique_origin", or Firefox has disabled the property meanwhile.

Mind that you may have opened a security hole, and you should reset this property to true as soon as possible!




Mittwoch, 2. August 2023

JPA Criteria Query Examples with Subqueries

This article is about JPA database actions on an unidirectional many-to-many relation between two entity-types. You will find associated JPQL (Java Persistence Query Language) and Criteria-API queries here. Additionally my article about Criteria API may be helpful.

Entity Base Class

All annotations in subsequent classes are imported from javax.persistence and thus do not refer to any JPA provider (like Hibernate or EclipseLink).

Following is useful as base class for database entities with an required identity-attribute (Id):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@MappedSuperclass
public abstract class BaseEntity
{
    @Id
    private String id = UUID.randomUUID().toString();

    public final String getId() {
        return id;
    }
    
    @Override
    public final boolean equals(Object o) {
        if (this == o)    // performance optimization
            return true;
        
        if (o == null || getClass() != o.getClass()) // exclude aliens
            return false;   // and one-to-one entities with same id
        
        return id.equals(((BaseEntity) o).id); // delegate equality to id
    }
    
    @Override
    public final int hashCode() {
        return id.hashCode();
    }
}

Having an Id value already at construction time solves the problems with entity-objects lost in hash containers (due to broken hashcode/equals contract when delegating to an ID field whose value is null after construction). With this class, we can delegate both hashCode() and equals() to the id field at any time.

Entity Classes

An individual can be member of several collectives, a collective can refer to several individuals.

Both of following entity types represents a database table, while the JPA provider (e.g. Hibernate EntityManager) creates a third table Collective_Individual, implementing the m:n relation between Individual and Collective.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Entity
public class Individual extends BaseEntity
{
    private String name;
    
    public Individual() {
    }
    public Individual(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    @Override
    public String toString() {
        return getName();
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Entity
public class Collective extends BaseEntity
{
    private String name;

    @ManyToMany(cascade = CascadeType.ALL)
    private Set<Individual> individuals = new HashSet<>();
    
    public Collective() {
    }
    public Collective(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    public Set<Individual> getIndividuals() {
        return individuals;
    }
    public void setIndividuals(Set<Individual> individuals) {
        this.individuals = individuals;
    }
    
    @Override
    public String toString() {
        return getName()+" "+getIndividuals();
    }
}

A collective holds a list of individuals, but individuals do not refer backwards with a list of collectives they are in. It may be illegal to cascade the deletion of a collective to the related individuals. Nevertheless the relation cascades with CascadeType.ALL, which is quite useful to avoid orphaned individuals. When JPA deletes a collective with this cascade-type, it will automatically delete its individuals, but only those that are not referenced by a still existing collective (see according test below).

Test Frame

Here is the test class frame, including test data. Please find the test's base classes (AbstractJpaTest, JpaUtil etc.) in my article about JPA Unit Tests across Providers and Database Products. Such a test can be run on several JPA providers, crossed with several database products. It does not need persistence.xml configuration. It also truncates all database tables after each test.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class IndividualToCollectiveTest extends AbstractJpaTest
{
    @Override
    protected List<Class<?>> getManagedClasses() {
        return List.of(Collective.class, Individual.class);
    }

    @Override
    protected PersistenceUnitInfo[] getPersistenceUnits(List<Class<?>> managedClasses)    {
        return new PersistenceUnitInfo[] {
            new HibernatePersistenceUnit(managedClasses)
        };
    }

    @Override
    protected Properties[] getDatabasePropertiesSets(String persistenceUnitName)  {
        return new Properties[] {
            new H2Properties(persistenceUnitName)
        };
    }
    
    private Individual leader1 = new Individual("Leader 1");
    private Individual deputy1 = new Individual("Deputy 1");
    private Individual sharedMember = new Individual("Shared Member");
    private Individual member21 = new Individual("Member 2/1");
    private Individual orphan = new Individual("Orphan");
    private Collective collective1 = new Collective("Collective 1");
    private Collective collective2 = new Collective("Collective 2");
    
    private void insertMembers(EntityManager entityManager) {
        collective1.getIndividuals().add(sharedMember);
        
        collective2.getIndividuals().add(sharedMember);
        collective2.getIndividuals().add(member21);
        
        JpaUtil.persistAll(new Object[] { collective1, collective2, orphan }, entityManager);
        
        final List<Collective> collectives = JpaUtil.findAll(Collective.class, entityManager);
        assertEquals(2, collectives.size());
        
        final List<Individual> individuals = JpaUtil.findAll(Individual.class, entityManager);
        assertEquals(3, individuals.size());
    }

    // insert tests and queries here ....

}

The getManagedClasses() implementation on line 4 lists all involved persistence classes, in deletion dependency order. (A collective must be deleted before the individuals it refers to.) This method, together with the methods on line 9 and 16, replaces persistence.xml.

There are not many but sufficient test data. We have two collectives, the first has a member "Shared Member" (individual) that also belongs to the second collective. The second collective has an additional member "Member 2/1" that does not belong to any other collective. There is also an "Orphan" individual that belongs to no collective at all.

Cascading Deletion Test

The following test removes "Collective 2" and asserts that "Shared Member" is still present afterwards, but "Member 2/1" has been deleted by cascading.

    @Test
    public void sharedPersonsShouldBeRetainedOnRemoveCascading() {
        executeForAll(entityManager -> {
            insertMembers(entityManager);
            
            // remove a collective cascading
            JpaUtil.remove(collective2, entityManager);
            
            final List<Collective> collectives = JpaUtil.findAll(Collective.class, entityManager);
            assertEquals(1, collectives.size());
            assertTrue(collectives.contains(collective1));
            
            final List<Individual> individuals = JpaUtil.findAll(Individual.class, entityManager);
            assertEquals(2, individuals.size());
            assertFalse(individuals.contains(member21)); // was just in deleted collective
            assertTrue(individuals.contains(sharedMember)); // still present because in both collectives
            assertTrue(individuals.contains(orphan)); // still present because in no collective
        });
    }

This behavior should happen due to the @ManyToMany(cascade = CascadeType.ALL) annotation on the Set individuals in class Collective.

Query Tests

Orphan Query

The next test calls a query that finds out orphans that are not referred to by any collective.

    @Test
    public void findOrphans() {
        executeForAll(entityManager -> {
            insertMembers(entityManager);
            
            List<Individual> orphans = queryOrphanedIndividuals(entityManager);
            System.err.println(orphans);
            assertEquals(1, orphans.size());
            assertTrue(orphans.contains(orphan));
        });
    }

Here is the query, in both JPQL and Criteria-API form. Mind that the JPA layer generated an artificial table Collective_Individual that has no Java class representation and implements the m:n relation between collective and individual.

        select i 
        from Individual i 
        where  not exists (
            select c.id
            from Collective c 
            inner join Collective_Individual c2i on c.id = c2i.Collective_id 
            inner join Individual i2c on c2i.individuals_id = i2c.id
            where i.id = i2c.id
         )
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    private List<Individual> queryOrphanedIndividuals(EntityManager entityManager) {
        final CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        
        final CriteriaQuery<Individual> individualQuery = builder.createQuery(Individual.class);
        final Root<Individual> individualRoot = individualQuery.from(Individual.class);
        
        final Subquery<Collective> collectiveQuery = individualQuery.subquery(Collective.class);
        final Root<Collective> collectiveRoot = collectiveQuery.from(Collective.class);
        collectiveQuery.select(collectiveRoot);
        final Join<Collective,Individual> collectiveJoin = collectiveRoot.join("individuals"); // OneToMany property in Collective
        collectiveQuery.where(builder.equal(individualRoot.get("id"), collectiveJoin.get("id")));
        
        individualQuery.where(builder.not(builder.exists(collectiveQuery)));
        
        return entityManager.createQuery(individualQuery).getResultList();
    }

Criteria queries are not easy to read, but they are fully typed and their logic is reusable. Mind that the property names "individuals" and "id" on lines 10 and 11 should come from source code generated by some generator like the Maven artifact hibernate-jpamodelgen.

Membership Count Query

Following test queries individuals with no collective membership, then with one membership, then with two.

    @Test
    public void findIndividualsWithNumberOfMemberships() {
        executeForAll(entityManager -> {
            insertMembers(entityManager);
            
            List<Individual> orphans = queryIndividualsHavingNumberOfMemberships(entityManager, 0L);
            assertEquals(1, orphans.size());
            assertEquals(orphan, orphans.get(0));
            
            List<Individual> inOneCollective = queryIndividualsHavingNumberOfMemberships(entityManager, 1L);
            assertEquals(1, inOneCollective.size());
            assertTrue(inOneCollective.contains(member21));
            
            List<Individual> inTwoCollectives = queryIndividualsHavingNumberOfMemberships(entityManager, 2L);
            assertEquals(1, inTwoCollectives.size());
            assertTrue(inTwoCollectives.contains(sharedMember));
        });
    }

Following query delivers individuals that are in 0, 1, 2 or ... collectives. The number of memberships is passed as runtime-parameter to the query.

        select i
        from Individual i 
        where (
            select count(c.id) 
            from Collective c 
            inner join Collective_Individual c2i on c.id = c2i.Collective_id 
            inner join Individual i2c on c2i.individual_id = i2c.id 
            where i.id = i2c.id
        ) = :numberOfMemberships
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    private List<Individual> queryIndividualsHavingNumberOfMemberships(EntityManager entityManager, Long numberOfMemberships) {
        final CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        
        final CriteriaQuery<Individual> individualQuery = builder.createQuery(Individual.class);
        final Root<Individual> individualRoot = individualQuery.from(Individual.class);
        
        final Subquery<Long> collectiveCountQuery = individualQuery.subquery(Long.class); // is a count query
        final Root collectiveCountRoot = collectiveCountQuery.from(Collective.class);
        collectiveCountQuery.select(builder.count(collectiveCountRoot.get("id")));
        final Join collectiveJoin = collectiveCountRoot.join("individuals"); // ManyToMany property in Collective
        collectiveCountQuery.where(builder.equal(individualRoot.get("id"), collectiveJoin.get("id")));
        
        individualQuery.where(builder.equal(collectiveCountQuery, numberOfMemberships));
        
        return entityManager.createQuery(individualQuery).getResultList();
    }

Both queries are good examples of how to add nested sub-queries via Criteria-API. Mind that you do not need to explictly join the Collective_Individual table in the criteria-join on line 10!

I do not want to discuss every line of code here. Criteria queries are hard to write and hard to read. You need to understand the involved classes, and that Predicates (WHERE clause conditions) always come from the CriteriaBuilder.

Good luck:-)