Horodatages de commit

Cette rubrique explique comment écrire un horodatage de commit pour chaque opération d'insertion et de mise à jour effectuée avec Cloud Spanner. Pour utiliser cette fonctionnalité, définissez l'option allow_commit_timestamp sur une colonne TIMESTAMP, puis écrivez l'horodatage dans le cadre de chaque transaction.

Présentation

L'horodatage de commit, basé sur la technologie TrueTime, est l'heure à laquelle une transaction est reçue dans la base de données. L'option de colonne allow_commit_timestamp permet de stocker de manière atomique l'horodatage de commit dans une colonne. À l'aide des horodatages de commit stockés dans les tables, vous pouvez déterminer l'ordre exact des mutations et créer des fonctionnalités telles que les journaux des modifications.

Pour insérer des horodatages de commit dans votre base de données, procédez comme suit :

  1. Créez une colonne de type TIMESTAMP avec l'option de colonne allow_commit_timestamp définie sur true dans la définition de schéma. Exemple :

    CREATE TABLE Performances (
        ...
        LastUpdateTime  TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
        ...
    ) PRIMARY KEY (...);
    
  2. Si vous effectuez des insertions ou des mises à jour en langage LMD, utilisez la fonction PENDING_COMMIT_TIMESTAMP pour écrire l'horodatage de commit.

    Si vous effectuez des insertions ou des mises à jour avec des mutations, utilisez la chaîne d'espace réservé spanner.commit_timestamp() (ou la constante de la bibliothèque cliente) en cas d'insertion ou de mise à jour dans votre colonne d'horodatage de commit.

Lorsque Cloud Spanner reçoit la transaction en utilisant ces espaces réservés comme valeurs de colonne, l'horodatage de commit réel est écrit dans la colonne spécifiée (par exemple, la colonne LastUpdateTime). Vous pouvez ensuite utiliser cette valeur de colonne pour créer un historique des mises à jour de la table.

Le caractère unique des valeurs d'horodatage de commit n'est pas garanti. Les transactions qui écrivent dans des ensembles de champs ne se chevauchant pas peuvent avoir le même horodatage. Les transactions qui écrivent dans des ensembles de champs qui se chevauchent ont des horodatages uniques.

Les horodatages de commit Cloud Spanner sont d'une précision qui s'exprime en microsecondes et sont convertis en nanosecondes lorsqu'ils sont stockés dans des colonnes TIMESTAMP.

Créer et supprimer une colonne d'horodatage de commit

Utilisez l'option de colonne allow_commit_timestamp pour ajouter et supprimer la compatibilité avec les horodatages de commit :

  • Lors de la création d'une table, pour spécifier qu'une colonne accepte les horodatages de commit.
  • Lors de la modification d'une table existante :
    • pour ajouter une nouvelle colonne acceptant les horodatages de commit ;
    • pour modifier une colonne TIMESTAMP existante afin d'accepter les horodatages de commit ;
    • pour modifier une colonne TIMESTAMP existante afin de supprimer la compatibilité avec l'horodatage de commit.

Clés et index

Vous pouvez utiliser une colonne d'horodatage de commit en tant que colonne de clé primaire ou en tant que colonne non clé. Les clés primaires peuvent être définies comme ASC ou DESC.

  • ASC (valeur par défaut) : les clés croissantes sont idéales pour répondre aux requêtes à partir d'une heure spécifique.
  • DESC : les clés décroissantes conservent les dernières lignes en haut de la table. Elles fournissent un accès rapide aux derniers enregistrements.

L'option allow_commit_timestamp doit être cohérente entre les clés primaires des tables parents et enfants. Dans le cas contraire, Cloud Spanner renvoie une erreur. Le seul moment où l'option peut être incohérente est lorsque vous créez ou mettez à jour le schéma.

L'utilisation d'horodatages de commit dans les scénarios suivants engendre des problèmes de hotspotting et réduit les performances de traitement des données :

  • Colonne d'horodatage de commit en tant que première partie de la clé primaire d'une table :

    CREATE TABLE Users (
      LastAccess TIMESTAMP NOT NULL,
      UserId     INT64 NOT NULL,
      ...
    ) PRIMARY KEY (LastAccess, UserId);
    
  • Première partie de la clé primaire d'un index secondaire :

    CREATE INDEX UsersByLastAccess ON Users(LastAccess)
    

    ou

    CREATE INDEX UsersByLastAccessAndName ON Users(LastAccess, FirstName)
    

