Créer une application Spring Kotlin avec Google Cloud Platform

1. Introduction

Le framework Spring 5.0 est compatible avec Kotlin dédié, ce qui facilite l'utilisation de Spring pour les développeurs Kotlin. Par conséquent, avec ces modifications, les intégrations Google Cloud fournies par Spring Cloud GCP fonctionnent également parfaitement en Kotlin. Dans cet atelier de programmation, vous verrez à quel point il est facile de commencer à utiliser les services Google Cloud dans vos applications Kotlin.

Cet atelier de programmation explique comment configurer en Kotlin une application d'enregistrement simple qui illustre l'utilisation de services GCP tels que Cloud Pub/Sub et Cloud SQL.

Ce que vous allez faire

Dans cet atelier de programmation, vous allez configurer une application Kotlin Spring Boot qui accepte les informations du titulaire, les publie dans un sujet Cloud Pub/Sub et les conserve dans une base de données Cloud MySQL.

Points abordés

Intégrer les services Google Cloud à votre application Spring Kotlin

Prérequis

  • Un projet Google Cloud Platform
  • Un navigateur tel que Chrome ou Firefox

Comment allez-vous utiliser ce tutoriel ?

Je vais le lire uniquement Je vais le lire et effectuer les exercices

Comment évalueriez-vous votre expérience en matière de création d'applications Web HTML/CSS ?

Débutant Intermédiaire Expert

Quel est votre niveau d'expérience avec les services Google Cloud Platform ?

<ph type="x-smartling-placeholder"></ph> Débutant Intermédiaire Expert
.

2. Préparation

