TypeConverter en Micronaut

Recientemente he tenido que implementar una movida para uno de mis proyectos en la que había que persistir en base de datos no sólo los datos típicos del cliente-usuario, sino que también tenía que guardar el Plan de subscripción en el que se encontraba.

La solución por la que optado está bien explicada en las guías de Micronaut pero me he decidido a escribirla aquí por el matiz de "negocio" que creo que puede ser interesante para otras situaciones.

Modelo

Digamos que tenemos una tabla donde persistimos los datos de un Customer tipo:

@MappedEntity
public class Customer {

	@Id
	private Long id;

    private String nombre;

    private String plan;

    ...
}

Donde plan era un string donde se iba a almacenar algun tipo de constante. Como no había nada en concreto para ello (ni se le esperaba) la idea era un mantenimiento a mano en la base de datos y en lugar de optar por valores numéricos que no dicen mucho se optó por un String que era más representativo, con lo que en la bbdd encontramos registros tipo

1, jorge, free
2, pepe, basic
3, manolito, free

"Problema"

Obviamente una vez que lo tienes funcionando y empiezas a guardar esos valores llega un momento en el que tienes que hacer algo con ese campo que no sea meramente guardarlo.

Así que lo primero que uno piensa es "codificar" los posibles valores de una forma más decente en lugar de los típicos static final String FREE="free"; y cambiar el tipo del atributo a un enum

"Solucion"

public enum Plan{
    FREE("free")
    BASIC("basic"),
    PREMIUM("premium");

    private final String name;

    Plan(final String name) {
        this.name = name;
    }

    public String getName(){
        return name;
    }

    @Override
    public String toString() {
        return name;
    }
}
@MappedEntity
public class Customer {

	@Id
	private Long id;

    private String nombre;

    private Plan plan;

    ...
}

Básicamente con esto Micronaut ya es capaz de guardar y recuperar registros de la bbdd asignando al atributo plan el enum correspondiente (y dando error si en la bbdd algún valor no está en el enum) SIN TENER QUE TOCAR LA BASE DE DATOS

"Problema"

Digamos que el "problema" al que me enfrentaba ahora era que, una vez recuperado un customer de la base de datos, debía conocer en qué plan se encontraba para poder sugerirle los siguientes planes (por ejemplo). En mi caso era tan "simple" como una serie de ifes pero el añadir nuevos planes, por ejemplo SUPER, haría que tuviera que revisar esos ifes

"Solucion"

Una de las posibles soluciones, tal vez la más sencilla, sería convertir al enum en un enum complejo tipo

public enum Plan {
    FREE("free", 1)
    BASIC("basic", 2),
    PREMIUM("premium", 3);

	private final String name;
	private final int level;

	Plan(String name, int level) {
		this.name = name;
		this.level = level;
	}

	public String getName() {
		return name;
	}

	public int getLevel() {
		return level;
	}
}

De esta forma ordenar planes en base a su "prioridad" es tan sencillo como ordenar por level y así puedes saber qué funcionalidades puede optar, etc.

"Problema"

Micronaut (y supongo que otros frameworks) NO saben (sin ayuda) convertir estos valores y al intentar recuperar de la base de datos algún Customer tendrás errores tipo:

"io.micronaut.data.exceptions.DataAccessException: Cannot convert type [class java.lang.String] with value [] to target type: Plan plan. Consider defining a TypeConverter bean to handle this case."

"Solucion"

Como bien nos dice Micronaut, tenemos que añadir un algo que le diga cómo convertir de un String a un Plan y viceversa (que es el objetivo de este post) para lo que hay que seguir estos pasos:

  • Anotaremos al enum Plan con un TypeDef

@TypeDef(type= DataType.STRING) (1)
public enum Plan {
    ...
}
1 Puesto que partimos de un string en el modelo inicial
  • Crearemos un Factory que cree dos ayudantes Singleton, uno para cada direccion de conversion

@Factory
public class PlanConverter {

	@Singleton
	TypeConverter<Plan, String> planStringTypeConverter(){
		return ((object, targetType, context) -> Optional.of(object.name()));
	}

	@Singleton
	TypeConverter<String, Plan> stringPlanTypeConverter(){
		return ((object, targetType, context) -> Arrays.stream(Plan.values())
			.filter(p->p.getName().equals(object))
			.findFirst()
		);
	}

}

Básicamente el primero dado un Plan devuelve un String y el segundo a la inversa, busca en el array de Plan aquel que coincida su nombre con el String proporcionado

Conclusión

Todo esto viene explicado en la guía de Micronaut Data pero el caso de uso que emplean para explicarlo no me decía mucho pero una vez leído y aplicado a mi problema me ha parecido interesante compartirlo

Follow comments at Telegram group Or subscribe to the Channel Telegram channel

2019 - 2021 | Mixed with Bootstrap | Baked with JBake v2.6.7