Les hotspots réduisent les performances de traitement des données, même avec des vitesses d'écriture faibles. L'activation de l'horodatage de commit sur des colonnes non clés non indexées n'impacte pas les performances.

Créer une colonne d'horodatage de commit

Le LDD suivant crée une table avec une colonne compatible avec les horodatages de commit.

CREATE TABLE Performances (
    SingerId        INT64 NOT NULL,
    VenueId         INT64 NOT NULL,
    EventDate       Date,
    Revenue         INT64,
    LastUpdateTime  TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (SingerId, VenueId, EventDate),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE)

L'ajout de l'option modifie la colonne d'horodatage comme suit :

  • Vous pouvez utiliser la chaîne d'espace réservé spanner.commit_timestamp() (ou une constante fournie par la bibliothèque cliente) pour les insertions et les mises à jour.
  • La colonne ne peut contenir que des valeurs antérieures au moment actuel. Pour plus d'informations, consultez la section Fournir votre propre valeur pour la colonne d'horodatage.

L'option allow_commit_timestamp est sensible à la casse.

Ajouter une colonne d'horodatage de commit à une table existante

Pour ajouter une colonne d'horodatage de commit à une table existante, utilisez l'instruction ALTER TABLE. Par exemple, pour ajouter une colonne LastUpdateTime à la table Performances, utilisez l'instruction suivante :

ALTER TABLE Performances ADD COLUMN LastUpdateTime TIMESTAMP
    NOT NULL OPTIONS (allow_commit_timestamp=true)

Convertir une colonne d'horodatage en une colonne d'horodatage de commit

Vous pouvez convertir une colonne d'horodatage existante en colonne d'horodatage de commit, mais, dans ce cas, Cloud Spanner doit vérifier que les valeurs d'horodatage existantes sont antérieures au moment actuel. Exemple :

ALTER TABLE Performances ALTER COLUMN LastUpdateTime
    SET OPTIONS (allow_commit_timestamp=true)

Vous ne pouvez pas modifier le type de données ou l'annotation NULL d'une colonne dans une instruction ALTER TABLE incluant SET OPTIONS. Pour plus d'informations, consultez la page Langage de définition de données.

Supprimer l'option d'horodatage de commit

Si vous souhaitez supprimer la compatibilité d'une colonne avec l'horodatage de commit, utilisez l'option allow_commit_timestamp=null dans une instruction ALTER TABLE. Le comportement lié à l'horodatage de commit est supprimé, mais la colonne reste toujours un horodatage. La modification de cette option n'altère pas les autres caractéristiques de la colonne, telles que le type ou l'option de valeur vide (NOT NULL). Par exemple :

ALTER TABLE Performances ALTER COLUMN LastUpdateTime
    SET OPTIONS (allow_commit_timestamp=null)

Écrire un horodatage de commit à l'aide d'une instruction LMD

Pour écrire l'horodatage de commit dans une instruction LMD, utilisez la fonction PENDING_COMMIT_TIMESTAMP. Cloud Spanner sélectionne l'horodatage de commit lorsque la transaction est enregistrée.

L'instruction LMD suivante met à jour la colonne LastUpdated de la table Singers avec l'horodatage de commit :

UPDATE Performances SET LastUpdated = PENDING_COMMIT_TIMESTAMP()
   WHERE SingerId=1 AND VenueId=2 AND EventDate="2015-10-21"

Insérer une ligne à l'aide d'une mutation

Lors de l'insertion d'une ligne, Cloud Spanner n'écrit la valeur d'horodatage de commit que si vous incluez la colonne dans la liste des colonnes et transmettez la chaîne d'espace réservé spanner.commit_timestamp() (ou constante de la bibliothèque cliente) comme valeur. Exemple :

C++

