¿Cómo usar el API Java para trabajar de forma transparente con JPA y Entidades en Onesait Platform?
Onesait Platform posee una arquitectura de tipo data centric, cuyo núcleo son las Entidades: abstracciones de la entidad de la base de datos, sin importar cuál sea ésta. Es por ello que todos los módulos de la Plataforma requieren de estas Entidades para poder funcionar correctamente.
Sin embargo, pueden darse situaciones en las que ya sea por requisitos técnicos o por otras decisiones en las que no podemos interferir, tengamos que trabajar directamente con las entidades de las bases de datos utilizando frameworks, como puede ser Spring Data, iBatis, etc., y tengamos que realizar las operaciones de CRUD desde ahí y no desde la Plataforma.
Claro, aunque no contemos con la gestión de estas Entidades directamente, si que nos interesa utilizar los módulos de la Plataforma, así que… ¿cómo podemos utilizar la Plataforma de modo que pueda manejar de forma transparente el concepto de entidad?
La solución es la propia Onesait Platform: contamos con una librería cliente Java que provee un cliente llamado NotifierClient de bajo nivel y con un wrapper para Sprint Boot que nos va a permitir crear las entidades desde clases Java, e informar a la Plataforma de las operaciones CRUD que realicemos a través del framework que estemos usando. Con ello, aunque la Plataforma no realice estas operaciones como tal, estará al tanto de lo que pase con las entidades. Así, al existir la definición de la entidad, se podrán usar todos los módulos sin problema.
A continuación veremos cómo funciona todo esto.
Creación de la entidad
Operaciones provistas
Este cliente provee de las siguientes operaciones:
- Creación y/o actualización de una entidad en base a una clase Java: A partir de una clase Java, se generará un JSON Schema para posteriormente crear la entidad en la Plataforma.
- Validación de un JSON de una entidad: Esta operación debería ejecutarse antes de su inserción o actualización: la Plataforma comprobará que el JSON es válido comparándolo con el JSON Schema de la entidad referenciada.
- Notificación de operaciones CRUD: Esta operación se debe ejecutar tras completarse el proceso en nuestra aplicación Java, cuando realicemos operaciones de CRUD, podremos usar el cliente para notificar a la Plataforma. Esta operación está disponible tanto en formato síncrono como asíncrono.
Creación del cliente
Vamos a poder crear el cliente de dos formas:
- Con un token de API de la Plataforma: de tipo X-OP-APIKey.
- Con usuario y contraseña: se utilizará OAuth2 para la autenticación.
Por ejemplo:
#Configuration
public class NotifierClientConfig {
private static final String USER = "developer";
private static final String PASS_WORD = "UberSecret!2495!";
private static final String SERVER = "http://localhost";
private static final String API_KEY = "45012g6hj8k6l2k1b0n86y8uj4t823";
@Bean("notifierClientApiKey")
public NotifierClient notifierClientApiKey() {
return new NotifierClient(SERVER, API_KEY, false);
}
@Bean("notifierClientApiKey")
public NotifierClient notifierClientOauth() {
return new NotifierClient(SERVER, USER, PASS_WORD, false);
}
}
El último parámetro del constructor sirve para saltarse la validación SSL, para el caso de entornos que tengan certificados auto-firmados.
Creación de la Entidad partir de la clase Java
A continuación veamos un ejemplo de entidad de JPA:
@Entity
@Table(name = "letter")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EqualsAndHashCode (onlyExplicitly Included = true)
@OPEntity
public class Letter {
@Id
@GeneratedValue
@EqualsAndHashCode.Include
@JsonProperty(required = true)
private UUID id;
@Column(name = "task_type", length = 100)
private String taskType;
@Column(name = "task_select_attribute", length = 50)
private String taskSelectAttribute;
@Column(name = "task_select_value", length = 100)
private String taskSelectValue;
@Column(name = "report_id", length = 50)
private String reportId;
@OneToMany (mappedBy = "letter", cascade = CascadeType.ALL, orphanRemoval = true, fetch FetchType.EAGER)
@JsonManagedReference
private final Set<LetterAttributes> letterAttributes = new HashSet<>();
}
@Entity
@Table(name = "letter_attributes")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EqualsAndHashCode (onlyExplicitly Included = true)
@OPEntity
public class LetterAttributes {
@Id
@GeneratedValue
@EqualsAndHashCode.Include
@JsonProperty (required = true)
private UUID id;
@Column(name = "task_attribute", length = 50)
private String taskAttribute;
@ManyToOne(fetch FetchType.EAGER)
@OnDelete(action OnDeleteAction.CASCADE)
@JoinColumn(name = "letter", nullable = false, foreignKey = @ForeignKey(name = "fk_letter_attributes_letter")
@JsonIgnore
@JsonManagedReference
private Letter letter;
}
Para la generación y/o actualización de la Entidad en la Plataforma haremos uso de todas las anotaciones de la librería Jackson Fasterxml. Esto nos dará una serie de ventajas, como por ejemplo:
- @JsonProperty(required = true): para las propiedades requeridas.
- @JsonIgnore y @JsonManagedReference: para evitar bucles infinitos de serialización.
Un buen momento para crear la Entidad en la Plataforma de nuestra entidad JPA puede ser al inicio de la aplicación. De esta forma siempre nos aseguraremos de que nuestra Entidad existe y está actualizada a su última versión del JSON Schema.
@PostConstruct
void createOrUpdateEntity() {
notifierClientApiKey().createOrUpdateOntology(Message.class);
}
Como veremos, esto nos creará la Entidad en la Plataforma con el siguiente JSON Schema:
Validación del JSON Schema
Cuando queramos guardar datos o actualizarlos, podremos usar este método para comprobar que la instancia JSON es correcta. Deberemos pasarle, o bien el nombre de la Entidad y el JSON en formato String, o bien el objeto que será serializado a JSON internamente:
notifierClient.validateSchema(message);
notifierClient.validateSchema("Message", json);
En caso de que la validación fallase, se lanzaría una excepción del tipo NotifierException con los errores de validación. Un ejemplo sería:
ERROR com.minsait.onesait.platform.client.NotifierClient - Validation failed with following errors Error processing data:{"message":"hello"}by:{"level":"error","schema":{"loadingURI":"#","pointer":""},"instance":{"pointer":""},"domain":"validation","keyword":"required","message":"object has missing required properties ([\"idMessage\",\"toMessage\"])","required":["idMessage","toMessage"],"missing":["idMessage","toMessage"]}
En caso contrario, el código seguirá su flujo normalmente.
Notificación a Onesait Platform
En función de las necesidades que tengamos, podemos utilizar la versión síncrona o la asíncrona. Deberemos pasarle un objeto Notification, en el que le indicaremos:
- QueryType: sólo si es operación QUERY. Enum {SQL, NATIVE}
- Query: sólo si es operación QUERY (la consulta en sí, vamos).
- Payload: para INSERT/UPDATE, que contendrá la instancia de la Entidad serializada; el equivalente a un toString()
- Operation: Enum {INSERT,UPDATE,DELETE,QUERY}
- Id: identificador de la instancia, para casos como el DELETE/UPDATE por ID.
Veamos un ejemplo:
// Sync
notifierClient.notify(Notification.builder().ontology("Message").id(message.getIdMessage()).operation(Operation.INSERT).payload(json).build());
// Async
notifierClient.notifyAsync(Notification.builder().ontology("Message").id(message.getIdMessage()).operation(Operation.INSERT).payload(json).build());
Ejemplo de uso
Podemos hacer uso de estas operaciones descritas en nuestra lógica de negocio; por ejemplo, cuando se cree un mensaje nuevo en base de datos, podemos previamente validar su contenido, y posteriormente notificarlo a la Plataforma:
@Overrider
public void createMessage(Message message) {
if (!Message.MessageStatus.PENDING.equals(message.getStatusMessage())) {
message.setStatusMessage(MessageStatus.PENDING);
}
if (message.getIdMessage() == null) {
message.setIdMessage(UUID.randomUUID().toString());
}
// Validate Schema
notifierClient.validateSchema(message);
messageRepository.save(message);
// Notify Onesait Platform
String json = null;
try {
json = mapper.writeValueAsString(message);
} catch (final JsonProcessingException e) {
log.error("Invalid Json");
throw new RuntimeException();
}
notifierClient.notifyAsync(Notification.builder().ontology("Message").id(message.getIdMessage()).operation(Operation.INSERT).payload(json)build());
De esta forma, si por ejemplo tenemos un flujo creado en el FlowEngine que trabaja con la entidad Message, recibiremos las notificaciones como si fuese la Plataforma quien estuviese haciendo las operaciones:
En nuestro repositorio de GitHub tenemos un ejemplo de uso de esta librería con algunos tests montados:
Para que funcione en el application.yml tendremos que cambiar las credenciales asociadas al cliente y al usuario (el API Key):
onesaitplatform:
iotclient:
token: 3b8a25fe233b4a45a19d1308bbb9073a
deviceTemplate: client
device: client
urlRestIoTBroker: https://development.onesaitplatform.com
notifierclient:
enabled: true
server: https://development.onesaitplatform.com
username: <username>
password: <password>
apikey: QdYmw02uPRdL4Slof1dqktSGG41mssCVB20yGAM
Wrapper para Spring Boot
Para el desarrollo de aplicaciones con Spring Boot se provee un wrapper con el objetivo de facilitar el uso de esta librería.
Propiedades
En primer lugar, habrá que inyectar la dependencia correspondiente:
<dependency>
<groupId>com.minsait.onesait.platform</groupId>
<artifactId>onesaitplatform-iotclient4springboot</artifactId>
<version>2.1.0-RELEASE</version>
<!-- For Spring Boot 3.X+ <version>10.0.0</version> -->
</dependency>
También se deberán indicar las siguientes propiedades:
onesaitplatform:
notifierclient:
enabled: true
server: https://development.onesaitplatform.com
username: <username>
password: <password>
#apikey: alternativa a username + password
Lo bueno de todo esto es que vamos a poder configurar el cliente para que use credenciales de usuario y contraseña, o que utilice una API Key de la Plataforma.
Anotación @OPEntity
Esta anotación se utilizará en las clases del modelo de nuestra aplicación. Por ejemplo, si utilizamos Spring Data:
@Document(collection = "Message")
@OPEntity
public class Message {
public enum MessageType {
MAIL, SMS
}
@Id
@JsonProperty(required = true)
private String idMessage;
private String txtMessage;
private MessageType typeMessage;
private String fromMessage;
}
El uso de esta anotación provocará la creación y/o actualización de una Entidad a partir de la propia clase Java en el arranque de la aplicación.
Para que los atributos queden como requeridos en el JSON Schema creado a partir de la clase, se deberá hacer uso de la anotación de JSON:
@JsonProperty(required = true)
Anotación @OPValidateSchema
Esta anotación lanzará una validación contra el esquema de la Entidad de manera síncrona, por lo que si falla, se lanzará una excepción y alterará el flujo del código. Se utiliza a nivel de atributo/argumento.
@Override
Message save(@OPValidateSchema Message entity);
Anotación @OPNotifierOperation
Esta anotación permite notificar a la Plataforma las operaciones que ocurren en nuestra aplicación, tanto de manera síncrona como de manera asíncrona. Se utiliza a nivel de método, y tiene los siguientes argumentos:
- async: boolean default false. Indica si la notificación se manda de manera síncrona o asíncrona.
- ontology: nombre de la Entidad de la Plataforma. Por ejemplo: Message.
- operationType: QUERY, INSERT, UPDATE, DELETE. Tipo de operación que se realiza.
- queryType: SQL, NATIVE, default NATIVE. Cómo en la mayoría de los casos se hará uso de las interfaces de Spring Data / JPA, no se hará uso de este campo.
- id: expresión SpEL que indica qué argumento/valor del método se utilizará como id para la notificación. Este id es el identificador único de la instancia con la que se está operando. Por ejemplo: “#p0”.
- payload: expresión SpEL que indica que argumento/valor del método se utilizará como cuerpo de la notificación. Por ejemplo: “#p1”.
Ejemplos de uso
INSERT
@OPNotifierOperation(ontology = "Message", operationType = OperationType.INSERT, async = true)
public void createMessage(@OPValidateSchema Message message) {
....
}
QUERY
@OPNotifierOperation(ontology = "Message", async = true)
List<Message> findByToMessage(String toMessage);
@OPNotifierOperation(ontology = "Message", async = false)
Message findByIdMessage(String idMessage);
UPDATE
@Override
@OPNotifierOperation(ontology = "Message", id = "#p0.idMessage", payload = "#p0", operationType = OperationType.UPDATE, async = true)
public void updateMessage(@OPValidateSchema Message message) {
messageRepository.save(message);
}
DELETE
@OPNotifierOperation(async = false, ontology = "Message", operationType = OperationType.DELETE, id = "#p0")
public void deleteMessage(String idMessage) {
messageRepository.deleteById(idMessage);
}
GitHub: Onesait Platform.