简体   繁体   中英

Converting Mongo DBObject to Entity POJO using Reflection

First I want to say that yes - I know there are ORMs like Morphia and Spring Data for MongoDB. I'm not trying to reinvent the weel - just to learn. So basic idea behind my AbstractRepository is to encapsulate logic that's shared between all repositories. Subclasses (repositories for specific entities) passes Entity class to .

Converting entity beans (POJOs) to DBObject using Reflection was pretty streightforward. Problem comes with converting DBObject to entity bean. Reason? I need to convert whatever field type in DBObject to entity bean property type. And this is where I'm stuck. I'm unable to get entity bean class in AbstractRepository method T getEntityFromDBObject(DBObject object)

I could pass entity class to this method but that would defeat the purpose of polymorphism. Another way would be to declare private T type property and then read type using Field. Defining additional property just so I can read doesn't sound right.

So the question is - how would you map DBObject to POJO using reflection using less parameteres possible. Once again this is the idea:

public  abstract class AbstractRepository<T> {
   T getEntityFromDBObject(DBObject object) {
      ....
   }
}

And specific repository would look like this:

public class EntityRepository extends AbstractRepository<T> {
}

Thanks!

Note: Ignore complex relations and references. Let's say it doesn't need to support references to another DBObjects or POJOs.

You need to build an instance of type T and fill it with the data that comes in ´DBObject´:

public abstract class AbstractRepository<T> {

    protected final Class<T> entityClass;

    protected AbstractRepository() {
        // Don't remember if this reflection stuff throws any exception
        // If it does, try-catch and throw RuntimeException 
        // (or assign null to entityClass)
        // Anyways, it's impossible that such exception occurs here
        Type t = this.getClass().getGenericSuperclass();
        this.entityClass = ((Class<T>)((ParameterizedType)t).getActualTypeArguments()[0]);
    }

    T getEntityFromDBObject(DBObject object) {
        // Use reflection to create an entity instance
        // Let's suppose all entities have a public no-args constructor (they should!)
        T entity = (T) this.entityClass.getConstructor().newInstance();

        // Now fill entity with DBObject's data
        // This is the place to fill common fields only, i.e. an ID
        // So maybe T could extend some abstract BaseEntity that provides setters for these common fields
        // Again, all this reflection stuff needs to be done within a try-catch block because of checked exceptions
        // Wrap the original exception in a RuntimeException and throw this one instead
        // (or maybe your own specific runtime exception for this case)

        // Now let specialized repositories fill specific fields
        this.fillSpecificFields(entity, object);

        return entity;
    }

    protected abstract void fillSpecificFields(T entity, DBObject object);

}

If you don't want to implement the method .fillSpecificFields() in every entity's repository, then you'd need to use reflection to set every field (including common ones such as ID, so don't set them manually).

If this is the case, you already have the entity class as a protected attribute, so it's available to every entity's repository. You need to iterate over ALL its fields, including the ones declared in superclasses (I believe you have to use method .getFields() instead of .getDeclaredFields() ) and set the values via reflection.

As a side note, I really don't know what data comes in that DBObject instance, and in what format, so please let me know if extracting fields' values from it results to be non trivial.

First I want to apologies for answering to your comments almost two months later. I did managed to figure it out on my own and here is how I've implemented it (and tested) so maybe someone will make a use of it:

public abstract class AbstractRepository<T> {

@Inject
private MongoConnectionProvider provider;

// Keeps current repository collection name
protected String collectionName;

@PostConstruct
public abstract void initialize();

public String getCollectionName() {
    return this.collectionName;
}

protected void setCollectionName(String collectionName) {
    this.collectionName = collectionName;
}

protected DBCollection getConnection() {
    DB conn = this.provider.getConnection();
    DBCollection collection = conn.getCollection(this.collectionName);
    return collection;
}

private void putFieldToDbObject(T source, DBObject target, Field field) {
    // TODO: Think more elegant solution for this
    try {
        field.setAccessible(true);
        // Should we cast String to ObjectId
        if (field.getName() == "id" && field.get(source) != null
                || field.isAnnotationPresent(DBRef.class)) {
            String key = field.getName().equals("id") ? "_id" : field.getName();
            target.put(key, new ObjectId(field.get(source).toString()));
        } else {
            if(!field.getName().equals("id")) {
                target.put(field.getName(), field.get(source));
            }
        }
    } catch (IllegalArgumentException | IllegalAccessException exception) {
        // TODO Auto-generated catch block
        exception.printStackTrace();
    } finally {
        field.setAccessible(false);
    }

}

@SuppressWarnings("rawtypes")
protected DBObject getDbObject(T entity) {
    DBObject result = new BasicDBObject();
    // Get entity class
    Class entityClass = entity.getClass();
    Field[] fields = entityClass.getDeclaredFields();
    // Update DBobject with entity data
    for (Field field : fields) {
        this.putFieldToDbObject(entity, result, field);
    }
    return result;
}

@SuppressWarnings({ "unchecked", "rawtypes" })
public T getEntityFromDBObject(DBObject object) throws MappingException {
        Type superclass = this.getClass().getGenericSuperclass();
        Type entityClass = ((ParameterizedType) superclass).getActualTypeArguments()[0];
        T entity;
    try {
        entity = ((Class<T>) entityClass).newInstance();
        // Map fields to entity properties
        Set<String> keySet = object.keySet();
        for(String key : keySet) {
            String fieldName = key.equals("_id") ? "id" : key;
            Field field = ((Class<T>) entityClass).getDeclaredField(fieldName);
            field.setAccessible(true);
            if(object.get(key).getClass().getSimpleName().equals("ObjectId")) {
                field.set(entity, object.get(key).toString());
            } else {
                // Get field type
                Type fieldType = field.getType();
                Object fieldValue = object.get(key);
                Class objectType = object.get(key).getClass();
                if(!fieldType.equals(objectType)) {
                    // Let's try to convert source type to destination type
                    try {
                        fieldValue = (((Class) fieldType).getConstructor(objectType)).newInstance(object.get(key));
                    } catch (NoSuchMethodException exception) {
                        // Let's try to use String as "man-in-the-middle"
                        String objectValue = object.get(key).toString();
                        // Get constructor for destination type that take String as parameter
                        Constructor constructor = ((Class) fieldType).getConstructor(String.class);
                        fieldValue = constructor.newInstance(objectValue);
                    }
                }
                field.set(entity, fieldValue);
            }

            field.setAccessible(false);
        }
    } catch (InstantiationException | IllegalAccessException | NoSuchFieldException | SecurityException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException e) {
        throw new MappingException(e.getMessage(), MappingExceptionCode.UNKNOWN_ERROR);
    }

    return entity;
}

public List<T> getAll() {
    DBCollection conn = this.getConnection();
    DBCursor cursor = conn.find();
    List<T> result = new LinkedList<T>();
    while (cursor.hasNext()) {
        DBObject obj = cursor.next();
        try {
            result.add(this.getEntityFromDBObject(obj));
        } catch (MappingException e) {

        }
    }
    return result;
}

public T getOneById(String id) {
    DBObject idRef = new BasicDBObject().append("_id", new ObjectId(id));
    DBCollection conn = this.getConnection();
    DBObject resultObj = conn.findOne(idRef);
    T result = null;
    try {
        result = this.getEntityFromDBObject(resultObj);
    } catch (MappingException e) {

    }

    return result;

}

public void save(T entity) {
    DBObject object = this.getDbObject(entity);
    DBCollection collection = this.getConnection();
    collection.save(object);
}

}

You've stumbled onto the problem of object mapping. There are a few libraries out there that look to help with this. You might check out ModelMapper (author here).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM