תחילת העבודה עם Spanner ב-Java

מטרות

במדריך הזה נסביר איך לבצע את הפעולות הבאות באמצעות ספריית הלקוח של Spanner ב-Java:

  • יוצרים מכונה ומסד נתונים ב-Spanner.
  • לכתוב, לקרוא ולהריץ שאילתות SQL על נתונים במסד הנתונים.
  • מעדכנים את הסכימה של מסד הנתונים.
  • עדכון נתונים באמצעות טרנזקציית קריאה-כתיבה.
  • מוסיפים אינדקס משני למסד הנתונים.
  • משתמשים באינדקס כדי לקרוא ולהריץ שאילתות SQL על נתונים.
  • אחזור נתונים באמצעות טרנזקציה לקריאה בלבד.

עלויות

במדריך הזה משתמשים ב-Spanner, שהוא רכיב בתשלום שלGoogle Cloud. למידע על עלות השימוש ב-Spanner, ראו תמחור.

לפני שמתחילים

מבצעים את השלבים שמפורטים בקטע הגדרה, שכוללים יצירה והגדרה של פרויקט Google Cloud כברירת מחדל, ��פעלת החיוב, הפעלת Cloud Spanner API והגדרת OAuth 2.0 כדי לקבל פרטי כניסה לאימות לשימוש ב-Cloud Spanner API.

חשוב במיוחד להריץ את הפקודה gcloud auth application-default login כדי להגדיר את סביבת הפיתוח המקומית עם פרטי הכניסה לאימות.

הכנת סביבת Java המקומית

כדי להתחיל להשתמש ב-Spanner באפליקציה שלכם, הוסיפו את ספריית הלקוח לתלויות של הפרויקט:

אם משתמשים ב-Maven, צריך להוסיף את הקוד הבא לקובץ pom.xml. במאמר העוסק בספריות BOM ל-Google Cloud Platform תוכלו לקרוא מידע נוסף על עצי מוצרים (BOM).

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>libraries-bom</artifactId>
      <version>26.90.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>com.google.cloud</groupId>
    <artifactId>google-cloud-spanner</artifactId>
  </dependency>
</dependencies>

אם משתמשים ב-Gradle, צריך להוסיף את הקוד הבא ליחסי התלות:

implementation platform('com.google.cloud:libraries-bom:26.90.0')

implementation 'com.google.cloud:google-cloud-spanner'

אם משתמשים ב-sbt, צריך להוסיף את הקוד הבא ליחסי התלות:

libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.125.0"

אם משתמשים ב-Visual Studio Code או ב-IntelliJ, אפשר להוסיף את ספריות הלקוח לפרויקט באמצעות יישומי הפלאגין הבאים של IDE:

באמצעות יישומי הפלאגין תוכלו להשתמש בפונקציות נוספות, כמו ניהול מפתחות לחשבונות שירות. לפרטים נוספים, קראו את מאמרי העזרה של כל אחד מיישומי הפלאגין.

לחלופין, אם אתם רוצים להריץ את האפליקציה לדוגמה שמשמשת במדריך הזה, בצעו את השלבים הבאים כדי להכין את הסביבה המקומית:

  1. אם הכלים הבאים לא מותקנים במחשב הפיתוח, צריך להתקין אותם:

  2. משכפלים את המאגר של האפליקציה לדוגמה ומעבירים אותו למכונה המקומית:

    git clone https://github.com/googleapis/google-cloud-java.git
    
  3. עוברים לספרייה שמכילה את הקוד לדוגמה של Spanner:

    cd google-cloud-java/java-spanner/samples/snippets
    
  4. יוצרים את קובץ ה-JAR לדוגמה:

    mvn clean package
    

יצירת מכונה

בפעם הראשונה שמשתמשים ב-Spanner, צריך ליצור מכונה – הקצאה של משאבים שמשמשים את מסדי ה��תונים של Spanner. כשיוצרים מכונה, בוחרים מערך הגדרות של מכונה שקובע איפה הנתונים יאוחסנו, וגם את מספר הצמתים שבהם משתמשים, שקובע את כמות משאבי האחסון וההצגה במכונה.