void InsertDataWithTimestamp(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto commit_result = client.Commit(spanner::Mutations{
      spanner::InsertOrUpdateMutationBuilder(
          "Performances",
          {"SingerId", "VenueId", "EventDate", "Revenue", "LastUpdateTime"})
          .EmplaceRow(1, 4, absl::CivilDay(2017, 10, 5), 11000,
                      spanner::CommitTimestamp{})
          .EmplaceRow(1, 19, absl::CivilDay(2017, 11, 2), 15000,
                      spanner::CommitTimestamp{})
          .EmplaceRow(2, 42, absl::CivilDay(2017, 12, 23), 7000,
                      spanner::CommitTimestamp{})
          .Build()});
  if (!commit_result) {
    throw std::runtime_error(commit_result.status().message());
  }
  std::cout
      << "Update was successful [spanner_insert_data_with_timestamp_column]\n";
}

C#

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
List<Performance> performances = new List<Performance> {
    new Performance {SingerId = 1, VenueId = 4, EventDate = DateTime.Parse("2017-10-05"),
        Revenue = 11000},
    new Performance {SingerId = 1, VenueId = 19, EventDate = DateTime.Parse("2017-11-02"),
        Revenue = 15000},
    new Performance {SingerId = 2, VenueId = 42, EventDate = DateTime.Parse("2017-12-23"),
        Revenue = 7000},
};
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    await connection.OpenAsync();
    // Insert rows into the Performances table.
    var cmd = connection.CreateInsertCommand("Performances",
        new SpannerParameterCollection {
            {"SingerId", SpannerDbType.Int64},
            {"VenueId", SpannerDbType.Int64},
            {"EventDate", SpannerDbType.Date},
            {"Revenue", SpannerDbType.Int64},
            {"LastUpdateTime", SpannerDbType.Timestamp},
    });
    await Task.WhenAll(performances.Select(performance =>
    {
        cmd.Parameters["SingerId"].Value = performance.SingerId;
        cmd.Parameters["VenueId"].Value = performance.VenueId;
        cmd.Parameters["EventDate"].Value = performance.EventDate;
        cmd.Parameters["Revenue"].Value = performance.Revenue;
        cmd.Parameters["LastUpdateTime"].Value = SpannerParameter.CommitTimestamp;
        return cmd.ExecuteNonQueryAsync();
    }));
    Console.WriteLine("Inserted data.");
}

Go


import (
	"context"

	"cloud.google.com/go/spanner"
)

func writeWithTimestamp(db string) error {
	ctx := context.Background()

	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	performanceColumns := []string{"SingerId", "VenueId", "EventDate", "Revenue", "LastUpdateTime"}
	m := []*spanner.Mutation{
		spanner.InsertOrUpdate("Performances", performanceColumns, []interface{}{1, 4, "2017-10-05", 11000, spanner.CommitTimestamp}),
		spanner.InsertOrUpdate("Performances", performanceColumns, []interface{}{1, 19, "2017-11-02", 15000, spanner.CommitTimestamp}),
		spanner.InsertOrUpdate("Performances", performanceColumns, []interface{}{2, 42, "2017-12-23", 7000, spanner.CommitTimestamp}),
	}
	_, err = client.Apply(ctx, m)
	return err
}

Java

static final List<Performance> PERFORMANCES =
    Arrays.asList(
        new Performance(1, 4, "2017-10-05", 11000),
        new Performance(1, 19, "2017-11-02", 15000),
        new Performance(2, 42, "2017-12-23", 7000));
static void writeExampleDataWithTimestamp(DatabaseClient dbClient) {
  List<Mutation> mutations = new ArrayList<>();
  for (Performance performance : PERFORMANCES) {
    mutations.add(
        Mutation.newInsertBuilder("Performances")
            .set("SingerId")
            .to(performance.singerId)
            .set("VenueId")
            .to(performance.venueId)
            .set("EventDate")
            .to(performance.eventDate)
            .set("Revenue")
            .to(performance.revenue)
            .set("LastUpdateTime")
            .to(Value.COMMIT_TIMESTAMP)
            .build());
  }
  dbClient.write(mutations);
}

Node.js

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

// Instantiate Spanner table objects
const performancesTable = database.table('Performances');

const data = [
  {
    SingerId: '1',
    VenueId: '4',
    EventDate: '2017-10-05',
    Revenue: '11000',
    LastUpdateTime: 'spanner.commit_timestamp()',
  },
  {
    SingerId: '1',
    VenueId: '19',
    EventDate: '2017-11-02',
    Revenue: '15000',
    LastUpdateTime: 'spanner.commit_timestamp()',
  },
  {
    SingerId: '2',
    VenueId: '42',
    EventDate: '2017-12-23',
    Revenue: '7000',
    LastUpdateTime: 'spanner.commit_timestamp()',
  },
];

