Cloud Spanner עם Hibernate ORM

1. סקירה כללית

Hibernate הפך לפתרון ה-ORM הסטנדרטי בפועל לפרויקטים ב-Java. הוא תומך בכל מסדי הנתונים הרלציוניים העיקריים ומאפשר כלי ORM חזקים עוד יותר כמו Spring Data JPA. בנוסף, יש הרבה frameworks רבות שתואמות למצב הירדמות, כמו Spring Boot, Microprofile ו-Quarkus.

בעזרת Cloud Spanner Dialect for Hibernate ORM אפשר להשתמש ב-Hibernate עם Cloud Spanner. נהנים מהיתרונות של Cloud Spanner – מדרגיות וסמנטיקה יחסית – עם ההתמדה האידיומטית של Hibernate. כך ניתן להעביר אפליקציות קיימות לענן או לכתוב אפליקציות חדשות, תוך ניצול פרודוקטיביות מוגברת של המפתחים הודות לטכנולוגיות המבוססות על Hibernate.

מה תלמדו

  • איך לכתוב אפליקציה פשוטה למצב 'שינה' שמתחברת ל-Cloud Spanner
  • איך יוצרים מסד נתונים ב-Cloud Spanner
  • איך משתמשים בדיאלקט Cloud Spanner ל-Hibernate ORM
  • איך ליישם פעולות יצירה-קריאה-עדכון-מחיקה (CRUD) באמצעות מצב שינה

מה צריך להכין

  • פרויקט ב-Google Cloud Platform
  • דפדפן, למשל Chrome או Firefox

2. הגדרה ודרישות

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

  1. נכנסים למסוף Cloud ויוצרים פרויקט חדש או עושים שימוש חוזר בפרויקט קיים. (אם עדיין אין לכם חשבון Gmail או G Suite, עליכם ליצור חשבון).

k6ai2NRmxIjV5iMFlVgA_ZyAWE4fhRrkrZZ5mZuCas81YLgk0iwIyvgoDet4s2lMYGC5K3xLSOjIbmC9kjiezvQuxuhdYRolbv1rft1lOmA_P2U3OYcaAzN9JgP-Ncm18i5qgf9LzA

UtcCMcSYtCOrEWuILx3XBwb3GILPqXDd6cJiQQxmylg8GNftqlnE7u8aJLhlr1ZLRkpncKdj8ERnqcH71wab2HlfUnO9CgXKd0-CAQC2t3CH0kuQRxdtP0ws43t5-O2d4d0WXDUfaw

KoK3nfWQ73s_x4QI69xqzqdDR4tUuNmrv4FC9Yq8vtK5IVm49h_8h6x9X281hAcJcOFDtX7g2BXPvP5O7SOR2V4UI6W8gN6cTJCVAdtWHRrS89zH-qWE0IQdjFpOs_8T-s4vQCXA6w

חשוב לזכור את מזהה הפרויקט, שם ייחודי לכל הפרויקטים ב-Google Cloud (השם שלמעלה כבר תפוס ולא מתאים לכם, סליחה). בהמשך ב-Codelab הזה, היא תיקרא PROJECT_ID.

  1. בשלב הבא צריך להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבים של Google Cloud.

מעבר ב-Codelab הזה לא אמור לעלות הרבה, אם בכלל. חשוב לבצע את כל ההוראות בקטע 'ניקוי' שמסביר איך להשבית משאבים כדי שלא תצברו חיובים מעבר למדריך הזה. משתמשים חדשים ב-Google Cloud זכאים להשתתף בתוכנית תקופת ניסיון בחינם בשווי 1,200 ש"ח.

הפעלת Cloud Shell

  1. במסוף Cloud, לוחצים על Activate Cloud Shell R47NpBm6yyzso5vnxnRBikeDAXxl3LsM6tip3rJxnKuS2EZdCI0h-eIOGm9aECq8JXbMFlJkd68uTutXU8gGmQUVa5iIwUdZcz2tzqZ_mj1OdZcz2tzqZ_mj.

STsYbcAtkIQyN6nL9BJhld3Fv5KxedYynpUVcRWwvIR-sYMMc4kfK-unIYgtsD4P6T0P8z-A12388tPmAh-Trsx80qobaW4KQXHJ7qJI6rwm762LrxurYbxwiDG-v_HiUYsWnXMciw

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

LnGMTn1ObgwWFtWpjSdzlA9TDvSbcY76GiLQLc_f7RP1QBK1Tl4H6kLCHzsi89Lkc-serOpqNH-F2XKmV5AnBqTbPon4HvCwSSrY_ERFHzeYmK1lnTfr-6x5eVoaHpRSrCUrolXUPQ

ההקצאה וההתחברות ל-Cloud Shell נמשכת כמה דקות.