במאמר יצירת מכונה מוסבר איך ליצור מכונה של Spanner באמצעות אחת מהשיטות הבאות. אתם יכולים לתת למכונה את השם test-instance כדי להשתמש בה עם נושאים אחרים במסמך הזה, שמתייחסים למכונה בשם test-instance.

  • ‫Google Cloud CLI
  • מסוף Google Cloud
  • ספריית לקוח (C++‎,‏ C#‎,‏ Go,‏ Java,‏ Node.js,‏ PHP,‏ Python או Ruby)

עיון בקבצים לדוגמה

מאגר הדוגמאות מכיל דוגמה שמראה איך להשתמש ב-Spanner עם Java.

יצירת מסד נתונים

GoogleSQL

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
createdatabase test-instance example-db

PostgreSQL

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
createpgdatabase test-instance example-db

אמור להופיע:

Created database [example-db]
הקוד הבא יוצר מסד נתונים ושתי טבלאות במסד הנתונים.

GoogleSQL

static void createDatabase(DatabaseAdminClient dbAdminClient,
    InstanceName instanceName, String databaseId) {
  CreateDatabaseRequest createDatabaseRequest =
      CreateDatabaseRequest.newBuilder()
          .setCreateStatement("CREATE DATABASE `" + databaseId + "`")
          .setParent(instanceName.toString())
          .addAllExtraStatements(Arrays.asList(
              "CREATE TABLE Singers ("
                  + "  SingerId   INT64 NOT NULL,"
                  + "  FirstName  STRING(1024),"
                  + "  LastName   STRING(1024),"
                  + "  SingerInfo BYTES(MAX),"
                  + "  FullName STRING(2048) AS "
                  + "  (ARRAY_TO_STRING([FirstName, LastName], \" \")) STORED"
                  + ") PRIMARY KEY (SingerId)",
              "CREATE TABLE Albums ("
                  + "  SingerId     INT64 NOT NULL,"
                  + "  AlbumId      INT64 NOT NULL,"
                  + "  AlbumTitle   STRING(MAX)"
                  + ") PRIMARY KEY (SingerId, AlbumId),"
                  + "  INTERLEAVE IN PARENT Singers ON DELETE CASCADE")).build();
  try {
    // Initiate the request which returns an OperationFuture.
    com.google.spanner.admin.database.v1.Database db =
        dbAdminClient.createDatabaseAsync(createDatabaseRequest).get();
    System.out.println("Created database [" + db.getName() + "]");
  } catch (ExecutionException e) {
    // If the operation failed during execution, expose the cause.
    throw (SpannerException) e.getCause();
  } catch (InterruptedException e) {
    // Throw when a thread is waiting, sleeping, or otherwise occupied,
    // and the thread is interrupted, either before or during the activity.
    throw SpannerExceptionFactory.propagateInterrupt(e);
  }
}

PostgreSQL

static void createPostgreSqlDatabase(
    DatabaseAdminClient dbAdminClient, String projectId, String instanceId, String databaseId) {
  final CreateDatabaseRequest request =
      CreateDatabaseRequest.newBuilder()
          .setCreateStatement("CREATE DATABASE \"" + databaseId + "\"")
          .setParent(InstanceName.of(projectId, instanceId).toString())
          .setDatabaseDialect(DatabaseDialect.POSTGRESQL).build();

  try {
    // Initiate the request which returns an OperationFuture.
    Database db = dbAdminClient.createDatabaseAsync(request).get();
    System.out.println("Created database [" + db.getName() + "]");
  } catch (ExecutionException e) {
    // If the operation failed during execution, expose the cause.
    throw (SpannerException) e.getCause();
  } catch (InterruptedException e) {
    // Throw when a thread is waiting, sleeping, or otherwise occupied,
    // and the thread is interrupted, either before or during the activity.
    throw SpannerExceptionFactory.propagateInterrupt(e);
  }
}
static void createTableUsingDdl(DatabaseAdminClient dbAdminClient, DatabaseName databaseName) {
  try {
    // Initiate the request which returns an OperationFuture.
    dbAdminClient.updateDatabaseDdlAsync(
        databaseName,
        Arrays.asList(
            "CREATE TABLE Singers ("
                + "  SingerId   bigint NOT NULL,"
                + "  FirstName  character varying(1024),"
                + "  LastName   character varying(1024),"
                + "  SingerInfo bytea,"
                + "  FullName character varying(2048) GENERATED "
                + "  ALWAYS AS (FirstName || ' ' || LastName) STORED,"
                + "  PRIMARY KEY (SingerId)"
                + ")",
            "CREATE TABLE Albums ("
                + "  SingerId     bigint NOT NULL,"
                + "  AlbumId      bigint NOT NULL,"
                + "  AlbumTitle   character varying(1024),"
                + "  PRIMARY KEY (SingerId, AlbumId)"
                + ") INTERLEAVE IN PARENT Singers ON DELETE CASCADE")).get();
    System.out.println("Created Singers & Albums tables in database: [" + databaseName + "]");
  } catch (ExecutionException e) {
    // If the operation failed during execution, expose the cause.
    throw SpannerExceptionFactory.asSpannerException(e);
  } catch (InterruptedException e) {
    // Throw when a thread is waiting, sleeping, or otherwise occupied,
    // and the thread is interrupted, either before or during the activity.
    throw SpannerExceptionFactory.propagateInterrupt(e);
  }
}

השלב הבא הוא כתיבת נתונים במסד הנתונים.

יצירת לקוח של מסד נתונים

כדי לבצע פעולות קריאה או כתיבה, צריך ליצור DatabaseClient. אפשר לחשוב על DatabaseClient כחיבור למסד נתונים: כל האינטראקציות עם Spanner חייבות לעבור דרך DatabaseClient. בדרך כלל יוצרים DatabaseClient כשהאפליקציה מופעלת, ואז משתמשים שוב באותו DatabaseClient כדי לקרוא, לכתוב ולבצע טרנזקציות.

SpannerOptions options = SpannerOptions.newBuilder().build();
Spanner spanner = options.getService();
DatabaseAdminClient dbAdminClient = null;
try {
  DatabaseClient dbClient = spanner.getDatabaseClient(db);
  dbAdminClient = spanner.createDatabaseAdminClient();
} finally {
  if (dbAdminClient != null) {
    if (!dbAdminClient.isShutdown() || !dbAdminClient.isTerminated()) {
      dbAdminClient.close();
    }
  }
  spanner.close();
}

כל לקוח משתמש במשאבים ב-Spanner, ולכן מומלץ לסגור לקוחות לא נחוצים באמצעות קריאה ל-close().

מידע נוסף זמין במאמרי העזרה של Javadoc ל-DatabaseClient.

כתיבת נתונים באמצעות DML

אפשר להוסיף נתונים באמצעות שפת טיפול בנתונים (DML) בטרנזקציית קריאה-כתיבה.

משתמשים ב-method‏ executeUpdate() כדי להריץ פקודת DML.

static void writeUsingDml(DatabaseClient dbClient) {
  // Insert 4 singer records
  dbClient
      .readWriteTransaction()
      .run(transaction -> {
        String sql =
            "INSERT INTO Singers (SingerId, FirstName, LastName) VALUES "
                + "(12, 'Melissa', 'Garcia'), "
                + "(13, 'Russell', 'Morales'), "
                + "(14, 'Jacqueline', 'Long'), "
                + "(15, 'Dylan', 'Shaw')";
        long rowCount = transaction.executeUpdate(Statement.of(sql));
        System.out.printf("%d records inserted.\n", rowCount);
        return null;
      });
}

מריצים את הדוגמה באמצעות הארגומנט writeusingdml.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    writeusingdml test-instance example-db

אמור להופיע:

4 records inserted.

כתיבת נתונים באמצעות מוטציות

אפשר גם להוסיף נתונים באמצעות מוטציות.

אפשר לכתוב נתונים באמצעות אובייקט Mutation. אובייקט Mutation הוא קונטיינר לפעולות מוטציה. Mutation מייצגת רצף של הוספות, עדכונים ומחיקות ש-Spanner מחיל באופן אטומי על שורות וטבלאות שונות במסד נתונים של Spanner.

ה-method‏ newInsertBuilder() במחלקה Mutation יוצרת מוטציה של INSERT, שמוסיפה שורה חדשה בטבלה. אם השורה כב�� ��י��מ��, ��כתיב�� ��כשלת. לחלופין, אפשר להשתמש ב-method‏ newInsertOrUpdateBuilder כדי ליצור מוטציה של INSERT_OR_UPDATE, שמעדכנת את ערכי העמודות אם השורה כבר קיימת.

ה-method‏ write() בכיתה DatabaseClient כותבת את המוטציות. כל השינויים באצווה אחת מוחלים באופן אטומי.

הקוד הזה מראה איך לכתוב את הנתונים באמצעות מוטציות:

static final List<Singer> SINGERS =
    Arrays.asList(
        new Singer(1, "Marc", "Richards"),
        new Singer(2, "Catalina", "Smith"),
        new Singer(3, "Alice", "Trentor"),
        new Singer(4, "Lea", "Martin"),
        new Singer(5, "David", "Lomond"));

static final List<Album> ALBUMS =
    Arrays.asList(
        new Album(1, 1, "Total Junk"),
        new Album(1, 2, "Go, Go, Go"),
        new Album(2, 1, "Green"),
        new Album(2, 2, "Forever Hold Your Peace"),
        new Album(2, 3, "Terrified"));
static void writeExampleData(DatabaseClient dbClient) {
  List<Mutation> mutations = new ArrayList<>();
  for (Singer singer : SINGERS) {
    mutations.add(
        Mutation.newInsertBuilder("Singers")
            .set("SingerId")
            .to(singer.singerId)
            .set("FirstName")
            .to(singer.firstName)
            .set("LastName")
            .to(singer.lastName)
            .build());
  }
  for (Album album : ALBUMS) {
    mutations.add(
        Mutation.newInsertBuilder("Albums")
            .set("SingerId")
            .to(album.singerId)
            .set("AlbumId")
            .to(album.albumId)
            .set("AlbumTitle")
            .to(album.albumTitle)
            .build());
  }
  dbClient.write(mutations);
}

��ריצים את הדוגמה באמצעות הארגומנט write.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    write test-instance example-db

הפקודה אמורה לפעול בהצלחה.

הרצת שאילתות על נתונים באמצעות SQL

‫Spanner תומך בממשק SQL לקריאת נתונים, שאפשר לגשת אליו בשורת הפקודה באמצעות Google Cloud CLI או באופן פרוגרמטי באמצעות ספריית הלקוח של Spanner ל-Java.

בשורת הפקודה

מריצים את הצהרת ה-SQL הבאה כדי לקרוא את הערכים של כל העמודות מהטבלה Albums:

GoogleSQL

gcloud spanner databases execute-sql example-db --instance=test-instance \
    --sql='SELECT SingerId, AlbumId, AlbumTitle FROM Albums'

תחביר Pipe ב-GoogleSQL

gcloud spanner databases execute-sql example-db --instance=test-instance \
    --sql='FROM Albums |> SELECT SingerId, AlbumId, AlbumTitle'

בתוצאה רואים:

SingerId AlbumId AlbumTitle
1        1       Total Junk
1        2       Go, Go, Go
2        1       Green
2        2       Forever Hold Your Peace
2        3       Terrified

שימוש בספריית הלקוח של Spanner ל-Java

בנוסף להרצת הצהרת SQL בשורת הפקודה, אפשר להנפיק את אותה הצהרת SQL באופן פרוגרמטי באמצעות ספריית הלקוח של Spanner ל-Java.

ה-methods והמחלקות הבאות משמשות להרצת שאילתת ה-SQL:

  • ה-method‏ singleUse() במחלקה DatabaseClient: משמשת לקריאת הערך של עמודה אחת או יותר משורה אחת או יותר בטבלת Spanner. ‫singleUse() מחזיר אובייקט ReadContext, שמשמש להרצת הצהרת קריאה או הצהרת SQL.
  • ה-method‏ executeQuery() של המחלקה ReadContext: משתמשים ב-method הזו כדי להריץ שאילתה במסד נתונים.
  • הכיתה Statement: משמשת ליצירת מחרוזת SQL.
  • הכיתה ResultSet: משמשת כדי לגשת לנתונים שמוחזרים על ידי הצהרת SQL או קריאה.

כך מריצים את השאילתה וניגשים לנתונים:

static void query(DatabaseClient dbClient) {
  try (ResultSet resultSet =
      dbClient
          .singleUse() // Execute a single read or query against Cloud Spanner.
          .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2));
    }
  }
}

מריצים את הדוגמה באמצעות הארגומנט query.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    query test-instance example-db

התוצאה הבאה אמורה להופיע:

1 1 Total Junk
1 2 Go, Go, Go
2 1 Green
2 2 Forever Hold Your Peace
2 3 Terrified

שאילתה באמצעות פרמטר SQL

אם באפליקציה שלכם יש שאילתה שמופעלת לעיתים קרובות, אתם יכולים לשפר את הביצועים שלה באמצעות פרמטריזציה. אפשר לשמור במטמון את השאילתה הפרמטרית שנוצרת ולהשתמש בה שוב, וכך להפחית את עלויות ההידור. מידע נוסף זמין במאמר שימוש בפרמטרים של שאילתות כדי להאיץ שאילתות שמריצים לעיתים קרובות.

הנה דוגמה לשימוש בפרמטר בסעיף WHERE כדי לשלוח שאילתה על רשומות שמכילות ערך ספציפי של LastName.

GoogleSQL

static void queryWithParameter(DatabaseClient dbClient) {
  Statement statement =
      Statement.newBuilder(
              "SELECT SingerId, FirstName, LastName "
                  + "FROM Singers "
                  + "WHERE LastName = @lastName")
          .bind("lastName")
          .to("Garcia")
          .build();
  try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %s %s\n",
          resultSet.getLong("SingerId"),
          resultSet.getString("FirstName"),
          resultSet.getString("LastName"));
    }
  }
}

PostgreSQL

static void queryWithParameter(DatabaseClient dbClient) {
  Statement statement =
      Statement.newBuilder(
              "SELECT singerid AS \"SingerId\", "
                  + "firstname as \"FirstName\", lastname as \"LastName\" "
                  + "FROM Singers "
                  + "WHERE LastName = $1")
          .bind("p1")
          .to("Garcia")
          .build();
  try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %s %s\n",
          resultSet.getLong("SingerId"),
          resultSet.getString("FirstName"),
          resultSet.getString("LastName"));
    }
  }
}

מריצים את הדוגמה באמצעות הארגומנט queryWithParameter.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    querywithparameter test-instance example-db

התוצאה הבאה אמורה להופיע:

12 Melissa Garcia

קריאת נתונים באמצעות Read API

בנוסף לממשק ה-SQL של Spanner, יש גם ממשק קריאה.

כדי לקרוא שורות ממסד הנתונים, משתמשים ב-method‏ read() של המחלקה ReadContext. משתמשים באובייקט KeySet כדי להגדיר אוסף של מפתחות וטווחי מפתחות לקריאה.

כך קוראים את הנתונים:

static void read(DatabaseClient dbClient) {
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .read(
              "Albums",
              KeySet.all(), // Read all rows in a table.
              Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %d %s\n", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2));
    }
  }
}

מריצים את הדוגמה באמצעות הארגומנט read.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    read test-instance example-db

הפלט אמור להיראות כך:

1 1 Total Junk
1 2 Go, Go, Go
2 1 Green
2 2 Forever Hold Your Peace
2 3 Terrified

עדכון הסכימה של מסד הנתונים

נניח שאתם צריכים להוסיף עמודה חדשה בשם MarketingBudget לטבלה Albums. כדי להוסיף עמודה חדשה לטבלה קיימת, צריך לעדכן את הסכימה של מסד הנתונים. ב-Spanner אפשר לעדכן את הסכימה של מסד נתונים בזמן שמסד הנתונים ממשיך לשרת את התנועה. עדכוני סכימה לא דורשים להעביר את מסד הנתונים למצב אופליין, והם לא נועלים טבלאות או עמודות שלמות. אפשר להמשיך לכתוב נתונים במסד הנתונים במהלך עדכון הסכימה. במאמר עדכוני סכימה מוסבר איך לעדכן סכימות ואיך שינויים בסכימה משפיעים על הביצועים.

הוספת עמודה

אפשר להוסיף עמודה בשורת הפקודה באמצעות Google Cloud CLI או באופן פרוגרמטי באמצעות ספריית הלקוח של Spanner ל-Java.

בשורת הפקודה

משתמשים בפקודה הבאה של ALTER TABLE כדי להוסיף את העמודה החדשה לטבלה:

GoogleSQL

gcloud spanner databases ddl update example-db --instance=test-instance \
    --ddl='ALTER TABLE Albums ADD COLUMN MarketingBudget INT64'

PostgreSQL

gcloud spanner databases ddl update example-db --instance=test-instance \
    --ddl='ALTER TABLE Albums ADD COLUMN MarketingBudget BIGINT'

אמור להופיע:

Schema updating...done.

שימוש בספריית הלקוח של Spanner ל-Java

כדי לשנות את הסכימה, משתמשים ב-method‏ updateDatabaseDdl() של המחלקה DatabaseAdminClient:

GoogleSQL

static void addMarketingBudget(DatabaseAdminClient adminClient, DatabaseName databaseName) {
  try {
    // Initiate the request which returns an OperationFuture.
    adminClient.updateDatabaseDdlAsync(
        databaseName,
        Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget INT64")).get();
    System.out.println("Added MarketingBudget column");
  } catch (ExecutionException e) {
    // If the operation failed during execution, expose the cause.
    throw (SpannerException) e.getCause();
  } catch (InterruptedException e) {
    // Throw when a thread is waiting, sleeping, or otherwise occupied,
    // and the thread is interrupted, either before or during the activity.
    throw SpannerExceptionFactory.propagateInterrupt(e);
  }
}

PostgreSQL

static void addMarketingBudget(DatabaseAdminClient adminClient, DatabaseName databaseName) {
  try {
    // Initiate the request which returns an OperationFuture.
    adminClient.updateDatabaseDdlAsync(
        databaseName,
        Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget bigint")).get();
    System.out.println("Added MarketingBudget column");
  } catch (ExecutionException e) {
    // If the operation failed during execution, expose the cause.
    throw (SpannerException) e.getCause();
  } catch (InterruptedException e) {
    // Throw when a thread is waiting, sleeping, or otherwise occupied,
    // and the thread is interrupted, either before or during the activity.
    throw SpannerExceptionFactory.propagateInterrupt(e);
  }
}

מריצים את הדוגמה באמצעות הארגומנט addmarketingbudget.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    addmarketingbudget test-instance example-db

אמור להופיע:

Added MarketingBudget column.

כתיבת נתונים בעמודה החדשה

הקוד הבא כותב נתונים בעמודה החדשה. היא מגדירה את MarketingBudget לערך 100000 בשורה עם המפתח Albums(1, 1), ולערך 500000 בשורה עם המפתח Albums(2, 2).

static void update(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(100000)
              .build(),
          Mutation.newUpdateBuilder("Albums")
              .set("SingerId")
              .to(2)
              .set("AlbumId")
              .to(2)
              .set("MarketingBudget")
              .to(500000)
              .build());
  // This writes all the mutations to Cloud Spanner atomically.
  dbClient.write(mutations);
}

מריצים את הדוגמה באמצעות הארגומנט update.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    update test-instance example-db

אפשר גם להריץ שאילתת SQL או קריאת read כדי לאחזר את הערכים שכתבתם.

זה הקוד להרצת השאילתה:

GoogleSQL

static void queryMarketingBudget(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 FROM Albums"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %d %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"));
    }
  }
}

PostgreSQL

static void queryMarketingBudget(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 as \"SingerId\", "
              + "albumid as \"AlbumId\", marketingbudget as \"MarketingBudget\" "
              + "FROM Albums"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %d %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"));
    }
  }
}

כדי להריץ את השאילתה הזו, מריצים את הדוגמה באמצעות הארגומנט querymarketingbudget.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    querymarketingbudget test-instance example-db

אמור להופיע:

1 1 100000
1 2 NULL
2 1 NULL
2 2 500000
2 3 NULL

עדכון נתונים

אפשר לעדכן נתונים באמצעות DML בעסקה של קריאה וכתיבה.

משתמשים ב-method‏ executeUpdate() כדי להריץ פקודת DML.

GoogleSQL

static void writeWithTransactionUsingDml(DatabaseClient dbClient) {
  dbClient
      .readWriteTransaction()
      .run(transaction -> {
        // Transfer marketing budget from one album to another. We do it in a transaction to
        // ensure that the transfer is atomic.
        String sql1 =
            "SELECT MarketingBudget from Albums WHERE SingerId = 2 and AlbumId = 2";
        ResultSet resultSet = transaction.executeQuery(Statement.of(sql1));
        long album2Budget = 0;
        while (resultSet.next()) {
          album2Budget = resultSet.getLong("MarketingBudget");
        }
        // Transaction will only be committed if this condition still holds at the time of
        // commit. Otherwise it will be aborted and the callable will be rerun by the
        // client library.
        long transfer = 200000;
        if (album2Budget >= transfer) {
          String sql2 =
              "SELECT MarketingBudget from Albums WHERE SingerId = 1 and AlbumId = 1";
          ResultSet resultSet2 = transaction.executeQuery(Statement.of(sql2));
          long album1Budget = 0;
          while (resultSet2.next()) {
            album1Budget = resultSet2.getLong("MarketingBudget");
          }
          album1Budget += transfer;
          album2Budget -= transfer;
          Statement updateStatement =
              Statement.newBuilder(
                      "UPDATE Albums "
                          + "SET MarketingBudget = @AlbumBudget "
                          + "WHERE SingerId = 1 and AlbumId = 1")
                  .bind("AlbumBudget")
                  .to(album1Budget)
                  .build();
          transaction.executeUpdate(updateStatement);
          Statement updateStatement2 =
              Statement.newBuilder(
                      "UPDATE Albums "
                          + "SET MarketingBudget = @AlbumBudget "
                          + "WHERE SingerId = 2 and AlbumId = 2")
                  .bind("AlbumBudget")
                  .to(album2Budget)
                  .build();
          transaction.executeUpdate(updateStatement2);
        }
        return null;
      });
}

PostgreSQL

static void writeWithTransactionUsingDml(DatabaseClient dbClient) {
  dbClient
      .readWriteTransaction()
      .run(transaction -> {
        // Transfer marketing budget from one album to another. We do it in a transaction to
        // ensure that the transfer is atomic.
        String sql1 =
            "SELECT marketingbudget as \"MarketingBudget\" from Albums WHERE "
                + "SingerId = 2 and AlbumId = 2";
        ResultSet resultSet = transaction.executeQuery(Statement.of(sql1));
        long album2Budget = 0;
        while (resultSet.next()) {
          album2Budget = resultSet.getLong("MarketingBudget");
        }
        // Transaction will only be committed if this condition still holds at the time of
        // commit. Otherwise it will be aborted and the callable will be rerun by the
        // client library.
        long transfer = 200000;
        if (album2Budget >= transfer) {
          String sql2 =
              "SELECT marketingbudget as \"MarketingBudget\" from Albums WHERE "
                  + "SingerId = 1 and AlbumId = 1";
          ResultSet resultSet2 = transaction.executeQuery(Statement.of(sql2));
          long album1Budget = 0;
          while (resultSet2.next()) {
            album1Budget = resultSet2.getLong("MarketingBudget");
          }
          album1Budget += transfer;
          album2Budget -= transfer;
          Statement updateStatement =
              Statement.newBuilder(
                      "UPDATE Albums "
                          + "SET MarketingBudget = $1 "
                          + "WHERE SingerId = 1 and AlbumId = 1")
                  .bind("p1")
                  .to(album1Budget)
                  .build();
          transaction.executeUpdate(updateStatement);
          Statement updateStatement2 =
              Statement.newBuilder(
                      "UPDATE Albums "
                          + "SET MarketingBudget = $1 "
                          + "WHERE SingerId = 2 and AlbumId = 2")
                  .bind("p1")
                  .to(album2Budget)
                  .build();
          transaction.executeUpdate(updateStatement2);
        }
        return null;
      });
}

מריצים את הדוגמה באמצעות הארגומנט writewithtransactionusingdml.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    writewithtransactionusingdml test-instance example-db

שימוש באינדקס משני

נניח שאתם רוצים לאחזר את כל ה��ו��ו�� ��ל Albums שיש להן ערכי AlbumTitle בטווח מסוים. אפשר לקרוא את כל הערכים מהעמודה AlbumTitle באמצעות הצהרת SQL או קריאה, ואז למחוק את השורות שלא עומדות בקריטריונים. אבל סריקה של כל הטבלה היא פעולה יקרה, במיוחד בטבלאות עם הרבה שורות. במקום זאת, אתם יכולים להאיץ את אחזור השורות כשאתם מחפשים לפי עמודות של מפתחות לא ראשיים, על ידי יצירת אינדקס משני בטבלה.

כדי להוסיף אינדקס משני לטבלה קיימת, צריך לעדכן את הסכימה. כמו עדכוני סכימה אחרים, Spanner תומך בהוספת אינדקס בזמן שמסד הנתונים ממשיך לשרת את התנועה. ‫Spanner ממלא באופן אוטומטי את החוסרים באינדקס עם הנתונים הקיימים. מילויים חוזרים עשויים להימשך כמה דקות, אבל לא צריך להעביר את מסד הנתונים למצב אופליין או להימנע מכתיבה בטבלה שנוסף לה אינדקס במהלך התהליך הזה. למידע נוסף, ראו הוספת אינדקס משני.

אחרי שמוסיפים אינדקס משני, Spanner משתמש בו באופן אוטומטי לשאילתות SQL שסביר להניח שיפעלו מהר יותר עם האינדקס. אם אתם משתמשים בממשק הקריאה, אתם צריכים לציין את האינדקס שבו אתם רוצים להשתמש.

הוספת אינדקס משני

אפשר להוסיף אינדקס בשורת הפקודה באמצעות ה-CLI של gcloud, או באופן פרוגרמטי באמצעות ספריית הלקוח של Spanner ל-Java.

