Implementare applicazioni SaaS multi-tenant usando il database SQL di Azure

Un'applicazione multi-tenant è un'applicazione ospitata in un ambiente cloud che fornisce lo stesso set di servizi a centinaia o migliaia di tenant che non condividono o non visualizzano i dati degli altri. Un esempio è un'applicazione SaaS che fornisce servizi ai tenant in un ambiente ospitato nel cloud. Questo modello consente di isolare i dati per ogni tenant e ottimizza la distribuzione delle risorse di costo.

Questa esercitazione illustra come creare un'applicazione SaaS multi-tenant con database SQL di Azure.

In questa esercitazione si apprenderà come:

  • Configurare un ambiente database per supportare un'applicazione SaaS multi-tenant, usando il modello database per ogni tenant
  • Creare un catalogo di tenant
  • Eseguire il provisioning del database tenant e registrarlo nel catalogo di tenant
  • Configurare un'applicazione Java di esempio
  • Accedere ai database tenant con una semplice applicazione console Java
  • Eliminare un tenant

Se non si ha una sottoscrizione di Azure, creare un account gratuito prima di iniziare.

Prerequisiti

Per completare questa esercitazione, accertarsi di avere:

Configurare l'ambiente dati

Verrà eseguito il provisioning di un database per ogni tenant. Il modello database per ogni tenant offre il massimo livello di isolamento tra tenant, con costi DevOps ridotti. Per ottimizzare i costi delle risorse cloud, verrà anche eseguito il provisioning dei database tenant in un pool elastico che consente di ottimizzare prezzi e prestazioni per un gruppo di database. Per informazioni su altri modelli di provisioning di database vedere qui.

Seguire questi passaggi per creare un server SQL e un pool elastico che ospiterà tutti i database tenant.

  1. Creare variabili per archiviare i valori che verranno usati nel resto dell'esercitazione. Assicurarsi di modificare la variabile indirizzo IP per includere il proprio indirizzo IP

    # Set an admin login and password for your database
    $adminlogin = "ServerAdmin"
    $password = "ChangeYourAdminPassword1"
    
    # Create random unique names for logical server and tenants
    $servername = "server-$(Get-Random)"
    $tenant1 = "geolamice"
    $tenant2 = "ranplex"
    
    # Store current client IP address (modify to include your IP address)
    $startIpAddress = 0.0.0.0 
    $endIpAddress = 0.0.0.0
    
  2. Accedere ad Azure e creare un server SQL e un pool elastico

    # Login to Azure 
    Login-AzureRmAccount
    
    # Create resource group 
    New-AzureRmResourceGroup -Name "myResourceGroup" -Location "northcentralus"
    
    # Create logical SQL Server with firewall rules 
    New-AzureRmSqlServer -ResourceGroupName "myResourceGroup" `
        -ServerName $servername `
        -Location "northcentralus" `
        -SqlAdministratorCredentials $(New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $adminlogin, $(ConvertTo-SecureString -String $password -AsPlainText -Force))
    
    New-AzureRmSqlServerFirewallRule -ResourceGroupName $resourcegroupname `
        -ServerName $servername `
        -FirewallRuleName "singleAddress" -StartIpAddress $startIpAddress -EndIpAddress $endIpAddress
    
    # Create elastic pool 
    New-AzureRmSqlElasticPool -ResourceGroupName "myResourceGroup"
        -ServerName $servername `
        -ElasticPoolName "myElasticPool" `
        -Edition "Standard" `
        -Dtu 50 `
        -DatabaseDtuMin 10 `
        -DatabaseDtuMax 20
    

Creare il catalogo di tenant

In un'applicazione SaaS multi-tenant è importante sapere dove sono archiviate le informazioni per un tenant. Vengono in genere archiviate in un database del catalogo. Il database del catalogo viene usato per contenere un mapping tra un tenant e un database in cui sono archiviati i dati del tenant. Si applica il modello base se viene usato un database a singolo tenant o multi-tenant.

Seguire questi passaggi per creare un database di catalogo per l'applicazione SaaS di esempio.

# Create empty database in pool
New-AzureRmSqlDatabase  -ResourceGroupName "myResourceGroup" `
    -ServerName $servername `
    -DatabaseName "tenantCatalog" `
    -ElasticPoolName "myElasticPool"

# Create table to track mapping between tenants and their databases
$commandText = "
CREATE TABLE Tenants
(
   TenantId         INT IDENTITY PRIMARY KEY,
   TenantName       NVARCHAR(128) NOT NULL,
   TenantDatabase   NVARCHAR(128) NOT NULL
);

CREATE INDEX IX_TenantName ON Tenants (TenantName);"

Invoke-SqlCmd `
    -Username $adminlogin `
    -Password $password `
    -ServerInstance ($servername + ".database.windows.net") `
    -Database "tenantCatalog" `
    -ConnectionTimeout 30 `
    -Query $commandText `
    -EncryptConnection

Eseguire il provisioning del database per 'tenant1' e registrare nel catalogo di tenant

Usare Powershell per eseguire il provisioning di un database per un nuovo tenant 'tenant1' e registrare il tenant nel catalogo.