hfu9bVHmrWw01Hnrlf4MBNja6yvssDnZzN9oupcG12PU88Vvo30tTluX9IySwnu5_TG3U2UXAasX9eCwqwZtc6Yhwxri95zG82DLUcKxrFYaXnVd7OqVoU6zanoZa0PtvubjLLHxnA

למכונה הווירטואלית הזו נטען כל כלי הפיתוח הדרושים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר משמעותית את ביצועי הרשת והאימות. אם לא את כולן, ניתן לבצע חלק גדול מהעבודה ב-Codelab הזה באמצעות דפדפן או Chromebook.

אחרי ההתחברות ל-Cloud Shell, אתם אמורים לראות שכבר בוצע אימות ושהפרויקט כבר מוגדר למזהה הפרויקט שלכם.

  1. מריצים את הפקודה הבאה ב-Cloud Shell כדי לוודא שהאימות בוצע:
gcloud auth list

פלט הפקודה

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

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

פלט הפקודה

[core]
project = <PROJECT_ID>

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

gcloud config set project <PROJECT_ID>

פלט הפקודה

Updated property [core/project].

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

אחרי ההשקה של Cloud Shell, תוכלו להתחיל להשתמש ב-gcloud כדי לקיים אינטראקציה עם הפרויקט שלכם ב-GCP.

קודם כול, מפעילים את Cloud Spanner API.

gcloud services enable spanner.googleapis.com

עכשיו ניצור מכונה של Cloud Spanner בשם codelab-instance.

gcloud spanner instances create codelab-instance \
 --config=regional-us-central1 \
 --description="Codelab Instance" --nodes=1

עכשיו צריך להוסיף מסד נתונים למכונה הזו. נקרא לו codelab-db.

gcloud spanner databases create codelab-db --instance=codelab-instance

4. יצירת אפליקציה ריקה

נשתמש ב-Maven Quickstart Archetype כדי ליצור אפליקציה פשוטה במסוף Java.

mvn archetype:generate \
 -DgroupId=codelab \
 -DartifactId=spanner-hibernate-codelab \
 -DarchetypeArtifactId=maven-archetype-quickstart \
 -DarchetypeVersion=1.4 \
 -DinteractiveMode=false

מעבר לספריית האפליקציות.

cd spanner-hibernate-codelab

הידור וההפעלה של האפליקציה באמצעות Maven.

mvn compile exec:java -Dexec.mainClass=codelab.App

Hello World! אמור להופיע במסוף.

5. הוספת יחסי תלות

כדי לחקור את קוד המקור, פותחים את Cloud Shell Editor ומעיינים בספרייה spanner-hibernate-codelab.

b5cb37d043d4d2b0.png

עד עכשיו, יש לנו רק אפליקציה בסיסית של מסוף Java שמדפיסה את "Hello World!". עם זאת, אנחנו באמת רוצים לכתוב אפליקציית Java שמשתמשת ב-Hibernate כדי לדבר עם Cloud Spanner. לשם כך נזדקק לדיאלקט של Cloud Spanner for Hibernate, למנהל התקן ה-JDBC של Cloud Spanner ולליבה של Hibernate. עכשיו נוסיף את יחסי התלות הבאים לבלוק <dependencies> שבתוך הקובץ pom.xml.

pom.xml

    <!-- Spanner Dialect -->
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-spanner-hibernate-dialect</artifactId>
      <version>1.5.0</version>
    </dependency>

    <!-- JDBC Driver -->
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-spanner-jdbc</artifactId>
      <version>2.0.0</version>
    </dependency>

    <!-- Hibernate -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>5.4.29.Final</version>
    </dependency>

6. הגדרת ORM למצב שינה

בשלב הבא ניצור קובצי תצורה של מצב 'שינה' hibernate.cfg.xml ו-hibernate.properties. מריצים את הפקודה הבאה כדי ליצור את הקבצים הריקים ואז עורכים אותם באמצעות Cloud Shell Editor.

mkdir src/main/resources \
 && touch src/main/resources/hibernate.cfg.xml \
 && touch src/main/resources/hibernate.properties

בואו נודיע ל-Hibernate על מחלקות הישויות עם הערות שנמפה למסד הנתונים על ידי מילוי של השדה hibernate.cfg.xml. (אנחנו ניצור את סיווגי הישויות מאוחר יותר).

src/main/resources/hibernate.cfg.xml

<hibernate-configuration>
  <session-factory>
    <!-- Annotated entity classes -->
    <mapping class="codelab.Album"/>
    <mapping class="codelab.Singer"/>
  </session-factory>
</hibernate-configuration>

