Cloud Spanner עם Hibernate ORM

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

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

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

מה תלמדו

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

מה תצטרכו

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

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

הגדרת סביבה בקצב אישי

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

k6ai2NRmxIjV5iMFlVgA_ZyAWE4fhRrkrZZ5mZuCas81YLgk0iwIyvgoDet4s2lMYGC5K3xLSOjIbmC9kjiezvQuxuhdYRolbv1rft1lOmA_P2U3OYcaAzN9JgP-Ncm18i5qgf9LzA

UtcCMcSYtCOrEWuILx3XBwb3GILPqXDd6cJiQQxmylg8GNftqlnE7u8aJLhlr1ZLRkpncKdj8ERnqcH71wab2HlfUnO9CgXKd0-CAQC2t3CH0kuQRxdtP0ws43t5-O2d4d0WXDUfaw

KoK3nfWQ73s_x4QI69xqzqdDR4tUuNmrv4FC9Yq8vtK5IVm49h_8h6x9X281hAcJcOFDtX7g2BXPvP5O7SOR2V4UI6W8gN6cTJCVAdtWHRrS89zH-qWE0IQdjFpOs_8T-s4vQCXA6w

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

  1. לאחר מכן, תצטרכו להפעיל את החיוב ב-Cloud Console כדי להשתמש במשאבים של Google Cloud.

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

הפעלת Cloud Shell

  1. ב-Cloud Console, לוחצים על Activate Cloud Shell R47NpBm6yyzso5vnxnRBikeDAXxl3LsM6tip3rJxnKuS2EZdCI0h-eIOGm9aECq8JXbMFlJkd68uTutXU8gGmQUVa5iI1OdZczXP2tzqZ_mj0pR4sZ8eSwOwUlWADR7ARCqrMTQPQA.

STsYbcAtkIQyN6nL9BJhld3Fv5KxedYynpUVcRWwvIR-sYMMc4kfK-unIYgtsD4P6T0P8z-A12388tPmAh-Trsx80qobaW4KQXHJ7qJI6rwm762LrxurYbxwiDG-v_HiUYsWnXMciw

אם זו הפעם הראשונה שאתם מפעילים את Cloud Shell, יוצג לכם מסך ביניים (בחלק הנגלל) עם תיאור של 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 Dialect for Hibernate, את Cloud Spanner JDBC driver ואת Hibernate core. לכן, מוסיפים את יחסי התלות הבאים לבלוק <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. הגדרת Hibernate ORM

בשלב הבא ניצור את קובצי התצורה של Hibernate‏ 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.cfg.xml כדי לציין ל-Hibernate את מחלקות הישויות עם ההערות שאותן נמפה למסד הנתונים. (ניצור את מחלקות הישויות בהמשך).

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, במנהל ההתקן Spanner JDBC ובמחרוזת החיבור 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. יכולנו גם למפות את Singers לרשימות של Albums עם הערה @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, כי הוא מונע נקודות חמות (hotspots) כשהמערכת מחלקת את הנתונים בין השרתים לפי טווחי מפתחות. מפתח של מספר שלם שעולה באופן מונוטוני יעבוד גם הוא, אבל הביצועים שלו עשויים להיות פחות טובים.

8. שמירה של ישויות וביצוע שאילתות לגביהן

אחרי שמגדירים את הכול ומגדירים אובייקטים של ישויות, אפשר להתחיל לכתוב למסד הנתונים ולשאול בו שאילתות. נפתח את Hibernate Session, ואז נשתמש בו כדי למחוק קודם את כל השורות בטבלה בשיטה clearData(), לשמור כמה ישויות בשיטה writeData() ולהריץ כמה שאילתות באמצעות שפת השאילתות של Hibernate‏ (HQL) בשיטה 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 ומסד נתונים
  • הגדרתם את האפליקציה לשימוש ב-Hibernate
  • יצרת שתי ישויות: אומן ואלבום
  • יצרתם באופן אוטומטי את סכימת מסד הנתונים של האפליקציה
  • שמרתם ישויות ב-Cloud Spanner וביצעתם עליהן שאילתות

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

מה השלב הבא?