# Create empty database in pool for 'tenant1'
New-AzureRmSqlDatabase  -ResourceGroupName "myResourceGroup" `
    -ServerName $servername `
    -DatabaseName $tenant1 `
    -ElasticPoolName "myElasticPool"

# Create table WhoAmI and insert tenant name into the table 
$commandText = "
CREATE TABLE WhoAmI (TenantName NVARCHAR(128) NOT NULL);
INSERT INTO WhoAmI VALUES ('Tenant $tenant1');"

Invoke-SqlCmd `
    -Username $adminlogin `
    -Password $password `
    -ServerInstance ($servername + ".database.windows.net") `
    -Database $tenant1 `
    -ConnectionTimeout 30 `
    -Query $commandText `
    -EncryptConnection

# Register 'tenant1' in the tenant catalog 
$commandText = "
INSERT INTO Tenants VALUES ('$tenant1', '$tenant1');"
Invoke-SqlCmd `
    -Username $adminlogin `
    -Password $password `
    -ServerInstance ($servername + ".database.windows.net") `
    -Database "tenantCatalog" `
    -ConnectionTimeout 30 `
    -Query $commandText `
    -EncryptConnection

Eseguire il provisioning del database per 'tenant2' e registrare nel catalogo di tenant

Usare Powershell per eseguire il provisioning di un database per un nuovo tenant 'tenant2' e registrare il tenant nel catalogo.

# Create empty database in pool for 'tenant2'
New-AzureRmSqlDatabase  -ResourceGroupName "myResourceGroup" `
    -ServerName $servername `
    -DatabaseName $tenant2 `
    -ElasticPoolName "myElasticPool"

# Create table WhoAmI and insert tenant name into the table 
$commandText = "
CREATE TABLE WhoAmI (TenantName NVARCHAR(128) NOT NULL);
INSERT INTO WhoAmI VALUES ('Tenant $tenant2');"

Invoke-SqlCmd `
    -Username $adminlogin `
    -Password $password `
    -ServerInstance ($servername + ".database.windows.net") `
    -Database $tenant2 `
    -ConnectionTimeout 30 `
    -Query $commandText `
    -EncryptConnection

# Register tenant 'tenant2' in the tenant catalog 
$commandText = "
INSERT INTO Tenants VALUES ('$tenant2', '$tenant2');"
Invoke-SqlCmd `
    -Username $adminlogin `
    -Password $password `
    -ServerInstance ($servername + ".database.windows.net") `
    -Database "tenantCatalog" `
    -ConnectionTimeout 30 `
    -Query $commandText `
    -EncryptConnection