// Inserts rows into the Singers table
// Note: Cloud Spanner interprets Node.js numbers as FLOAT64s, so
// they must be converted to strings before being inserted as INT64s
try {
  await performancesTable.insert(data);
  console.log('Inserted data.');
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished
  database.close();
}

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Inserts sample data into a table with a commit timestamp column.
 *
 * The database and table must already exist and can be created using
 * `create_table_with_timestamp_column`.
 * Example:
 * ```
 * insert_data_with_timestamp_column($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function insert_data_with_timestamp_column($instanceId, $databaseId)
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $operation = $database->transaction(['singleUse' => true])
        ->insertBatch('Performances', [
            ['SingerId' => 1, 'VenueId' => 4, 'EventDate' => '2017-10-05', 'Revenue' => 11000, 'LastUpdateTime' => $spanner->commitTimestamp()],
            ['SingerId' => 1, 'VenueId' => 19, 'EventDate' => '2017-11-02', 'Revenue' => 15000, 'LastUpdateTime' => $spanner->commitTimestamp()],
            ['SingerId' => 2, 'VenueId' => 42, 'EventDate' => '2017-12-23', 'Revenue' => 7000, 'LastUpdateTime' => $spanner->commitTimestamp()],
        ])
        ->commit();

    print('Inserted data.' . PHP_EOL);
}

Python

def insert_data_with_timestamp(instance_id, database_id):
    """Inserts data with a COMMIT_TIMESTAMP field into a table. """

    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)

    database = instance.database(database_id)

    with database.batch() as batch:
        batch.insert(
            table='Performances',
            columns=(
                'SingerId', 'VenueId', 'EventDate',
                'Revenue', 'LastUpdateTime',),
            values=[
                (1, 4, "2017-10-05", 11000, spanner.COMMIT_TIMESTAMP),
                (1, 19, "2017-11-02", 15000, spanner.COMMIT_TIMESTAMP),
                (2, 42, "2017-12-23", 7000, spanner.COMMIT_TIMESTAMP)])

    print('Inserted data.')

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

# Get commit_timestamp
commit_timestamp = client.commit_timestamp

client.commit do |c|
  c.insert "Performances", [
    { SingerId: 1, VenueId: 4, EventDate: "2017-10-05", Revenue: 11_000, LastUpdateTime: commit_timestamp },
    { SingerId: 1, VenueId: 19, EventDate: "2017-11-02", Revenue: 15_000, LastUpdateTime: commit_timestamp },
    { SingerId: 2, VenueId: 42, EventDate: "2017-12-23", Revenue: 7000, LastUpdateTime: commit_timestamp }
  ]
end

puts "Inserted data"

Les horodatages de commit ne peuvent être écrits que dans des colonnes annotées avec l'option allow_commit_timestamp=true.

Si des mutations sont présentes sur des lignes dans plusieurs tables, vous devez spécifier spanner.commit_timestamp() (ou une constante de la bibliothèque cliente) pour la colonne d'horodatage de commit de chaque table.

Mettre à jour une ligne à l'aide d'une mutation

Lors de la mise à jour d'une ligne, Cloud Spanner n'écrit la valeur d'horodatage de commit que si vous incluez la colonne dans la liste des colonnes et transmettez la chaîne d'espace réservé spanner.commit_timestamp() (ou constante de la bibliothèque cliente) comme valeur. Vous ne pouvez pas mettre à jour la clé primaire d'une ligne. Pour mettre à jour la clé primaire, supprimez la ligne existante et créez-en une nouvelle.

Par exemple, pour mettre à jour une colonne d'horodatage de commit nommée LastUpdateTime :

C++

void UpdateDataWithTimestamp(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto commit_result = client.Commit(spanner::Mutations{
      spanner::UpdateMutationBuilder(
          "Albums",
          {"SingerId", "AlbumId", "MarketingBudget", "LastUpdateTime"})
          .EmplaceRow(1, 1, 1000000, spanner::CommitTimestamp{})
          .EmplaceRow(2, 2, 750000, spanner::CommitTimestamp{})
          .Build()});
  if (!commit_result) {
    throw std::runtime_error(commit_result.status().message());
  }
  std::cout
      << "Update was successful [spanner_update_data_with_timestamp_column]\n";
}

C#

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    var cmd = connection.CreateUpdateCommand("Albums",
        new SpannerParameterCollection {
            {"SingerId", SpannerDbType.Int64},
            {"AlbumId", SpannerDbType.Int64},
            {"MarketingBudget", SpannerDbType.Int64},
            {"LastUpdateTime", SpannerDbType.Timestamp},
        });
    var cmdLookup =
        connection.CreateSelectCommand("SELECT * FROM Albums");
    using (var reader = await cmdLookup.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            if (reader.GetFieldValue<int>("SingerId") == 1
                && reader.GetFieldValue<int>("AlbumId") == 1)
            {
                cmd.Parameters["SingerId"].Value =
                    reader.GetFieldValue<int>("SingerId");
                cmd.Parameters["AlbumId"].Value =
                    reader.GetFieldValue<int>("AlbumId");
                cmd.Parameters["MarketingBudget"].Value = 1000000;
                cmd.Parameters["LastUpdateTime"].Value =
                    SpannerParameter.CommitTimestamp;
                await cmd.ExecuteNonQueryAsync();
            }
            if (reader.GetInt64(0) == 2 && reader.GetInt64(1) == 2)
            {
                cmd.Parameters["SingerId"].Value =
                    reader.GetFieldValue<int>("SingerId");
                cmd.Parameters["AlbumId"].Value =
                    reader.GetFieldValue<int>("AlbumId");
                cmd.Parameters["MarketingBudget"].Value = 750000;
                cmd.Parameters["LastUpdateTime"].Value =
                    SpannerParameter.CommitTimestamp;
                await cmd.ExecuteNonQueryAsync();
            }
        }
    }
}
Console.WriteLine("Updated data.");

Go


import (
	"context"
	"io"

	"cloud.google.com/go/spanner"
)

func updateWithTimestamp(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	cols := []string{"SingerId", "AlbumId", "MarketingBudget", "LastUpdateTime"}
	_, err = client.Apply(ctx, []*spanner.Mutation{
		spanner.Update("Albums", cols, []interface{}{1, 1, 1000000, spanner.CommitTimestamp}),
		spanner.Update("Albums", cols, []interface{}{2, 2, 750000, spanner.CommitTimestamp}),
	})
	return err
}

Java

static void updateWithTimestamp(DatabaseClient dbClient) {
  // Mutation can be used to update/insert/delete a single row in a table. Here we use
  // newUpdateBuilder to create update mutations.
  List<Mutation> mutations =
      Arrays.asList(
          Mutation.newUpdateBuilder("Albums")
              .set("SingerId")
              .to(1)
              .set("AlbumId")
              .to(1)
              .set("MarketingBudget")
              .to(1000000)
              .set("LastUpdateTime")
              .to(Value.COMMIT_TIMESTAMP)
              .build(),
          Mutation.newUpdateBuilder("Albums")
              .set("SingerId")
              .to(2)
              .set("AlbumId")
              .to(2)
              .set("MarketingBudget")
              .to(750000)
              .set("LastUpdateTime")
              .to(Value.COMMIT_TIMESTAMP)
              .build());
  // This writes all the mutations to Cloud Spanner atomically.
  dbClient.write(mutations);
}

Node.js

// ...

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

// Update a row in the Albums table
// Note: Cloud Spanner interprets Node.js numbers as FLOAT64s, so they
// must be converted to strings before being inserted as INT64s
const albumsTable = database.table('Albums');

const data = [
  {
    SingerId: '1',
    AlbumId: '1',
    MarketingBudget: '1000000',
    LastUpdateTime: 'spanner.commit_timestamp()',
  },
  {
    SingerId: '2',
    AlbumId: '2',
    MarketingBudget: '750000',
    LastUpdateTime: 'spanner.commit_timestamp()',
  },
];

try {
  await albumsTable.update(data);
  console.log('Updated data.');
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished
  database.close();
}

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Updates sample data in a table with a commit timestamp column.
 *
 * Before executing this method, a new column MarketingBudget has to be added to the Albums
 * table by applying the DDL statement "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64".
 *
 * In addition this update expects the LastUpdateTime column added by applying the DDL statement
 * "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP OPTIONS (allow_commit_timestamp=true)"
 *
 * Example:
 * ```
 * update_data_with_timestamp_column($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function update_data_with_timestamp_column($instanceId, $databaseId)
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $operation = $database->transaction(['singleUse' => true])
        ->updateBatch('Albums', [
            ['SingerId' => 1, 'AlbumId' => 1, 'MarketingBudget' => 1000000, 'LastUpdateTime' => $spanner->commitTimestamp()],
            ['SingerId' => 2, 'AlbumId' => 2, 'MarketingBudget' => 750000, 'LastUpdateTime' => $spanner->commitTimestamp()],
        ])
        ->commit();

    print('Updated data.' . PHP_EOL);
}