Hibernate צריך גם לדעת איך להתחבר למכונה של Cloud Spanner ובאיזה ניב להשתמש. לכן נאמר לו להשתמש ב-SpannerDialect לתחביר SQL, במנהל התקן JDBC Spanner ובמחרוזת החיבור JDBC עם הקואורדינטות של מסד הנתונים. הוא נכנס לקובץ hibernate.properties.

src/main/resources/hibernate.properties

hibernate.dialect=com.google.cloud.spanner.hibernate.SpannerDialect
hibernate.connection.driver_class=com.google.cloud.spanner.jdbc.JdbcDriver
hibernate.connection.url=jdbc:cloudspanner:/projects/{PROJECT_ID}/instances/codelab-instance/databases/codelab-db
# auto-create or update DB schema
hibernate.hbm2ddl.auto=update
hibernate.show_sql=true

זכרו להחליף את {PROJECT_ID} במזהה הפרויקט. כדי לקבל אותו, מריצים את הפקודה הבאה:

gcloud config get-value project

אין לנו סכימה קיימת של מסד נתונים, לכן הוספנו את המאפיין hibernate.hbm2ddl.auto=update כדי לאפשר ל-Hibernate ליצור את שתי הטבלאות ב-Cloud Spanner כשאנחנו מפעילים את האפליקציה בפעם הראשונה.

בדרך כלל צריך גם לוודא שהוגדרו פרטי הכניסה לאימות באמצעות קובץ JSON של חשבון שירות במשתנה הסביבה GOOGLE_APPLICATION_CREDENTIALS או באמצעות פרטי הכניסה שמוגדרים כברירת מחדל של האפליקציה באמצעות הפקודה gcloud auth application-default login. עם זאת, מכיוון שאנחנו מריצים את Cloud Shell, כבר הוגדרו פרטי כניסה לפרויקט כברירת מחדל.

7. יצירת מחלקות של ישויות עם הערות

עכשיו אנחנו מוכנים לכתוב קוד.

נגדיר שני אובייקטים ישנים רגילים של Java (POJO) שימפו לטבלאות ב-Cloud Spanner – Singer ו-Album. לחשבון Album יהיה קשר @ManyToOne עם Singer. אפשר גם למפות Singer לרשימות של Album עם הערה @OneToMany, אבל בדוגמה הזו, אנחנו לא רוצים לטעון את כל האלבומים בכל פעם שצריך לאחזר זמר ממסד הנתונים.

מוסיפים את מחלקות הישויות Singer ו-Album.

יוצרים את הקבצים של הכיתה.

touch src/main/java/codelab/Singer.java \
&& touch src/main/java/codelab/Album.java

מדביקים את התוכן של הקבצים.

src/main/java/codelab/Singer.java

package codelab;

import java.util.Date;
import java.util.UUID;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.hibernate.annotations.Type;

@Entity
public class Singer {

  @Id
  @GeneratedValue
  @Type(type = "uuid-char")
  private UUID singerId;

  private String firstName;

  private String lastName;

  @Temporal(TemporalType.DATE)
  private Date birthDate;

  public Singer() {
  }

  public Singer(String firstName, String lastName, Date birthDate) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.birthDate = birthDate;
  }

  public UUID getSingerId() {
    return singerId;
  }

  public void setSingerId(UUID singerId) {
    this.singerId = singerId;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }

  public Date getBirthDate() {
    return birthDate;
  }

  public void setBirthDate(Date birthDate) {
    this.birthDate = birthDate;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof Singer)) {
      return false;
    }

    Singer singer = (Singer) o;

    if (!firstName.equals(singer.firstName)) {
      return false;
    }
    if (!lastName.equals(singer.lastName)) {
      return false;
    }
    return birthDate.equals(singer.birthDate);
  }

  @Override
  public int hashCode() {
    int result = firstName.hashCode();
    result = 31 * result + lastName.hashCode();
    result = 31 * result + birthDate.hashCode();
    return result;
  }
}

src/main/java/codelab/Album.java

package codelab;

import java.util.UUID;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import org.hibernate.annotations.Type;

@Entity
public class Album {

  @Id
  @GeneratedValue
  @Type(type = "uuid-char")
  UUID albumId;

  @ManyToOne
  Singer singer;

  String albumTitle;

  public Album() {
  }

  public Album(Singer singer, String albumTitle) {
    this.singer = singer;
    this.albumTitle = albumTitle;
  }

  public UUID getAlbumId() {
    return albumId;
  }

  public void setAlbumId(UUID albumId) {
    this.albumId = albumId;
  }

  public Singer getSinger() {
    return singer;
  }

  public void setSinger(Singer singer) {
    this.singer = singer;
  }

  public String getAlbumTitle() {
    return albumTitle;
  }

  public void setAlbumTitle(String albumTitle) {
    this.albumTitle = albumTitle;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof Album)) {
      return false;
    }

    Album album = (Album) o;

    if (!singer.equals(album.singer)) {
      return false;
    }
    return albumTitle.equals(album.albumTitle);
  }

  @Override
  public int hashCode() {
    int result = singer.hashCode();
    result = 31 * result + albumTitle.hashCode();
    return result;
  }
}

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

8. שמירת ישויות ושליחת שאילתות

לאחר שכל האובייקטים מוגדרים והאובייקטים של הישויות, אנחנו יכולים להתחיל לכתוב למסד הנתונים ולשלוח שאילתות לגביו. אנחנו נפתח Session של שנת יםclearData()writeData()readData()

מחליפים את התוכן של App.java בערך הבא:

src/main/java/codelab/App.java

package codelab;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;

public class App {

  public final static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

  public static void main(String[] args) {
    // create a Hibernate sessionFactory and session
    StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure().build();
    SessionFactory sessionFactory = new MetadataSources(registry).buildMetadata()
        .buildSessionFactory();
    Session session = sessionFactory.openSession();

    clearData(session);

    writeData(session);

    readData(session);

    // close Hibernate session and sessionFactory
    session.close();
    sessionFactory.close();
  }

  private static void clearData(Session session) {
    session.beginTransaction();

    session.createQuery("delete from Album where 1=1").executeUpdate();
    session.createQuery("delete from Singer where 1=1").executeUpdate();

    session.getTransaction().commit();
  }

  private static void writeData(Session session) {
    session.beginTransaction();

    Singer singerMelissa = new Singer("Melissa", "Garcia", makeDate("1981-03-19"));
    Album albumGoGoGo = new Album(singerMelissa, "Go, Go, Go");
    session.save(singerMelissa);
    session.save(albumGoGoGo);

    session.save(new Singer("Russell", "Morales", makeDate("1978-12-02")));
    session.save(new Singer("Jacqueline", "Long", makeDate("1990-07-29")));
    session.save(new Singer("Dylan", "Shaw", makeDate("1998-05-02")));

    session.getTransaction().commit();
  }

  private static void readData(Session session) {
    List<Singer> singers = session.createQuery("from Singer where birthDate >= '1990-01-01' order by lastName")
        .list();
    List<Album> albums = session.createQuery("from Album").list();

    System.out.println("Singers who were born in 1990 or later:");
    for (Singer singer : singers) {
      System.out.println(singer.getFirstName() + " " + singer.getLastName() + " born on "
          + DATE_FORMAT.format(singer.getBirthDate()));
    }

    System.out.println("Albums: ");
    for (Album album : albums) {
      System.out
          .println("\"" + album.getAlbumTitle() + "\" by " + album.getSinger().getFirstName() + " "
              + album.getSinger().getLastName());
    }
  }

  private static Date makeDate(String dateString) {
    try {
      return DATE_FORMAT.parse(dateString);
    } catch (ParseException e) {
      e.printStackTrace();
      return null;
    }
  }
}

עכשיו נקמפל ונריץ את הקוד. נוסיף את האפשרות -Dexec.cleanupDaemonThreads=false כדי לבטל אזהרות לגבי ניקוי של שרשורי דימון (daemon) ש-Maven ינסה לבצע.

mvn compile exec:java -Dexec.mainClass=codelab.App -Dexec.cleanupDaemonThreads=false

בפלט אמור להופיע משהו כזה:

Singers who were born in 1990 or later:
Jacqueline Long born on 1990-07-29
Dylan Shaw born on 1998-05-02
Albums: 
"Go, Go, Go" by Melissa Garcia

בשלב הזה, אם עוברים למסוף Cloud Spanner וצופים בנתונים של הטבלאות Singer ו-Album (אלבום) במסד הנתונים, תראו משהו כזה:

f18276ea54cc266f.png

952d9450dd659e75.png

9. הסרת המשאבים

נמחק את המכונה של Cloud Spanner שיצרנו בהתחלה, כדי לוודא שהיא לא משתמשת במשאבים שלא לצורך.

gcloud spanner instances delete codelab-instance

10. מזל טוב

יופי! יצרת בהצלחה אפליקציית Java שמשתמשת ב-Hibernate כדי לשמור נתונים ב-Cloud Spanner.

  • יצרתם מכונה של Cloud Spanner ומסד נתונים
  • הגדרת את האפליקציה לשימוש במצב 'שינה'
  • יצרתם שתי ישויות: 'אומן' ו'אלבום'
  • יצרתם באופן אוטומטי את סכימת מסד הנתונים לאפליקציה שלכם
  • שמרת בהצלחה ישויות ב-Cloud Spanner ושלחת להן שאילתות

עכשיו אתם יודעים מהם השלבים העיקריים שנדרשים כדי לכתוב אפליקציית Hibernate באמצעות Cloud Spanner.

מה השלב הבא?