בשורת הפקודה

משתמשים בפקודה הבאה CREATE INDEX כדי להוסיף אינדקס למסד הנתונים:

gcloud spanner databases ddl update example-db --instance=test-instance \
    --ddl='CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)'

אמור להופיע:

Schema updating...done.

שימוש בספריית הלקוח של Spanner ל-Java

משתמשים ב-method‏ updateDatabaseDdl() של המחלקה DatabaseAdminClient כדי להוסיף אינדקס:

static void addIndex(DatabaseAdminClient adminClient, DatabaseName databaseName) {
  try {
    // Initiate the request which returns an OperationFuture.
    adminClient.updateDatabaseDdlAsync(
        databaseName,
        Arrays.asList("CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)")).get();
    System.out.println("Added AlbumsByAlbumTitle index");
  } catch (ExecutionException e) {
    // If the operation failed during execution, expose the cause.
    throw (SpannerException) e.getCause();
  } catch (InterruptedException e) {
    // Throw when a thread is waiting, sleeping, or otherwise occupied,
    // and the thread is interrupted, either before or during the activity.
    throw SpannerExceptionFactory.propagateInterrupt(e);
  }
}

מריצים את הדוגמה באמצעות הארגומנט addindex.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    addindex test-instance example-db

הוספת אינדקס יכולה להימשך כמה דקות. אחרי שמוסיפים את האינדקס, אמור להופיע:

Added the AlbumsByAlbumTitle index.

קריאה באמצעות האינדקס

בשאילתות SQL, ‏ Spanner משתמש באופן אוטומטי באינדקס מתאים. בממשק הקריאה, צריך לציין את האינדקס בבקשה.

כדי להשתמש באינדקס בממשק הקריאה, משתמשים ב-method‏ readUsingIndex() של המחלקה ReadContext.

הקוד הבא מאחזר את כל העמודות AlbumId ו-AlbumTitle מהאינדקס AlbumsByAlbumTitle.

static void readUsingIndex(DatabaseClient dbClient) {
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .readUsingIndex(
              "Albums",
              "AlbumsByAlbumTitle",
              KeySet.all(),
              Arrays.asList("AlbumId", "AlbumTitle"))) {
    while (resultSet.next()) {
      System.out.printf("%d %s\n", resultSet.getLong(0), resultSet.getString(1));
    }
  }
}

מריצים את הדוגמה באמצעות הארגומנט readindex.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    readindex test-instance example-db

אמור להופיע:

2 Forever Hold Your Peace
2 Go, Go, Go
1 Green
3 Terrified
1 Total Junk

הוספת אינדקס לקריאות של אינדקס בלבד

אולי שמתם לב שבדוגמה הקודמת של פעולת הקריאה לא קראנו את העמודה MarketingBudget. הסיבה לכך היא שממשק הקריאה של Spanner לא תומך באפשרות לצרף אינדקס לטבלת נתונים כדי לחפש ערכים שלא מאוחסנים באינדקס.

יוצרים הגדרה חלופית של AlbumsByAlbumTitle ששומרת עותק של MarketingBudget באינדקס.

בשורת הפקודה

GoogleSQL

gcloud spanner databases ddl update example-db --instance=test-instance \
    --ddl='CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)

PostgreSQL

gcloud spanner databases ddl update example-db --instance=test-instance \
    --ddl='CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) INCLUDE (MarketingBudget)

הוספת אינדקס יכולה להימשך כמה דקות. אחרי שמוסיפים את האינדקס, אמור להופיע:

Schema updating...done.

שימוש בספריית הלקוח של Spanner ל-Java

משתמשים ב-method‏ updateDatabaseDdl() של המחלקה DatabaseAdminClient כדי להוסיף אינדקס עם משפט STORING ל-GoogleSQL ומשפט INCLUDE ל-PostgreSQL:

GoogleSQL

static void addStoringIndex(DatabaseAdminClient adminClient, DatabaseName databaseName) {
  try {
    // Initiate the request which returns an OperationFuture.
    adminClient.updateDatabaseDdlAsync(
        databaseName,
        Arrays.asList(
            "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) "
                + "STORING (MarketingBudget)")).get();
    System.out.println("Added AlbumsByAlbumTitle2 index");
  } catch (ExecutionException e) {
    // If the operation failed during execution, expose the cause.
    throw (SpannerException) e.getCause();
  } catch (InterruptedException e) {
    // Throw when a thread is waiting, sleeping, or otherwise occupied,
    // and the thread is interrupted, either before or during the activity.
    throw SpannerExceptionFactory.propagateInterrupt(e);
  }
}

PostgreSQL

static void addStoringIndex(DatabaseAdminClient adminClient, DatabaseName databaseName) {
  try {
    // Initiate the request which returns an OperationFuture.
    adminClient.updateDatabaseDdlAsync(
        databaseName,
        Arrays.asList(
            "CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) "
                + "INCLUDE (MarketingBudget)")).get();
    System.out.println("Added AlbumsByAlbumTitle2 index");
  } catch (ExecutionException e) {
    // If the operation failed during execution, expose the cause.
    throw (SpannerException) e.getCause();
  } catch (InterruptedException e) {
    // Throw when a thread is waiting, sleeping, or otherwise occupied,
    // and the thread is interrupted, either before or during the activity.
    throw SpannerExceptionFactory.propagateInterrupt(e);
  }
}