Configuration de l'environnement d'auto-formation

  1. Connectez-vous à Cloud Console, puis créez un projet ou réutilisez un projet existant. (Si vous n'avez pas encore de compte Gmail ou G Suite, vous devez en créer un.)

dMbN6g9RawQj_VXCSYpdYncY-DbaRzr2GbnwoV7jFf1u3avxJtmGPmKpMYgiaMH-qu80a_NJ9p2IIXFppYk8x3wyymZXavjglNLJJhuXieCem56H30hwXtd8PvXGpXJO9gEUDu3cZw

ci9Oe6PgnbNuSYlMyvbXF1JdQyiHoEgnhl4PlV_MFagm2ppzhueRkqX4eLjJllZco_2zCp0V0bpTupUSKji9KkQyWqj11pqit1K1faS1V6aFxLGQdkuzGp4rsQTan7F01iePL5DtqQ

8-tA_Lheyo8SscAVKrGii2coplQp2_D1Iosb2ViABY0UUO1A8cimXUu6Wf1R9zJIRExL5OB2j946aIiFtyKTzxDcNnuznmR45vZ2HMoK3o67jxuoUJCAnqvEX6NgPGFjCVNgASc-lg

Mémorisez l'ID du projet. Il s'agit d'un nom unique permettant de différencier chaque projet Google Cloud (le nom ci-dessus est déjà pris ; vous devez en trouver un autre). Il sera désigné par le nom PROJECT_ID tout au long de cet atelier de programmation.

  1. Vous devez ensuite activer la facturation dans Cloud Console pour pouvoir utiliser les ressources Google Cloud.

L'exécution de cet atelier de programmation est très peu coûteuse, voire gratuite. Veillez à suivre les instructions de la section "Nettoyer" qui indique comment désactiver les ressources afin d'éviter les frais une fois ce tutoriel terminé. Les nouveaux utilisateurs de Google Cloud peuvent participer au programme d'essai sans frais pour bénéficier d'un crédit de 300 $.

Google Cloud Shell

Bien que Google Cloud puisse être utilisé à distance depuis votre ordinateur portable, nous allons utiliser Google Cloud Shell dans cet atelier de programmation, un environnement de ligne de commande exécuté dans le cloud.

Activer Cloud Shell

  1. Dans Cloud Console, cliquez sur Activer Cloud Shell H7JlbhKGHITmsxhQIcLwoe5HXZMhDlYue4K-SPszMxUxDjIeWfOHBfxDHYpmLQTzUmQ7Xx8o6OJUlANnQF0iBuUyfp1RzVad_4nCa0Zz5LtwBlUZFXFCWFrmrWZLqg1MkZz2LdgUDQ.

zlNW0HehB_AFW1qZ4AyebSQUdWm95n7TbnOr7UVm3j9dFcg6oWApJRlC0jnU1Mvb-IQp-trP1Px8xKNwt6o3pP6fyih947sEhOFI4IRF0W7WZk6hFqZDUGXQQXrw21GuMm2ecHrbzQ

Si vous n'avez encore jamais démarré Cloud Shell, un écran intermédiaire s'affiche en dessous de la ligne de séparation pour décrire de quoi il s'agit. Si tel est le cas, cliquez sur Continuer (cet écran ne s'affiche qu'une seule fois). Voici à quoi il ressemble :

kEPbNAo_w5C_pi9QvhFwWwky1cX8hr_xEMGWySNIoMCdi-Djx9AQRqWn-__DmEpC7vKgUtl-feTcv-wBxJ8NwzzAp7mY65-fi2LJo4twUoewT1SUjd6Y3h81RG3rKIkqhoVlFR-G7w

Le provisionnement et la connexion à Cloud Shell ne devraient pas prendre plus de quelques minutes.

pTv5mEKzWMWp5VBrg2eGcuRPv9dLInPToS-mohlrqDASyYGWnZ_SwE-MzOWHe76ZdCSmw0kgWogSJv27lrQE8pvA5OD6P1I47nz8vrAdK7yR1NseZKJvcxAZrPb8wRxoqyTpD-gbhA

Cette machine virtuelle contient tous les outils de développement nécessaires. Elle intègre un répertoire d'accueil persistant de 5 Go et s'exécute sur Google Cloud, ce qui améliore nettement les performances réseau et l'authentification. Vous pouvez réaliser une grande partie, voire la totalité, des activités de cet atelier dans un simple navigateur ou sur votre Chromebook.

Une fois connecté à Cloud Shell, vous êtes en principe authentifié et le projet est défini avec votre ID de projet.

  1. Exécutez la commande suivante dans Cloud Shell pour vérifier que vous êtes authentifié :
gcloud auth list

Résultat de la commande

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
gcloud config list project

Résultat de la commande

[core]
project = <PROJECT_ID>

Si vous obtenez un résultat différent, exécutez cette commande :

gcloud config set project <PROJECT_ID>

Résultat de la commande

Updated property [core/project].

3. Provisionner des ressources Pub/Sub

Nous devons d'abord configurer un sujet et un abonnement Cloud Pub/Sub. Dans cette demande, nous allons publier les informations d'inscription dans un sujet Pub/Sub. les informations sont ensuite lues à partir de ce sujet et conservées dans une base de données.

Dans ce tutoriel, nous allons nous appuyer sur Cloud Shell pour provisionner nos ressources. Notez que vous pouvez également configurer des ressources Pub/Sub via la section Cloud Pub/Sub de la console Google Cloud.

Dans votre terminal Cloud Shell, commencez par activer l'API Pub/Sub.

$ gcloud services enable pubsub.googleapis.com

Nous allons maintenant créer un sujet Pub/Sub nommé registrations pour cette application. Les informations d'inscription envoyées via la demande seront publiées sur ce sujet.

$ gcloud pubsub topics create registrations

Enfin, créez un abonnement pour le sujet. Un abonnement Pub/Sub vous permet de recevoir des messages à partir d'un sujet.

$ gcloud pubsub subscriptions create registrations-sub --topic=registrations

Vous avez maintenant terminé la création d'un sujet et d'un abonnement Cloud Pub/Sub pour votre application.

4. Créer une instance et une base de données Cloud SQL (MySQL)

Pour notre exemple d'application, nous devons également configurer une instance de base de données pour stocker les informations du titulaire. Cette étape repose également sur le terminal Cloud Shell pour provisionner les ressources Cloud SQL. Notez que vous pouvez également afficher et configurer vos instances Cloud SQL via la console Google Cloud.

Commencez par activer l'API Cloud SQL Admin.

$ gcloud services enable sqladmin.googleapis.com

Vous allez ensuite provisionner une instance Cloud SQL (MySQL). Cette commande peut prendre un certain temps.

$ gcloud sql instances create codelab-instance --region=us-east1

Une fois votre instance Cloud SQL créée, créez une base de données dans votre instance nommée registrants.

$ gcloud sql databases create registrants --instance codelab-instance

Vous avez maintenant terminé la configuration de l'instance et de la base de données Cloud SQL pour votre application.

5. Initialiser une application Spring Boot

Nous sommes maintenant prêts à commencer à écrire l'application. Dans les étapes suivantes, vous continuerez à utiliser Cloud Shell décrit dans la procédure de configuration.

Nous allons commencer par utiliser Initializr pour générer le code d'échafaudage du projet. Dans votre fenêtre Cloud Shell, exécutez la commande suivante:

$ cd ~
$ curl https://start.spring.io/starter.tgz \
  -d language=kotlin \
  -d bootVersion=2.4.0 \
  -d dependencies=web,data-jpa,integration,cloud-gcp-pubsub,thymeleaf \
  -d baseDir=registrations-codelab | tar -xzvf -
$ cd registrations-codelab

Cette commande génère une configuration initiale de projet Maven ainsi qu'un code d'échafaudage pour votre application dans le répertoire registrations-codelab/. Les sections suivantes décrivent les modifications de code nécessaires pour produire une application fonctionnelle.

Éditeur de code Cloud Shell

Le moyen le plus simple de commencer à modifier et à afficher le code dans l'environnement Cloud Shell consiste à utiliser l'éditeur de code Cloud Shell intégré.

Une fois que vous avez ouvert une instance Cloud Shell, cliquez sur l'icône en forme de crayon pour ouvrir l'éditeur de code. L'éditeur doit vous permettre de modifier directement les fichiers du projet produits par Initialzr.

cce293b40119c37b.png

6. Configuration de la base de données

Commencez par configurer votre application afin qu'elle puisse se connecter à la base de données Cloud MySQL que vous avez configurée. Les bibliothèques Spring Cloud GCP proposent un déclencheur Cloud MySQL qui fournit les dépendances nécessaires à la connexion à une instance Cloud MySQL.

Ajoutez la dépendance spring-cloud-gcp-starter-sql-mysql au fichier pom.xml du projet:

registrations-codelab/pom.xml

...
<dependencies>

  ... Other dependencies above ...

  <!-- Add the MySQL starter to the list of dependencies -->
  <dependency>
    <groupId>com.google.cloud</groupId>
    <artifactId>spring-cloud-gcp-starter-sql-mysql</artifactId>
  </dependency>
</dependencies>

En outre, vous devez modifier le fichier de configuration application.properties pour décrire la configuration de votre base de données. Copiez les propriétés suivantes dans le fichier application.properties.

Recherchez le nom de connexion de l'instance à votre base de données:

$ gcloud sql instances describe codelab-instance \
  --format 'value(connectionName)'

Le résultat de cette commande sera utilisé dans le fichier application.properties pour configurer les informations de connexion.

src/main/resources/application.properties

# Modify this property using the output from the previous command line.
spring.cloud.gcp.sql.instance-connection-name=INSTANCE_CONNECTION_NAME

# Your database name
spring.cloud.gcp.sql.database-name=registrants

# So app starts despite "table already exists" errors.
spring.datasource.continue-on-error=true

# Enforces database initialization
spring.datasource.initialization-mode=always

# Cloud SQL (MySQL) only supports InnoDB, not MyISAM
spring.jpa.database-platform=org.hibernate.dialect.MySQL55Dialect
spring.jpa.hibernate.ddl-auto=create-drop

# This is used if you want to connect to a different database instance
# user other than root; not used in codelab.
# spring.datasource.username=root

# This is used to specify the password of the database user;
# not used in codelab.
# spring.datasource.password=password

La seule propriété que vous devez modifier est le nom de connexion de l'instance. Cette valeur doit suivre le format suivant: YOUR_GCP_PROJECT_ID:REGION:DATABASE_INSTANCE_NAME.

7. Créer le contenu statique

Commençons par créer l'interface de notre application. La demande doit comporter un formulaire permettant à quelqu'un d'enregistrer des personnes physiques, ainsi qu'une vue affichant tous les participants qui ont accepté la demande.

Pour la page d'accueil, créez un index.html contenant le formulaire d'inscription.

src/main/resources/static/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Registration Sample Application</title>
</head>
<body>

<h1>Registration</h1>

<div>
  <nav>
    <a href="/">Home</a><br>
    <a href="/registrants">Registered People</a><br>
  </nav>

  <p>
    This is a demo registration application which sends user information to a Pub/Sub topic and
    persists it into a MySQL database.
  </p>

  <h2>Register Person</h2>
  <div>
    <form action="/registerPerson" method="post">
      First Name: <input type="text" name="firstName" />
      Last Name: <input type="text" name="lastName" />
      Email: <input type="text" name="email" />
      <input type="submit" value="Submit"/>
    </form>
  </div>
</div>

</body>
</html>

Nous allons maintenant créer un modèle Thymeleaf nommé registrants.html pour afficher les utilisateurs inscrits. Thymeleaf est un framework de création de modèles que nous utilisons pour construire et diffuser du code HTML créé de façon dynamique. Le modèle se présente au format HTML, sauf qu'il comporte des éléments Markdown supplémentaires pour gérer le contenu dynamique. Ce modèle accepte un seul paramètre appelé personsList, qui contient tous les participants enregistrés via l'application.

src/main/resources/templates/registrants.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>Registrants List</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<h1>Registrants List</h1>
<p>
  This page displays all the people who were registered through the Pub/Sub topic.
  All results are retrieved from the MySQL database.
</p>
<table border="1">
  <tr>
    <th>First Name</th>
    <th>Last Name</th>
    <th>Email</th>
  </tr>
  <tr th:each="person : ${personsList}">
    <td>[[${person.firstName}]]</td>
    <td>[[${person.lastName}]]</td>
    <td>[[${person.email}]]</td>
  </tr>
</table>

</body>
</html>

À ce stade, vous pouvez vérifier que le contenu statique est diffusé.

Créez et exécutez l'application à l'aide de Maven:

$ ./mvnw spring-boot:run

Dans la fenêtre Cloud Shell, cliquez sur le bouton d'aperçu et vérifiez que la page d'accueil s'affiche. Cependant, aucune des fonctionnalités de l'interface utilisateur ne fonctionnera, car il manque un contrôleur Web. Cette information sera ajoutée à l'étape suivante.

5e38bb0d0e93002e.png

Après avoir prévisualisé l'application, appuyez sur CTRL+C pour l'arrêter.

8. Envoyer les titulaires vers un sujet Pub/Sub

Au cours de cette étape, nous allons implémenter la fonctionnalité qui permet de publier dans un sujet Cloud Pub/Sub les participants envoyés via le formulaire Web.

Ajouter les classes de données

Nous allons commencer par créer des classes de données Kotlin. il s'agira de nos entités JPA et servira également de représentation intermédiaire des Participants soumis via le formulaire.

Dans le package de démonstration, ajoutez deux fichiers: une classe Person et une PersonRepository Spring Data. Ces deux classes nous permettront de stocker et de récupérer facilement les entrées d'enregistrement de notre base de données MySQL à l'aide de Spring Data JPA.

src/main/kotlin/com/example/demo/Person.kt

package com.example.demo

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id

@Entity
data class Person(
    val firstName: String,
    val lastName: String,
    val email: String,
    @Id @GeneratedValue
    var id: Long? = 0)

src/main/kotlin/com/example/demo/PersonRepository.kt

package com.example.demo

import org.springframework.data.repository.CrudRepository

interface PersonRepository : CrudRepository<Person, Long>

Ajouter le contrôleur Web

Nous allons ensuite créer une classe Contrôleur qui traitera les titulaires du formulaire et enverra les informations au sujet Cloud Pub/Sub que vous avez créé précédemment. Ce contrôleur crée deux points de terminaison:

  • /registerPerson: point de terminaison POST dans lequel les informations du titulaire sont envoyées, puis envoyées au sujet Pub/Sub. Dans la fonction registerPerson(..), les informations du titulaire sont envoyées au sujet Pub/Sub à l'aide de PubSubTemplate, une classe pratique issue des intégrations Pub/Sub Spring Cloud GCP qui réduit le code récurrent nécessaire pour commencer à interagir avec Cloud Pub/Sub.
  • /registrants: affiche tous les titulaires correctement enregistrés dans la base de données. Ces informations sont extraites de l'instance MySQL à l'aide du dépôt Spring Data que nous avons créé à l'étape précédente.

Créez la classe Contrôleur suivante dans le package de démonstration:

src/main/kotlin/com/example/demo/Controller.kt

package com.example.demo

import com.google.cloud.spring.pubsub.core.PubSubTemplate
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.servlet.ModelAndView
import org.springframework.web.servlet.view.RedirectView

@RestController
class Controller(val pubSubTemplate: PubSubTemplate, val personRepository: PersonRepository) {
  
  // The Pub/Sub topic name created earlier.
  val REGISTRATION_TOPIC = "registrations"

  @PostMapping("/registerPerson")
  fun registerPerson(
    @RequestParam("firstName") firstName: String,
    @RequestParam("lastName") lastName: String,
    @RequestParam("email") email: String): RedirectView {

    pubSubTemplate.publish(
        REGISTRATION_TOPIC,
        Person(firstName, lastName, email))
    return RedirectView("/")
  }

  @GetMapping("/registrants")
  fun getRegistrants(): ModelAndView {
    val personsList = personRepository.findAll().toList()
    return ModelAndView("registrants", mapOf("personsList" to personsList))
  }
}

Le contrôleur lit les informations du titulaire envoyées via le formulaire Web, puis les publie dans le sujet Pub/Sub.

Ajouter le bean de l'API JSON Object Mapper

Vous avez peut-être remarqué dans le contrôleur que nous publions un objet Person dans le sujet Pub/Sub et non une chaîne. Cela est possible grâce à la compatibilité de Spring Cloud GCP avec les charges utiles JSON personnalisées à envoyer aux sujets. Les bibliothèques vous permettent de sérialiser des objets au format JSON, d'envoyer des charges utiles JSON à un sujet et de désérialiser la charge utile à sa réception.

Pour profiter de cette fonctionnalité, nous devons ajouter un bean ObjectMapper au contexte de votre application. Ce bean ObjectMapper permet de sérialiser des objets vers et depuis JSON lorsque votre application envoie et reçoit des messages. Dans la classe DemoApplication.kt, ajoutez le bean Spring JacksonPubSubMessageConverter:

src/main/kotlin/com/example/demo/DemoApplication.kt

package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

// new imports to add
import org.springframework.context.annotation.Bean
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.cloud.spring.pubsub.support.converter.JacksonPubSubMessageConverter

@SpringBootApplication
class DemoApplication {
  // This bean enables serialization/deserialization of
  // Java objects to JSON for Pub/Sub payloads
  @Bean
  fun jacksonPubSubMessageConverter(objectMapper: ObjectMapper) = 
      JacksonPubSubMessageConverter(objectMapper)
}

fun main(args: Array<String>) {
        runApplication<DemoApplication>(*args)
}

À ce stade, vous pouvez réessayer d'exécuter l'application en exécutant la commande suivante:

$ ./mvnw spring-boot:run

À partir du formulaire Web de la page principale, l'application enverra désormais les informations au sujet Pub/Sub que vous avez créé. Cependant, il ne fait encore rien d'utile, car nous avons encore besoin de lire ce sujet Pub/Sub. Nous le ferons à l'étape suivante.

9. Lire les inscrits à partir du sujet Pub/Sub

Pour la dernière étape, nous traiterons les informations du titulaire issues du sujet Pub/Sub et les conserverons dans la base de données Cloud MySQL. La demande d'inscription sera ainsi finalisée. Vous pourrez ainsi envoyer de nouveaux inscrits via le formulaire et afficher tous les utilisateurs enregistrés via le point de terminaison /registrants.

Cette application tire parti de l'intégration de Spring, qui offre de nombreuses abstractions pratiques pour gérer la messagerie. Nous allons ajouter un PubSubInboundChannelAdapter pour lire les messages du sujet Pub/Sub et les placer sur pubsubInputChannel pour un traitement plus poussé. Nous configurerons ensuite la fonction messageReceiver à l'aide de @ServiceActivator pour qu'elle soit appelée avec les messages arrivant sur pubsubInputChannel.

src/main/kotlin/com/example/demo/DemoApplication.kt

package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

import org.springframework.context.annotation.Bean
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.cloud.gcp.pubsub.support.converter.JacksonPubSubMessageConverter

// new imports to add
import com.google.cloud.spring.pubsub.core.PubSubTemplate
import com.google.cloud.spring.pubsub.integration.AckMode
import com.google.cloud.spring.pubsub.integration.inbound.PubSubInboundChannelAdapter
import com.google.cloud.spring.pubsub.support.BasicAcknowledgeablePubsubMessage
import com.google.cloud.spring.pubsub.support.GcpPubSubHeaders
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.integration.annotation.ServiceActivator
import org.springframework.integration.channel.DirectChannel
import org.springframework.messaging.MessageChannel
import org.springframework.messaging.handler.annotation.Header

@SpringBootApplication
class DemoApplication {

  private val REGISTRANT_SUBSCRIPTION = "registrations-sub"

  @Autowired
  private lateinit var personRepository: PersonRepository

  // New Spring Beans to add
  @Bean
  fun pubsubInputChannel() = DirectChannel()

  @Bean
  fun messageChannelAdapter(
      @Qualifier("pubsubInputChannel") inputChannel: MessageChannel,
      pubSubTemplate: PubSubTemplate): PubSubInboundChannelAdapter {

    val adapter = PubSubInboundChannelAdapter(
        pubSubTemplate, REGISTRANT_SUBSCRIPTION)
    adapter.outputChannel = inputChannel
    adapter.ackMode = AckMode.MANUAL
    adapter.payloadType = Person::class.java
    return adapter
  }

  @ServiceActivator(inputChannel = "pubsubInputChannel")
  fun messageReceiver(
      payload: Person,
      @Header(GcpPubSubHeaders.ORIGINAL_MESSAGE) message: BasicAcknowledgeablePubsubMessage) {
    personRepository.save(payload)
    print("Message arrived! Payload: $payload")
    message.ack()
  }

  // ObjectMapper bean from previous step
  @Bean
  fun jacksonPubSubMessageConverter(objectMapper: ObjectMapper) = JacksonPubSubMessageConverter(objectMapper)
}

fun main(args: Array<String>) {
        runApplication<DemoApplication>(*args)
}

À ce stade, vous avez terminé la configuration de l'application. Pour vérifier que l'application fonctionne correctement, exécutez la commande suivante:

$ ./mvnw spring-boot:run

Cliquez à nouveau sur le bouton Aperçu, puis essayez d'enregistrer un utilisateur en remplissant le formulaire et en l'envoyant.

e0d0b0f0c94120c2.png

Cliquez sur le lien Registered People (Personnes enregistrées) pour vérifier que le nouveau titulaire apparaît dans le tableau.

ab3b980423d0c51.png

Félicitations, vous avez terminé ! Arrêtez l'application en appuyant sur CTRL+C dans la fenêtre de terminal.

10. Nettoyage

Pour nettoyer votre environnement, vous devez supprimer le sujet Pub/Sub et l'instance Cloud MySQL que vous avez créés.

Supprimer l'instance Cloud MySQL

$ gcloud sql instances delete codelab-instance

Supprimer les ressources Pub/Sub

$ gcloud pubsub subscriptions delete registrations-sub
$ gcloud pubsub topics delete registrations

11. Félicitations !

Vous avez maintenant terminé l'écriture d'une application Kotlin Spring qui s'intègre à Cloud Pub/Sub et Cloud SQL (MySQL).

En savoir plus

Licence

Ce document est publié sous une licence Creative Commons Attribution 2.0 Generic.