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:
@MappedEntitypublic class Customer {@Idprivate Long id;private String nombre;private String plan;...}@MappedEntity public class Customer { @Id private Long id; private String nombre; private String plan; ... }@MappedEntity public class Customer { @Id private Long id; private String nombre; private String plan; ... }
Enter fullscreen mode Exit fullscreen mode
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, free2, pepe, basic3, manolito, free1, jorge, free 2, pepe, basic 3, manolito, free1, jorge, free 2, pepe, basic 3, manolito, free
Enter fullscreen mode Exit fullscreen mode
“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;}@Overridepublic String toString() {return name;}}@MappedEntitypublic class Customer {@Idprivate Long id;private String nombre;private Plan plan;...}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; ... }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; ... }
Enter fullscreen mode Exit fullscreen mode
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;}}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; } }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; } }
Enter fullscreen mode Exit fullscreen mode
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 {...}@TypeDef(type= DataType.STRING) (1) public enum Plan { ... }@TypeDef(type= DataType.STRING) (1) public enum Plan { ... }
Enter fullscreen mode Exit fullscreen mode
| 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
|
@Factorypublic class PlanConverter {@SingletonTypeConverter<Plan, String> planStringTypeConverter(){return ((object, targetType, context) -> Optional.of(object.name()));}@SingletonTypeConverter<String, Plan> stringPlanTypeConverter(){return ((object, targetType, context) -> Arrays.stream(Plan.values()).filter(p->p.getName().equals(object)).findFirst());}}@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() ); } }@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() ); } }
Enter fullscreen mode Exit fullscreen mode
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
暂无评论内容