Python

def update_data_with_timestamp(instance_id, database_id):
    """Updates Performances tables in the database with the COMMIT_TIMESTAMP
    column.

    This updates the `MarketingBudget` column which must be created before
    running this sample. You can add the column by running the `add_column`
    sample or by running this DDL statement against your database:

        ALTER TABLE Albums ADD COLUMN MarketingBudget INT64

    In addition this update expects the LastUpdateTime column added by
    applying this DDL statement against your database:

        ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP
        OPTIONS(allow_commit_timestamp=true)
    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)

    database = instance.database(database_id)

    with database.batch() as batch:
        batch.update(
            table='Albums',
            columns=(
                'SingerId', 'AlbumId', 'MarketingBudget', 'LastUpdateTime'),
            values=[
                (1, 1, 1000000, spanner.COMMIT_TIMESTAMP),
                (2, 2, 750000, spanner.COMMIT_TIMESTAMP)])

    print('Updated data.')

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

commit_timestamp = client.commit_timestamp

client.commit do |c|
  c.update "Albums", [
    { SingerId: 1, AlbumId: 1, MarketingBudget: 100_000, LastUpdateTime: commit_timestamp },
    { SingerId: 2, AlbumId: 2, MarketingBudget: 750_000, LastUpdateTime: commit_timestamp }
  ]
end

puts "Updated data"

Les horodatages de commit ne peuvent être écrits que dans des colonnes annotées avec l'option allow_commit_timestamp=true.

Si des mutations sont présentes sur des lignes dans plusieurs tables, vous devez spécifier spanner.commit_timestamp() (ou la constante de la bibliothèque cliente) pour la colonne d'horodatage de commit de chaque table.

Interroger une colonne d'horodatage de commit

L'exemple suivant interroge la colonne d'horodatage de commit de la table.

C++

void QueryDataWithTimestamp(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;

  spanner::SqlStatement select(
      "SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime"
      "  FROM Albums"
      " ORDER BY LastUpdateTime DESC");
  using RowType =
      std::tuple<std::int64_t, std::int64_t, absl::optional<std::int64_t>,
                 absl::optional<spanner::Timestamp>>;

  auto rows = client.ExecuteQuery(select);
  for (auto const& row : spanner::StreamOf<RowType>(rows)) {
    if (!row) throw std::runtime_error(row.status().message());
    std::cout << std::get<0>(*row) << " " << std::get<1>(*row);
    auto marketing_budget = std::get<2>(*row);
    if (!marketing_budget) {
      std::cout << " NULL";
    } else {
      std::cout << ' ' << *marketing_budget;
    }
    auto last_update_time = std::get<3>(*row);
    if (!last_update_time) {
      std::cout << " NULL";
    } else {
      std::cout << ' ' << *last_update_time;
    }
    std::cout << "\n";
  }
}

C#

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    var cmd =
        connection.CreateSelectCommand(
            "SELECT SingerId, AlbumId, "
            + "MarketingBudget, LastUpdateTime FROM Albums");
    using (var reader = await cmd.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            string budget = string.Empty;
            if (reader["MarketingBudget"] != DBNull.Value)
            {
                budget = reader.GetFieldValue<string>("MarketingBudget");
            }
            string timestamp = string.Empty;
            if (reader["LastUpdateTime"] != DBNull.Value)
            {
                timestamp = reader.GetFieldValue<string>("LastUpdateTime");
            }
            Console.WriteLine("SingerId : "
            + reader.GetFieldValue<string>("SingerId")
            + " AlbumId : "
            + reader.GetFieldValue<string>("AlbumId")
            + $" MarketingBudget : {budget}"
            + $" LastUpdateTime : {timestamp}");
        }
    }
}

Go


import (
	"context"
	"fmt"
	"io"
	"strconv"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/iterator"
)

func queryWithTimestamp(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{
		SQL: `SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime
				FROM Albums ORDER BY LastUpdateTime DESC`}
	iter := client.Single().Query(ctx, stmt)
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			return nil
		}
		if err != nil {
			return err
		}
		var singerID, albumID int64
		var marketingBudget spanner.NullInt64
		var lastUpdateTime spanner.NullTime
		if err := row.ColumnByName("SingerId", &singerID); err != nil {
			return err
		}
		if err := row.ColumnByName("AlbumId", &albumID); err != nil {
			return err
		}
		if err := row.ColumnByName("MarketingBudget", &marketingBudget); err != nil {
			return err
		}
		budget := "NULL"
		if marketingBudget.Valid {
			budget = strconv.FormatInt(marketingBudget.Int64, 10)
		}
		if err := row.ColumnByName("LastUpdateTime", &lastUpdateTime); err != nil {
			return err
		}
		timestamp := "NULL"
		if lastUpdateTime.Valid {
			timestamp = lastUpdateTime.String()
		}
		fmt.Fprintf(w, "%d %d %s %s\n", singerID, albumID, budget, timestamp)
	}
}

Java

static void queryMarketingBudgetWithTimestamp(DatabaseClient dbClient) {
  // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to
  // null. A try-with-resource block is used to automatically release resources held by
  // ResultSet.
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .executeQuery(
              Statement.of(
                  "SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime FROM Albums"
                      + " ORDER BY LastUpdateTime DESC"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %d %s %s\n",
          resultSet.getLong("SingerId"),
          resultSet.getLong("AlbumId"),
          // We check that the value is non null. ResultSet getters can only be used to retrieve
          // non null values.
          resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"),
          resultSet.isNull("LastUpdateTime") ? "NULL" : resultSet.getTimestamp("LastUpdateTime"));
    }
  }
}

Node.js

// ...

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

const query = {
  sql: `SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime
          FROM Albums ORDER BY LastUpdateTime DESC`,
};

// Queries rows from the Albums table
try {
  const [rows] = await database.run(query);

  rows.forEach(row => {
    const json = row.toJSON();

    console.log(
      `SingerId: ${json.SingerId}, AlbumId: ${
        json.AlbumId
      }, MarketingBudget: ${
        json.MarketingBudget ? json.MarketingBudget : null
      }, LastUpdateTime: ${json.LastUpdateTime}`
    );
  });
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished
  database.close();
}

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Queries sample data from a database with a commit timestamp column.
 *
 * This sample uses the `MarketingBudget` column. You can add the column
 * by running the `add_column` sample or by running this DDL statement against
 * your database:
 *
 *      ALTER TABLE Albums ADD COLUMN MarketingBudget INT64
 *
 * This sample also uses the 'LastUpdateTime' commit timestamp column. You can
 * add the column by running the `add_timestamp_column` sample or by running
 * this DDL statement against your database:
 *
 * 		ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP OPTIONS (allow_commit_timestamp=true)
 *
 * Example:
 * ```
 * query_data_with_timestamp_column($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function query_data_with_timestamp_column($instanceId, $databaseId)
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $results = $database->execute(
        'SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime ' .
        ' FROM Albums ORDER BY LastUpdateTime DESC'
    );

    foreach ($results as $row) {
        if ($row['MarketingBudget'] == null) {
            $row['MarketingBudget'] = 'NULL';
        }
        if ($row['LastUpdateTime'] == null) {
            $row['LastUpdateTime'] = 'NULL';
        }
        printf('SingerId: %s, AlbumId: %s, MarketingBudget: %s, LastUpdateTime: %s' . PHP_EOL,
            $row['SingerId'], $row['AlbumId'], $row['MarketingBudget'], $row['LastUpdateTime']);
    }
}

Python

def query_data_with_timestamp(instance_id, database_id):
    """Queries sample data from the database using SQL.

    This updates the `LastUpdateTime` column which must be created before
    running this sample. You can add the column by running the
    `add_timestamp_column` sample or by running this DDL statement
    against your database:

        ALTER TABLE Performances ADD COLUMN LastUpdateTime TIMESTAMP
        OPTIONS (allow_commit_timestamp=true)

    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)

    database = instance.database(database_id)

    with database.snapshot() as snapshot:
        results = snapshot.execute_sql(
            'SELECT SingerId, AlbumId, MarketingBudget FROM Albums '
            'ORDER BY LastUpdateTime DESC')

    for row in results:
        print(u'SingerId: {}, AlbumId: {}, MarketingBudget: {}'.format(*row))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