Configurare l'applicazione Java di esempio

  1. Creare un progetto Maven. Digitare quanto segue in una finestra del prompt dei comandi:

    mvn archetype:generate -DgroupId=com.microsoft.sqlserver -DartifactId=mssql-jdbc -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
    
  2. Aggiungere la dipendenza, il livello di linguaggio e compilare l'opzione per supportare i file manifesto in file JAR nel file pom.xml:

    <dependency>
          <groupId>com.microsoft.sqlserver</groupId>
          <artifactId>mssql-jdbc</artifactId>
          <version>6.1.0.jre8</version>
    </dependency>
    
    <properties>
          <maven.compiler.source>1.8</maven.compiler.source>
          <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    
    <build>
         <plugins>
            <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-jar-plugin</artifactId>
               <version>3.0.0</version>
               <configuration>
                  <archive>
                     <manifest>
                        <mainClass>com.sqldbsamples.App</mainClass>
                     </manifest>
                  </archive>
               </configuration>
            </plugin>
         </plugins>
    </build>
    
  3. Aggiungere il codice seguente nel file App.Java:

    package com.sqldbsamples;
    
    import java.util.Map;
    
    import java.util.HashMap;
    
    import java.io.BufferedReader;
    
    import java.io.InputStreamReader;
    
    import java.sql.Connection;
    
    import java.sql.Statement;
    
    import java.sql.PreparedStatement;
    
    import java.sql.ResultSet;
    
    import java.sql.DriverManager;
    
    public class App {
    
    private static final String SERVER_NAME = "your-server-name";
    
    private static final String CATALOG_DB_NAME = "tenantCatalog";
    
    private static final String USER = "ServerAdmin";
    
    private static final String PASSWORD = "ChangeYourAdminPassword1";
    
    private static final String CATALOG_DB_URL = String.format("jdbc:sqlserver://%s.database.windows.net:1433;database=%s;user=%s;password=%s;encrypt=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;", SERVER_NAME, CATALOG_DB_NAME, USER, PASSWORD);
    
    private static final String CMD_LIST = "LIST";
    
    private static final String CMD_QUERY = "QUERY";
    
    private static final String CMD_QUIT = "QUIT";
    
    public static void main(String[] args) {
    
    System.out.println("\n############################");
    
    System.out.println("## SAAS DATABASE TUTORIAL ##");
    
    System.out.println("############################\n");
    
    System.out.println("OPTIONS");
    
    System.out.println(" " + CMD_LIST + " - list tenants");
    
    System.out.println(" " + CMD_QUERY + " <NAME> - connect and tenant query tenant <NAME>");
    
    System.out.println(" " + CMD_QUIT + " - quit the application\n");
    
    try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
    
    while(true) {
    
    String[] input = br.readLine().split("\\s");
    
    if (null != input && input.length > 0) {
    
    if (input[0].equalsIgnoreCase(CMD_LIST)) {
    
    listTenants();
    
    } else if (input[0].toLowerCase().startsWith(CMD_QUERY.toLowerCase()) && input.length == 2) {
    
    queryTenant(input[1].trim());
    
    } else if (input[0].equalsIgnoreCase(CMD_QUIT)) {
    
    break;
    
    } else {
    
    System.out.println(" -> Command not supported");
    
    }
    
    }
    
    System.out.println("");
    
    }
    
    } catch (Exception e) {
    
    System.out.println(e.getMessage());
    
    e.printStackTrace();
    
    }
    
    }
    
    private static void listTenants() {
    
    // List all tenants that currently exist in the system
    
    String sql = "SELECT TenantName FROM Tenants";
    
    try (Connection connection = DriverManager.getConnection(CATALOG_DB_URL);
    
    Statement stmt = connection.createStatement();
    
    ResultSet resultSet = stmt.executeQuery(sql)) {
    
    while (resultSet.next()) {
    
    System.out.println(" -> " + resultSet.getString(1));
    
    }
    
    } catch (Exception e) {
    
    System.out.println(e.getMessage());
    
    e.printStackTrace();
    
    }
    
    }
    
    private static void queryTenant(String name) {
    
    // Query the data that was previously inserted into the primary database from the geo replicated database
    
    String url = null;
    
    String sql = "SELECT TenantDatabase FROM Tenants WHERE TenantName = ?";
    
    try (Connection connection = DriverManager.getConnection(CATALOG_DB_URL);
    
    PreparedStatement pstmt = connection.prepareStatement(sql)) {
    
    pstmt.setString(1, name);
    
    try (ResultSet resultSet = pstmt.executeQuery()) {
    
    if (resultSet.next()) {
    
    url = String.format("jdbc:sqlserver://%s.database.windows.net:1433;database=%s;user=%s;password=%s;encrypt=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;", SERVER_NAME, resultSet.getString(1), USER, PASSWORD);
    
    }
    
    }
    
    } catch (Exception e) {
    
    System.out.println(e.getMessage());
    
    e.printStackTrace();
    
    }
    
    if (null != url) {
    
    String tenantSql = "SELECT TenantName FROM WhoAmI";
    
    try (Connection connection = DriverManager.getConnection(url);
    
    Statement stmt = connection.createStatement();
    
    ResultSet resultSet = stmt.executeQuery(tenantSql)) {
    
    while (resultSet.next()) {
    
    System.out.println(" -> Entry in table WhoAmI in tenant " + name + " is: " + resultSet.getString(1));
    
    }
    
    } catch (Exception e) {
    
    System.out.println(e.getMessage());
    
    e.printStackTrace();
    
    }
    
    } else {
    
    System.out.println(" -> Tenant " + name + " not found");
    
    }
    
    }
    
    }
    
  4. Salvare il file.

  5. Passare alla console dei comandi ed eseguire

    mvn package
    
  6. Al termine, eseguire il comando seguente per eseguire l'applicazione

    mvn -q -e exec:java "-Dexec.mainClass=com.sqldbsamples.App"
    

L'output sarà simile al seguente se viene eseguito correttamente:

############################

## SAAS DATABASE TUTORIAL ##

############################

OPTIONS

LIST - list tenants

QUERY <NAME> - connect and tenant query tenant <NAME>

QUIT - quit the application

* List the tenants

* Query tenants you created

Eliminare il primo tenant

Usare PowerShell per eliminare il database tenant e la voce di catalogo per il primo tenant.

# Remove 'tenant1' from catalog 
$commandText = "DELETE FROM Tenants WHERE TenantName = '$tenant1';"
Invoke-SqlCmd `
    -Username $adminlogin `
    -Password $password `
    -ServerInstance ($servername + ".database.windows.net") `
    -Database "tenantCatalog" `
    -ConnectionTimeout 30 `
    -Query $commandText `
    -EncryptConnection

# Delete database 
Remove-AzureRmSqlDatabase -ResourceGroupName "myResourceGroup" `
    -ServerName $servername `
    -DatabaseName $tenant1

Provare a connettersi a 'tenant1' usando l'applicazione Java. Si otterrà un errore che informa che il tenant non esiste.

Passaggi successivi

Questa esercitazione illustra come:

  • Configurare un ambiente database per supportare un'applicazione SaaS multi-tenant, usando il modello database per ogni tenant
  • Creare un catalogo di tenant
  • Eseguire il provisioning del database tenant e registrarlo nel catalogo di tenant
  • Configurare un'applicazione Java di esempio
  • Accedere ai database tenant con una semplice applicazione console Java
  • Eliminare un tenant