ברוכים הבאים למדריך לדוגמת שכבת המטמון של Hibernate. היום נצפה על EHCache של Hibernate שהוא ספק המטמון שכבת השנייה הפופולרי ביותר.
שכבת המטמון של Hibernate
אחד היתרונות המרכזיים בשימוש ב-Hibernate באפליקציה גדולה הוא התמיכה שלו במטמון, ולכן יש פחות שאילתות למסד הנתונים וביצועים טובים יותר. בדוגמה הקודמת, ביקשנו את שכבת המטמון הראשונה של Hibernate והיום נביט בשכבת המטמון השנייה של Hibernate באמצעות הממשק של Hibernate EHCache. ספקי המטמון של שכבת המטמון השנייה של Hibernate כוללים את EHCache ו-Infinispan, אך EHCache הוא יותר פופולרי ונשתמש בו לדוגמת הפרויקט שלנו. אך לפני שנמשיך לפרויקט שלנו, עלינו לדעת את האסטרטגיות השונות למטמון אובייקט.
- קריאה בלבד: אסטרטגיית המטמון הזו צריכה לשמש לאובייקטים עמידים שתמיד יקראו אבל לעולם לא יעודכנו. זה טוב לקריאה ולמטמון של הגדרות האפליקציה ונתונים סטטיים אחרים שלעולם לא מתעדכנים. זו האסטרטגיה הפשוטה ביותר עם ביצועים הטובים ביותר מאחר ואין עומס לבדיקה אם האובייקט עודכן במסד הנתונים או לא.
- קריאה וכתיבה: זה יתרון עבור אובייקטים עמידים שיכולים להתעדכן על ידי אפליקציית ההיברנייט. אך אם הנתונים מתעדכנים באמצעות הגבהינרנייט או אפליקציות אחרות, אין דרך שההיברנייט ידע על כך והנתונים עשויים להיות מיושנים. לכן בעת שימוש באסטרטגיה זו, וודאו שאתם משתמשים ב- API של היברנייט לעדכון הנתונים.
- קריאה וכתיבה לא מוגבלת: אם האפליקציה זקוקה לעדכון נתונים באופן חריג ואינה דורשת בהכרח בידוד עסקי מחמיר, זיכרון מטמון לא מוגבל בקריאה וכתיבה עשוי להיות מתאים.
- טרנזקציונלי: אסטרטגיית המטמון הטרנזקציונלית מספקת תמיכה בספקי מטמון טרנזקציונליים מלאים כמו JBoss TreeCache. מטמון כזה יכול להיות בשימוש רק בסביבת JTA ועליך לציין hibernate.transaction.manager_lookup_class.
היברנייט EHCache
מאחר ו- EHCache תומך בכל אסטרטגיות המטמון האמורות לעיל, זהו הבחירה הטובה ביותר כאשר אתה מחפש מטמון מדרג שני בהיברנייט. לא אעסוק בפרטים רבים על EHCache, המוקד העיקרי שלי יהיה להפעיל אותו עבור אפליקציית ההיברנייט. צור פרויקט מייבן באקליפס או ב- IDE האהוב עליך, המימוש הסופי יראה דומה לתמונה למטה. בואו נבחן את כל רכיבי האפליקציה אחד אחד.
תלותי Maven של Hibernate EHCache
עבור מטמון הרמה השנייה של Hibernate, נצטרך להוסיף תלות ehcache-core ו־hibernate-ehcache ביישום שלנו. EHCache משתמש ב־slf4j לרישום, לכן הוספתי גם slf4j-simple לצורך רישום. אני משתמש בגרסאות האחרונות של כל אלו ה־APIים, יש סיכוי מעט שAPIים של hibernate-ehcache לא יהיו תואמים לAPI של ehcache-core, במקרה כזה עליכם לבדוק את ה־pom.xml של hibernate-ehcache כדי לגלות את הגרסה הנכונה לשימוש. ה־pom.xml הסופי שלנו נראה כך.
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.journaldev.hibernate</groupId>
<artifactId>HibernateEHCacheExample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Hibernate Secondary Level Cache Example using EHCache implementation</description>
<dependencies>
<!-- Hibernate Core API -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.3.5.Final</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.0.5</version>
</dependency>
<!-- EHCache Core APIs -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.9</version>
</dependency>
<!-- Hibernate EHCache API -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>4.3.5.Final</version>
</dependency>
<!-- EHCache uses slf4j for logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.5</version>
</dependency>
</dependencies>
</project>
הגדרת מטמון הרמה השנייה של Hibernate – הגדרת EHCache של Hibernate
מטמון הרמה השנייה של Hibernate מושבת כברירת מחדל, לכן עלינו להפעיל אותו ולהוסיף כמה הגדרות כדי להפעיל אותו. קובץ התצורה שלנו, hibernate.cfg.xml, נראה כך.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration SYSTEM "classpath://org/hibernate/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.password">pankaj123</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost/TestDB</property>
<property name="hibernate.connection.username">pankaj</property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
<property name="hibernate.current_session_context_class">thread</property>
<property name="hibernate.show_sql">true</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
<!-- For singleton factory -->
<!-- <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory</property>
-->
<!-- enable second level cache and query cache -->
<property name="hibernate.cache.use_second_level_cache">true</property>
<property name="hibernate.cache.use_query_cache">true</property>
<property name="net.sf.ehcache.configurationResourceName">/myehcache.xml</property>
<mapping class="com.journaldev.hibernate.model.Employee" />
<mapping class="com.journaldev.hibernate.model.Address" />
</session-factory>
</hibernate-configuration>
כמה נקודות חשובות אודות הגדרות מטמון הרמה השנייה של Hibernate הם:
- hibernate.cache.region.factory_class משמש להגדרת מחלקת המפעיל עבור המטמון ברמת הרכב השני, אני משתמש ב־
org.hibernate.cache.ehcache.EhCacheRegionFactory
עבור זה. אם ברצונך שמחלקת המפעיל תהיה סינגלטון, עליך להשתמש במחלקהorg.hibernate.cache.ehcache.SingletonEhCacheRegionFactory
. אם אתה משתמש בהיברנייט 3, המחלקות המתאימות יהיוnet.sf.ehcache.hibernate.EhCacheRegionFactory
ו־net.sf.ehcache.hibernate.SingletonEhCacheRegionFactory
. - hibernate.cache.use_second_level_cache משמש להפעלת המטמון ברמת הרכב השני.
- hibernate.cache.use_query_cache משמש להפעלת מטמון השאילתות, בלעדיו תוצאות שאילתות HQL לא יישמרו במטמון.
- net.sf.ehcache.configurationResourceName משמש להגדרת מיקום קובץ התצורה של EHCache, זו פרמטר אופציונלי ואם הוא אינו נוכח EHCache ינסה לאתר את קובץ ehcache.xml בספריית המחלקה של האפליקציה.
קובץ התצורה של EHCache של היברנייט
קובץ התצורה שלנו myehcache.xml נראה כמו שמופיע למטה.
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd" updateCheck="true"
monitoring="autodetect" dynamicConfig="true">
<diskStore path="java.io.tmpdir/ehcache" />
<defaultCache maxEntriesLocalHeap="10000" eternal="false"
timeToIdleSeconds="120" timeToLiveSeconds="120" diskSpoolBufferSizeMB="30"
maxEntriesLocalDisk="10000000" diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU" statistics="true">
<persistence strategy="localTempSwap" />
</defaultCache>
<cache name="employee" maxEntriesLocalHeap="10000" eternal="false"
timeToIdleSeconds="5" timeToLiveSeconds="10">
<persistence strategy="localTempSwap" />
</cache>
<cache name="org.hibernate.cache.internal.StandardQueryCache"
maxEntriesLocalHeap="5" eternal="false" timeToLiveSeconds="120">
<persistence strategy="localTempSwap" />
</cache>
<cache name="org.hibernate.cache.spi.UpdateTimestampsCache"
maxEntriesLocalHeap="5000" eternal="true">
<persistence strategy="localTempSwap" />
</cache>
</ehcache>
הגדרת EHCache של היברנייט מציעה מגוון אפשרויות, אני לא אדור לעומק רב, אך חלק מההגדרות החשובות לעיל הן:
- diskStore: EHCache מאחסן נתונים בזיכרון אך כאשר הוא מתחיל להתמלא מדי, הוא מתחיל לכתוב נתונים למערכת הקבצים. אנו משתמשים בתכונה זו כדי להגדיר את המיקום שבו EHCache יכתוב את הנתונים שהתמלאו.
- defaultCache: זו תצורה חובה, היא משמשת כאשר יש צורך להטמין אובייקט ואין אזורי הטמנה מוגדרים עבורו.
- cache name="employee": אנו משתמשים באלמנט cache כדי להגדיר את האזור והגדרותיו. אנו יכולים להגדיר מספר אזורים ותכונותיהם, בעת הגדרת תכונות הטמנה של מודלי beans, אנו יכולים גם להגדיר אזור עם אסטרטגיות הטמנה. תכונות ה-cache קלות להבנה וברורות מהשם.
- אזורי Cache
org.hibernate.cache.internal.StandardQueryCache
ו-org.hibernate.cache.spi.UpdateTimestampsCache
מוגדרים מכיוון ש-EHCache הציג אזהרה לכך.
Hibernate Second Level Cache – אסטרטגיית הטמנה של Model Bean
אנו משתמשים באנוטציה org.hibernate.annotations.Cache
כדי לספק את תצורת ההטמנה. org.hibernate.annotations.CacheConcurrencyStrategy
משמשת להגדרת אסטרטגיית ההטמנה ואנו יכולים גם להגדיר את אזור ה-cache לשימוש עבור ה-model beans.
package com.journaldev.hibernate.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
@Entity
@Table(name = "ADDRESS")
@Cache(usage=CacheConcurrencyStrategy.READ_ONLY, region="employee")
public class Address {
@Id
@Column(name = "emp_id", unique = true, nullable = false)
@GeneratedValue(generator = "gen")
@GenericGenerator(name = "gen", strategy = "foreign",
parameters = { @Parameter(name = "property", value = "employee") })
private long id;
@Column(name = "address_line1")
private String addressLine1;
@Column(name = "zipcode")
private String zipcode;
@Column(name = "city")
private String city;
@OneToOne
@PrimaryKeyJoinColumn
private Employee employee;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getAddressLine1() {
return addressLine1;
}
public void setAddressLine1(String addressLine1) {
this.addressLine1 = addressLine1;
}
public String getZipcode() {
return zipcode;
}
public void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
}
package com.journaldev.hibernate.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.Cascade;
@Entity
@Table(name = "EMPLOYEE")
@Cache(usage=CacheConcurrencyStrategy.READ_ONLY, region="employee")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "emp_id")
private long id;
@Column(name = "emp_name")
private String name;
@Column(name = "emp_salary")
private double salary;
@OneToOne(mappedBy = "employee")
@Cascade(value = org.hibernate.annotations.CascadeType.ALL)
private Address address;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
}
ציין כי אני משתמש באותה הגדרת מסד נתונים כמו בדוגמת HQL שב־דוגמה, כדאי לך לבדוק את זה כדי ליצור את טבלאות המסד נתונים ולטעון דוגמאות.
מחלקת שימוש ב־Hibernate SessionFactory
יש לנו מחלקת יעול להגדיר את ה־Hibernate ולקבל את מופע ה-SessionFactory
היחיד.
package com.journaldev.hibernate.util;
import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;
public class HibernateUtil {
private static SessionFactory sessionFactory;
private static SessionFactory buildSessionFactory() {
try {
// יצירת SessionFactory מתוך hibernate.cfg.xml
Configuration configuration = new Configuration();
configuration.configure("hibernate.cfg.xml");
System.out.println("Hibernate Configuration loaded");
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties()).build();
System.out.println("Hibernate serviceRegistry created");
SessionFactory sessionFactory = configuration.buildSessionFactory(serviceRegistry);
return sessionFactory;
}
catch (Throwable ex) {
System.err.println("Initial SessionFactory creation failed." + ex);
ex.printStackTrace();
throw new ExceptionInInitializerError(ex);
}
}
public static SessionFactory getSessionFactory() {
if(sessionFactory == null) sessionFactory = buildSessionFactory();
return sessionFactory;
}
}
פרויקט המטמון שלנו בשלב השני של Hibernate באמצעות Hibernate EHCache הוא מוכן, בואו נכתוב תוכנית פשוטה לבדיקה.
תוכנית לבדיקת Hibernate EHCache
package com.journaldev.hibernate.main;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.stat.Statistics;
import com.journaldev.hibernate.model.Employee;
import com.journaldev.hibernate.util.HibernateUtil;
public class HibernateEHCacheMain {
public static void main(String[] args) {
System.out.println("Temp Dir:"+System.getProperty("java.io.tmpdir"));
// אתחול ישיבות
SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
Statistics stats = sessionFactory.getStatistics();
System.out.println("Stats enabled="+stats.isStatisticsEnabled());
stats.setStatisticsEnabled(true);
System.out.println("Stats enabled="+stats.isStatisticsEnabled());
Session session = sessionFactory.openSession();
Session otherSession = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
Transaction otherTransaction = otherSession.beginTransaction();
printStats(stats, 0);
Employee emp = (Employee) session.load(Employee.class, 1L);
printData(emp, stats, 1);
emp = (Employee) session.load(Employee.class, 1L);
printData(emp, stats, 2);
// ניקוי מטמון השלב הראשון, כך שינוצל מטמון השלב השני
session.evict(emp);
emp = (Employee) session.load(Employee.class, 1L);
printData(emp, stats, 3);
emp = (Employee) session.load(Employee.class, 3L);
printData(emp, stats, 4);
emp = (Employee) otherSession.load(Employee.class, 1L);
printData(emp, stats, 5);
// שחרור משאבים
transaction.commit();
otherTransaction.commit();
sessionFactory.close();
}
private static void printStats(Statistics stats, int i) {
System.out.println("***** " + i + " *****");
System.out.println("Fetch Count="
+ stats.getEntityFetchCount());
System.out.println("Second Level Hit Count="
+ stats.getSecondLevelCacheHitCount());
System.out
.println("Second Level Miss Count="
+ stats
.getSecondLevelCacheMissCount());
System.out.println("Second Level Put Count="
+ stats.getSecondLevelCachePutCount());
}
private static void printData(Employee emp, Statistics stats, int count) {
System.out.println(count+":: Name="+emp.getName()+", Zipcode="+emp.getAddress().getZipcode());
printStats(stats, count);
}
}
org.hibernate.stat.Statistics
מספק את הסטטיסטיקות של Hibernate SessionFactory, אנו משתמשים בו כדי להדפיס את מספרי האחזור והפגיעה במטמון השלב השני. הסטטיסטיקות מושבתות כברירת מחדל לשדרוג ביצועים טובים יותר, ולכן אני מפעיל אותן בתחילת התוכנית. כאשר אנו מפעילים את התוכנית למעלה, אנו מקבלים המון פלט שנוצר על ידי Hibernate ו-EHCache APIs, אך אנו מתעניינים בנתונים שאנו מדפיסים. בהרצת דוגמה קטנה נדפיס את הפלט הבא.
Temp Dir:/var/folders/h4/q73jjy0902g51wkw0w69c0600000gn/T/
Hibernate Configuration loaded
Hibernate serviceRegistry created
Stats enabled=false
Stats enabled=true
***** 0 *****
Fetch Count=0
Second Level Hit Count=0
Second Level Miss Count=0
Second Level Put Count=0
Hibernate: select employee0_.emp_id as emp_id1_1_0_, employee0_.emp_name as emp_name2_1_0_, employee0_.emp_salary as emp_sala3_1_0_, address1_.emp_id as emp_id1_0_1_, address1_.address_line1 as address_2_0_1_, address1_.city as city3_0_1_, address1_.zipcode as zipcode4_0_1_ from EMPLOYEE employee0_ left outer join ADDRESS address1_ on employee0_.emp_id=address1_.emp_id where employee0_.emp_id=?
1:: Name=Pankaj, Zipcode=95129
***** 1 *****
Fetch Count=1
Second Level Hit Count=0
Second Level Miss Count=1
Second Level Put Count=2
2:: Name=Pankaj, Zipcode=95129
***** 2 *****
Fetch Count=1
Second Level Hit Count=0
Second Level Miss Count=1
Second Level Put Count=2
3:: Name=Pankaj, Zipcode=95129
***** 3 *****
Fetch Count=1
Second Level Hit Count=2
Second Level Miss Count=1
Second Level Put Count=2
Hibernate: select employee0_.emp_id as emp_id1_1_0_, employee0_.emp_name as emp_name2_1_0_, employee0_.emp_salary as emp_sala3_1_0_, address1_.emp_id as emp_id1_0_1_, address1_.address_line1 as address_2_0_1_, address1_.city as city3_0_1_, address1_.zipcode as zipcode4_0_1_ from EMPLOYEE employee0_ left outer join ADDRESS address1_ on employee0_.emp_id=address1_.emp_id where employee0_.emp_id=?
4:: Name=Lisa, Zipcode=560100
***** 4 *****
Fetch Count=2
Second Level Hit Count=2
Second Level Miss Count=2
Second Level Put Count=4
5:: Name=Pankaj, Zipcode=95129
***** 5 *****
Fetch Count=2
Second Level Hit Count=4
Second Level Miss Count=2
Second Level Put Count=4
כפי שניתן לראות מהפלט, הסטטיסטיקות היו מושבתות בתחילה אך אנו הפעלנו אותן כדי לבדוק את המטמון השני של Hibernate שלנו. הסבר שלב אחר שלב של הפלט הוא כדלקמן:
- לפני שאנו טוענים נתונים באפליקציה שלנו, כל הסטטיסטיקות הן 0 כפי שצפוי.
- כאשר אנו טוענים את העובד עם המזהה 1 לפעם הראשונה, יש חיפוש ראשוני במטמון השלב הראשון ואז במטמון השני. אם לא נמצא במטמון, הוא מבצע שאילתת מסד נתונים ולכן מספר האחזור יהיה 1. לאחר שהאובייקט נטען, הוא נשמר במטמון השלב הראשון ובמטמון השני גם. לכן, מספר הפגיעה בשלב השני נשמר על 0 ומספר החסרון הוא 1. שים לב שמספר השמירה הוא 2, זה בגלל שאובייקט העובד כולל גם כתובת, ולכן שני האובייקטים נשמרים במטמון השני והמספר עולה ל-2.
- בשלב הבא, אנו שוב טוענים את העובד עם המזהה 1, הפעם הוא נמצא במטמון השלב הראשון. לכן, אתה לא רואה שאילתת מסד נתונים וכל הסטטיסטיקות של המטמון השני נשמרות גם כן.
- עכשיו אנו משתמשים בשיטה `evict()` כדי להסיר את אובייקט העובד ממטמון הזיכרון הראשי. כעת, כאשר אנו מנסים לטעון אותו מחדש, היברנייט מוצא אותו במטמון זיכרון השני. לכן אין שאילתת מסד נתונים שנשלחת וסופר כמות הטעינות נשמרת 1. שימו לב שספירת הפגיעות עולה מ-0 ל-2 מכיוון שכמעט כל הפעולות מתבצעות מהמטמון זיכרון השני. פעולות החמיצה וההוספה נשמרות על הערך הקודם שלהן.
- כעת אנו טוענים עובד עם מזהה 3, שאילתת מסד נתונים מתבצעת ומספר הטעינות עולה ל-2, ספירת החמיצות עולה מ-1 ל-2 וספירת ההוספות עולה מ-2 ל-4.
- בשלב הבא אנו מנסים לטעון עובד עם מזהה 1 בסשן אחר, מאחר ומטמון הזיכרון השני של היברנייט משותף בין סשנים, המידע נמצא במטמון ולא מתבצעת שאילתת מסד נתונים. ספירת הטעינות, החמיצות וההוספות נשמרות בערך הקודם שלהן, אך ספירת הפגיעות עולה מ-2 ל-4.
כך שברור שהמטמון השני של היברנייט, הנקרא גם "Hibernate EHCache", פועל בצורה נכונה. נתוני הסטטיסטיקה של היברנייט מסייעים לזהות נקודות פגיעה במערכת ולייעל אותן כך שניתן יהיה להפחית את מספר הטעינות מהמטמון ולטעון יותר מידע מהמטמון. זהו כל מה שיש לנו לספק עבור דוגמת "Hibernate EHCache", אני מקווה שזה יסייע לך בהגדרת EHCache ביישומי Hibernate שלך ובשיפור הביצועים דרך מטמון השני של Hibernate. תוכל להוריד את הפרויקט הדוגמא מהקישור למטה ולהשתמש בנתוני הסטטיסטיקה הנוספים כדי ללמוד עוד.
Source:
https://www.digitalocean.com/community/tutorials/hibernate-ehcache-hibernate-second-level-cache