client.execute("SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime
                FROM Albums ORDER BY LastUpdateTime DESC").rows.each do |row|
  puts "#{row[:SingerId]} #{row[:AlbumId]} #{row[:MarketingBudget]} #{row[:LastUpdateTime]}"
end

Fournir votre propre valeur pour la colonne d'horodatage

Vous pouvez fournir votre propre valeur pour la colonne d'horodatage de commit au lieu de transmettre spanner.commit_timestamp() (ou la constante de la bibliothèque cliente) en tant que valeur de colonne. La valeur doit être un horodatage antérieur au moment actuel. Cette restriction garantit que l'écriture d'horodatages est une opération rapide et peu coûteuse. Un moyen simple de confirmer qu'une valeur est antérieure au moment actuel consiste à la comparer à la valeur renvoyée par la fonction SQL CURRENT_TIMESTAMP. Le serveur renvoie une erreur FailedPrecondition si un horodatage futur est spécifié.

Créer un journal des modifications

Supposons que vous souhaitiez créer un journal des modifications pour chaque mutation qui se produit dans une table, puis l'utiliser pour un audit. Prenons l'exemple d'une table qui stocke l'historique des modifications apportées aux documents de traitement de texte. L'horodatage de commit facilite la création du journal des modifications, car les horodatages peuvent imposer le classement des modifications des entrées. Vous pouvez créer un journal des modifications qui stocke l'historique des modifications apportées à un document donné à l'aide d'un schéma semblable à l'exemple suivant :

CREATE TABLE Documents (
  UserId     INT64 NOT NULL,
  DocumentId INT64 NOT NULL,
  Contents   STRING(MAX) NOT NULL,
) PRIMARY KEY (UserId, DocumentId);

CREATE TABLE DocumentHistory (
  UserId     INT64 NOT NULL,
  DocumentId INT64 NOT NULL,
  Ts         TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
  Delta      STRING(MAX),
) PRIMARY KEY (UserId, DocumentId, Ts),
  INTERLEAVE IN PARENT Documents ON DELETE NO ACTION;

Pour créer un journal des modifications, insérez une nouvelle ligne dans la table DocumentHistory, dans la même transaction que celle dans laquelle vous insérez ou mettez à jour une ligne dans la table Document. Lors de l'insertion de la nouvelle ligne dans la table DocumentHistory, utilisez l'espace réservé spanner.commit_timestamp() (ou la constante de la bibliothèque cliente) pour indiquer à Cloud Spanner d'écrire l'horodatage de commit dans la colonne Ts. Si vous entrelacez la table DocumentsHistory avec la table Documents, la localité des données, les insertions et les mises à jour seront plus efficaces. Cependant, ce procédé ajoute également une contrainte : les lignes parents et enfants doivent être supprimées ensemble. Pour garder les lignes dans la table DocumentHistory après la suppression des lignes de la table Documents, n'entrelacez pas les tables.

Pour des performances optimales, la taille d'une ligne doit être inférieure à 4 Go. (Cette taille inclut la ligne racine, et toutes les lignes des tables enfants entrelacées et des index.) Dans cet exemple, la table DocumentHistory peut contenir un nombre limité de lignes pour un document particulier, en raison de la limite de taille des lignes. Si vous vous attendez à ce que le journal des modifications de la table DocumentHistory soit volumineux, vous pouvez créer votre application de manière à supprimer les lignes les plus anciennes de la table DocumentHistory. De même, vous pouvez concevoir votre schéma de sorte que la table DocumentHistory soit une table racine au lieu d'être une table entrelacée.

Étape suivante

Utilisez les horodatages de commit pour créer un journal des modifications avec Go.