Same erasure on for subclass that maps parent method to a different class

Anthony :

I am attempting to refactor a DAO to make it a bit more usable in our code base. We currently have a parameterized AbstractDao that takes in three types:

  1. The database table
  2. The database pojo
  3. A different mapped pojo representation of 2)

So it ends up looking like:

public class AbstractDao<T extends DatabaseTable, R extends DatabaseRecord, M> {
  public AbstractDao(Connection connection, Mapper<R,M> mapper) {
  //save connection and mapper to protected variables
}
public List<M> insert(List<M> records) {
 connection.insertBulk(
   StreamEx.of(records).map(mapper::map).toList()
 );
 }
}

However, this doesn't work on the classic DAO case where we are dealing only with the pojo and the table.

However, there is a common functionality here that can be abstracted into a more basic AbstractDao that is useful across projects. Something like:

AbstractDao<T extends DatabaseTable, R extends Record>

which has a subclass

AbstractMappedDao<T extends DatabaseTable, R extends Record, M> extends AbstractDao<T, R>

The Abstract has a method like:

public List<R> insert(List<R> records) {
  connection.insertBulk(records);
}

and the Mapped should have a method like:

public List<M> insert(List<M> records) {
  super.insert(StreamEx.of(records).map(mapper::map).toList());
}

However, this gives a "same erasure" issue because insert takes in a List of generics.

I have tried abstracting it out into an interface:

public interface Dao<T> {
  public List<T> insert(List<T> records);
}

And making Abstract implement Dao and Mapped implement Dao, but again, same issue.

So my question is how to best approach this problem? This works as expected if I change map's signature to something like:

insertMapped(List<M> mapped);

But I would prefer to keep the contract the same.

Thanks for the help. Looking forward to the discussion!

Marco R. :

When it comes down to composing behavior it is always best to either use composition over inheritance, and this is actually your case. The mapper does not augment behavior already existing in your Dao as much as adds behavior, an extra layer of indirection in it; which is not necessarily a concern of the Dao, like an aspect/cross cutting concern.

So, my recommendation is to create a single AbstractDao class with the ability to compose mappers (you can have just one as you wanted; but with composition is easy to allow a single Dao object to support multiple mappers):

        private Map<Class, Function> mappers;

        public <M> void registerMapper(Class<M> mappingClass, Function<M, R> mapper) {
            mappers.put(mappingClass, mapper);
        }

Then create an insert method that allows for handling the pre-transformation of the records that do not extend Record using the mappers that it has registered, like this:

        public <M> List<M> insert(List<M> records) {
            if (records.isEmpty()) return records;
            M rec = records.get(0);

            List<? extends Record> actualRecords = (rec instanceof Record) ? 
                    (List<Record>)records : createMappedRecords(records, rec.getClass());

            connection.insertBulk(actualRecords);
            return records;
        }

This is cleaner, more robust and more extensible, since your insert can be made to entertain all sorts of concerns in a centralized manner with composed concerns. The full compiling code would look like something like this:

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

public class ParentErasure {

    public abstract class AbstractDao<T extends DatabaseTable, R extends Record> {
        private Connection connection;
        private Map<Class, Function> mappers = new HashMap<>();

        public <M> void registerMapper(Class<M> mappingClass, Function<M, R> mapper) {
            mappers.put(mappingClass, mapper);
        }

        public <M> List<M> insert(List<M> records) {
            if (records.isEmpty()) return records;
            M rec = records.get(0);

            List<? extends Record> actualRecords = (rec instanceof Record) ? 
                    (List<Record>)records : createMappedRecords(records, rec.getClass());

            connection.insertBulk(actualRecords);
            return records;
        }

        private <M> List<R> createMappedRecords(List<M> records, Class<? extends Object> recordsClazz) {
            Function<M, R> mapper = mappers.get(recordsClazz);
            return records.stream()
                    .map(mapper::apply)
                    .collect(Collectors.toList());
        }
    }

    public interface Dao<T> {
        public List<T> insert(List<T> records);
    }
}

class Record {}
class DatabaseTable {}
class DatabaseRecord {}
class Connection {
    public void insertBulk(List<? extends Record> records) {}
}

Complete code on GitHub

Hope this helps.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=79073&siteId=1