מריצים את הדוגמה באמצעות הארגומנט addstoringindex.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    addstoringindex test-instance example-db

הוספת אינדקס יכולה להימשך כמה דקות. אחרי שמוסיפים את האינדקס, אמור להופיע:

Added AlbumsByAlbumTitle2 index

עכשיו אפשר לבצע קריאה שמאחזרת את כל העמודות AlbumId,‏ AlbumTitle ו-MarketingBudget מהאינדקס AlbumsByAlbumTitle2:

static void readStoringIndex(DatabaseClient dbClient) {
  // We can read MarketingBudget also from the index since it stores a copy of MarketingBudget.
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .readUsingIndex(
              "Albums",
              "AlbumsByAlbumTitle2",
              KeySet.all(),
              Arrays.asList("AlbumId", "AlbumTitle", "MarketingBudget"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %s %s\n",
          resultSet.getLong(0),
          resultSet.getString(1),
          resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"));
    }
  }
}

מריצים את הדוגמה באמצעות הארגומנט readstoringindex.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    readstoringindex test-instance example-db

הפלט אמור להיראות כך:

2 Forever Hold Your Peace 300000
2 Go, Go, Go NULL
1 Green NULL
3 Terrified NULL
1 Total Junk 300000

אחזור נתונים באמצעות עסקאות לקריאה בלבד

נניח שאתם רוצים לבצע יותר מקריאה אחת באותה חותמת זמן. עסקאות לקריאה בלבד צופות בתחילית עקבית של היסטוריית ה-commit של העסקה, כך שהאפליקציה תמיד מקבלת נתונים עקביים. משתמשים באובייקט ReadOnlyTransaction כדי לבצע טרנזקציות לקריאה בלבד. משתמשים ב-method‏ readOnlyTransaction() של המחלקה DatabaseClient כדי לקבל אובייקט ReadOnlyTransaction.

כך מריצים שאילתה ומבצעים קריאה באותה עסקה לקריאה בלבד:

static void readOnlyTransaction(DatabaseClient dbClient) {
  // ReadOnlyTransaction must be closed by calling close() on it to release resources held by it.
  // We use a try-with-resource block to automatically do so.
  try (ReadOnlyTransaction transaction = dbClient.readOnlyTransaction()) {
    try (ResultSet queryResultSet =
        transaction.executeQuery(
            Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) {
      while (queryResultSet.next()) {
        System.out.printf(
            "%d %d %s\n",
            queryResultSet.getLong(0), queryResultSet.getLong(1), queryResultSet.getString(2));
      }
    } // queryResultSet.close() is automatically called here
    try (ResultSet readResultSet =
        transaction.read(
          "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) {
      while (readResultSet.next()) {
        System.out.printf(
            "%d %d %s\n",
            readResultSet.getLong(0), readResultSet.getLong(1), readResultSet.getString(2));
      }
    } // readResultSet.close() is automatically called here
  } // transaction.close() is automatically called here
}

מריצים את הדוגמה באמצעות הארגומנט readonlytransaction.

java -jar target/spanner-snippets/spanner-google-cloud-samples.jar \
    readonlytransaction test-instance example-db

הפלט אמור להיראות כך:

2 2 Forever Hold Your Peace
1 2 Go, Go, Go
2 1 Green
2 3 Terrified
1 1 Total Junk
1 1 Total Junk
1 2 Go, Go, Go
2 1 Green
2 2 Forever Hold Your Peace
2 3 Terrified

הסרת המשאבים

כדי להימנע מחיובים נוספים בחשבון לחיוב ב-Cloud על המשאבים שבהם השתמשתם במדריך הזה, אתם צריכים למחוק את מסד הנתונים ואת המכונה שיצרתם.

מחיקת מסד הנתונים

אם מוחקים מכונה, כל מסדי הנתונים שבתוכה נמחקים באופן אוטומטי. בשלב הזה נסביר איך למחוק מסד נתונים בלי למחוק מכונה (עדיין תחויבו על המכונה).

בשורת הפקודה

gcloud spanner databases delete example-db --instance=test-instance

באמצעות מסוף Google Cloud

  1. נכנסים לדף Spanner Instances במסוף Google Cloud .

    כניסה לדף Instances

  2. לוחצים על המופע.

  3. לוחצים על מסד הנתונים שרוצים למחוק.

  4. בדף Database details, לוחצים על Delete.

  5. מאשרים שרוצים למחוק את מסד הנתונים ולוחצים על מחיקה.

מחיקת המכונה

מחיקת מכונה גורמת להסרה אוטומטית של כל מסדי הנתונים שנוצרו בה.

בשורת הפקודה

gcloud spanner instances delete test-instance

באמצעות מסוף Google Cloud

  1. נכנסים לדף Spanner Instances במסוף Google Cloud .

    כניסה לדף Instances

  2. לוחצים על המכונה.

  3. לוחצים על Delete.

  4. מאשרים שרוצים למחוק את המכונה ולוחצים על מחיקה.

המאמרים הבאים