आज हम चौथे जेआरयू मॉड्यूल पर फाइनल प्रोजेक्ट करेंगे। यह क्या हो जाएगा? आइए विभिन्न तकनीकों के साथ काम करने का प्रयास करें: MySQL, Hibernate, Redis, Docker। अब और विषय।

कार्य: हमारे पास स्कीमा (देश-शहर, देश द्वारा भाषा) के साथ एक संबंधपरक MySQL डेटाबेस है। और शहर का बार-बार अनुरोध है, जो धीमा हो जाता है। हम एक समाधान के साथ आए - रेडिस के लिए अक्सर अनुरोध किए जाने वाले सभी डेटा को स्थानांतरित करने के लिए (की-वैल्यू प्रकार के मेमोरी स्टोरेज में)।

और हमें MySQL में संग्रहीत सभी डेटा की आवश्यकता नहीं है, लेकिन केवल फ़ील्ड्स का एक चयनित सेट है। प्रोजेक्ट एक ट्यूटोरियल के रूप में होगा। यानी यहां हम समस्या को उठाएंगे और तुरंत उसका समाधान करेंगे।

तो, चलिए शुरू करते हैं कि हमें किस सॉफ्टवेयर की आवश्यकता होगी:

  1. आईडिया अल्टीमेट (जिनके पास चाबी खत्म हो गई - रोमन को स्लैक में लिखें)
  2. वर्कबेंच (या MySQL के लिए कोई अन्य क्लाइंट)
  3. डाक में काम करनेवाला मज़दूर
  4. redis-अंतर्दृष्टि - वैकल्पिक

हमारी कार्य योजना:

  1. डॉकर सेट अप करें (मैंने ट्यूटोरियल में ऐसा नहीं किया है, क्योंकि प्रत्येक ओएस की अपनी विशेषताएं होंगी और इंटरनेट पर "विंडोज़ पर डॉकर कैसे इंस्टॉल करें" जैसे सवालों के बहुत सारे जवाब हैं), जांचें कि सब कुछ काम करता है।
  2. MySQL सर्वर को डॉकटर कंटेनर के रूप में चलाएं।
  3. डंप का विस्तार करें ।
  4. आइडिया में एक प्रोजेक्ट बनाएं, मावेन निर्भरताएं जोड़ें।
  5. लेयर डोमेन बनाएं।
  6. MySQL से सभी डेटा प्राप्त करने के लिए एक विधि लिखें।
  7. एक डेटा ट्रांसफ़ॉर्मेशन मेथड लिखें (रेडिस में हम केवल वही डेटा लिखेंगे जो बार-बार अनुरोध किया जाता है)।
  8. रेडिस सर्वर को डॉकटर कंटेनर के रूप में चलाएं।
  9. रेडिस को डेटा लिखें।
  10. वैकल्पिक: रेडिस-इनसाइट स्थापित करें, रेडिस में संग्रहीत डेटा को देखें।
  11. रेडिस से डाटा प्राप्त करने की विधि लिखिए।
  12. MySQL से डाटा प्राप्त करने की विधि लिखिए।
  13. MySQL और Redis से समान डेटा प्राप्त करने की गति की तुलना करें।

डॉकर सेटअप

डॉकर अनुप्रयोगों के विकास, वितरण और संचालन के लिए एक खुला मंच है। हम इसका उपयोग स्थानीय मशीन पर रेडिस को स्थापित और कॉन्फ़िगर करने के लिए नहीं, बल्कि तैयार छवि का उपयोग करने के लिए करेंगे। आप यहां डॉकर के बारे में अधिक पढ़ सकते हैं या इसे यहां देख सकते हैं । यदि आप डॉकटर से परिचित नहीं हैं, तो मैं सिर्फ दूसरे लिंक को देखने की सलाह देता हूं।

यह सुनिश्चित करने के लिए कि आपके पास डॉकर स्थापित और कॉन्फ़िगर किया गया है, कमांड चलाएँ:docker -v

यदि सब कुछ ठीक रहा, तो आप डॉकर संस्करण देखेंगे

MySQL सर्वर को डॉकर कंटेनर के रूप में चलाएं

MySQL और Redis से डेटा लौटाने के समय की तुलना करने में सक्षम होने के लिए, हम docker में MySQL का भी उपयोग करेंगे। PowerShell में (या अन्य कंसोल टर्मिनल यदि आप Windows का उपयोग नहीं कर रहे हैं), कमांड चलाएँ:

docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8 

विचार करें कि हम इस आदेश के साथ क्या कर रहे हैं:

  • docker run- छवि को लॉन्च करना (और डाउनलोड करना, अगर यह अभी तक स्थानीय मशीन पर डाउनलोड नहीं किया गया है)। लॉन्च के परिणामस्वरूप, हमें एक रनिंग कंटेनर मिलता है।
  • --name mysql- MySQL कंटेनर का नाम सेट करें।
  • -d- एक झंडा जो कहता है कि कंटेनर को काम करना जारी रखना चाहिए, भले ही आप उस टर्मिनल विंडो को बंद कर दें जिससे यह कंटेनर लॉन्च किया गया था।
  • -p 3306:3306- बंदरगाहों को निर्दिष्ट करता है। कोलन से पहले - स्थानीय मशीन पर पोर्ट, कोलन के बाद - कंटेनर में पोर्ट।
  • -e MYSQL_ROOT_PASSWORD=root- पर्यावरण चर MYSQL_ROOT_PASSWORD को मान रूट के साथ कंटेनर में पास करना। Mysql/ छवि के लिए विशिष्ट ध्वज
  • --restart unless-stopped- व्यवहार नीति निर्धारित करना (क्या बंद होने पर कंटेनर को फिर से चालू किया जाना चाहिए)। जब तक रुके हुए मूल्य का मतलब हमेशा पुनरारंभ करना है, सिवाय इसके कि जब कंटेनर को रोका गया हो /
  • -v mysql:/var/lib/mysql - वॉल्यूम जोड़ें (जानकारी संग्रहीत करने के लिए छवि)।
  • mysql:8 - छवि का नाम और उसका संस्करण।

टर्मिनल में कमांड निष्पादित करने के बाद, डॉकर छवि की सभी परतों को डाउनलोड करेगा और कंटेनर शुरू करेगा:

महत्वपूर्ण नोट: यदि आपने MySQL को अपने स्थानीय कंप्यूटर पर एक सेवा के रूप में स्थापित किया है और यह चल रहा है, तो आपको स्टार्ट कमांड में एक अलग पोर्ट निर्दिष्ट करना होगा, या इस चल रही सेवा को रोकना होगा।

डंप का विस्तार करें

डंप का विस्तार करने के लिए, आपको कार्यक्षेत्र से डेटाबेस के लिए एक नया कनेक्शन बनाना होगा, जहां आप पैरामीटर निर्दिष्ट करते हैं। मैंने डिफ़ॉल्ट पोर्ट (3306) का उपयोग किया, उपयोगकर्ता नाम (डिफ़ॉल्ट रूप से रूट) नहीं बदला, और रूट उपयोगकर्ता (रूट) के लिए पासवर्ड सेट किया।

कार्यक्षेत्र में, करें Data Import/Restoreऔर चुनें Import from Self Contained Fileनिर्दिष्ट करें कि आपने डंप को फ़ाइल के रूप में कहां डाउनलोड किया है । आपको स्कीमा पहले से बनाने की आवश्यकता नहीं है - इसका निर्माण डंप फ़ाइल में शामिल है। एक सफल आयात के बाद, आपके पास तीन तालिकाओं वाला एक विश्व स्कीमा होगा:

  1. शहर शहरों की एक तालिका है।
  2. देश - देश तालिका।
  3. देश_भाषा - एक तालिका जो इंगित करती है कि देश में कितने प्रतिशत जनसंख्या एक विशेष भाषा बोलती है।

चूंकि हमने कंटेनर शुरू करते समय वॉल्यूम का उपयोग किया था, रुकने के बाद और यहां तक ​​कि MySQL कंटेनर को हटाने और स्टार्ट कमांड ( docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8) को फिर से निष्पादित करने के बाद, डंप को फिर से तैनात करने की कोई आवश्यकता नहीं होगी - यह पहले से ही वॉल्यूम में तैनात है।

आइडिया में प्रोजेक्ट बनाएं, मावेन निर्भरताएं जोड़ें

आप पहले से ही जानते हैं कि आइडिया में प्रोजेक्ट कैसे बनाया जाता है - यह आज के प्रोजेक्ट का सबसे आसान बिंदु है।

पोम फ़ाइल में निर्भरताएँ जोड़ें:


<dependencies> 
   <dependency> 
      <groupId>mysql</groupId> 
      <artifactId>mysql-connector-java</artifactId> 
      <version>8.0.30</version> 
   </dependency> 
 
   <dependency> 
      <groupId>org.hibernate</groupId> 
      <artifactId>hibernate-core-jakarta</artifactId> 
      <version>5.6.14.Final</version> 
   </dependency> 
 
   <dependency> 
      <groupId>p6spy</groupId> 
      <artifactId>p6spy</artifactId> 
      <version>3.9.1</version> 
   </dependency> 
    
   <dependency> 
      <groupId>io.lettuce</groupId> 
      <artifactId>lettuce-core</artifactId> 
      <version>6.2.2.RELEASE</version> 
   </dependency> 
 
   <dependency> 
      <groupId>com.fasterxml.jackson.core</groupId> 
      <artifactId>jackson-databind</artifactId> 
      <version>2.14.0</version> 
   </dependency> 
</dependencies> 

आप पहले तीन निर्भरताओं से लंबे समय से परिचित हैं।

lettuce-coreरेडिस के साथ काम करने के लिए उपलब्ध जावा क्लाइंट्स में से एक है।

jackson-databind- ObjectMapper का उपयोग करने के लिए निर्भरता (रेडिस में भंडारण के लिए डेटा बदलने के लिए (स्ट्रिंग प्रकार का कुंजी-मान))।

इसके अलावा संसाधन फ़ोल्डर (src/main/resources) में हाइबरनेट निष्पादित पैरामीटर वाले अनुरोधों को देखने के लिए spy.properties जोड़ें। फ़ाइल सामग्री:

driverlist=com.mysql.cj.jdbc.Driver 
dateformat=yyyy-MM-dd hh:mm:ss a 
appender=com.p6spy.engine.spy.appender.StdoutLogger 
logMessageFormat=com.p6spy.engine.spy.appender.MultiLineFormat 

लेयर डोमेन बनाएं

पैकेज com.codegym.domain बनाएँ

आइडिया में टेबल स्ट्रक्चर का उपयोग करने के लिए किसी इकाई पर टेबल मैप करते समय यह मेरे लिए सुविधाजनक है, तो चलिए आइडिया में डेटाबेस कनेक्शन जोड़ते हैं।

मैं इस क्रम में संस्थाएँ बनाने का सुझाव देता हूँ:

  • देश
  • शहर
  • देश की भाषा

यह वांछनीय है कि आप स्वयं मैपिंग करें।

देश वर्ग कोड:

package com.codegym.domain;

import jakarta.persistence.*;

import java.math.BigDecimal;
import java.util.Set;

@Entity
@Table(schema = "world", name = "country")
public class Country {
    @Id
    @Column(name = "id")
    private Integer id;

    private String code;

    @Column(name = "code_2")
    private String alternativeCode;

    private String name;

    @Column(name = "continent")
    @Enumerated(EnumType.ORDINAL)
    private Continent continent;

    private String region;

    @Column(name = "surface_area")
    private BigDecimal surfaceArea;

    @Column(name = "indep_year")
    private Short independenceYear;

    private Integer population;

    @Column(name = "life_expectancy")
    private BigDecimal lifeExpectancy;

    @Column(name = "gnp")
    private BigDecimal GNP;

    @Column(name = "gnpo_id")
    private BigDecimal GNPOId;

    @Column(name = "local_name")
    private String localName;

    @Column(name = "government_form")
    private String governmentForm;

    @Column(name = "head_of_state")
    private String headOfState;

    @OneToOne
    @JoinColumn(name = "capital")
    private City city;

    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "country_id")
    private Set<CountryLanguage> languages;


    //Getters and Setters omitted

}

कोड में 3 दिलचस्प बिंदु हैं।

पहला कॉन्टिनेंट एनाम है , जो डेटाबेस में क्रमिक मूल्यों के रूप में संग्रहीत है। देश तालिका की संरचना में, महाद्वीप क्षेत्र की टिप्पणियों में, आप देख सकते हैं कि कौन सा संख्यात्मक मान किस महाद्वीप से मेल खाता है।

package com.codegym.domain;

public enum Continent {
    ASIA,
    EUROPE,
    NORTH_AMERICA,
    AFRICA,
    OCEANIA,
    ANTARCTICA,
    SOUTH_AMERICA
}

दूसरा बिंदु संस्थाओं का एक समूह हैCountryLanguage । यहाँ एक कड़ी है @OneToManyजो इस मॉड्यूल के दूसरे मसौदे में नहीं थी। डिफ़ॉल्ट रूप से, देश इकाई का अनुरोध करते समय हाइबरनेट इस सेट के मान को नहीं खींचेगा। लेकिन चूंकि हमें कैशिंग के लिए रिलेशनल डेटाबेस से सभी मूल्यों को घटाना है, इसलिए FetchType.EAGER.

तीसरा शहर का मैदान है । संचार @OneToOne- जैसे सब कुछ परिचित और समझने योग्य है। लेकिन, यदि हम डेटाबेस में विदेशी कुंजी संरचना को देखते हैं, तो हम देखते हैं कि देश (देश) का राजधानी (शहर) से लिंक है, और शहर (शहर) का देश (देश) से लिंक है। चक्रीय सम्बन्ध होता है।

हम इसके साथ अभी तक कुछ नहीं करेंगे, लेकिन जब हम "MySQL से सभी डेटा प्राप्त करने के लिए एक विधि लिखें" आइटम प्राप्त करते हैं, तो देखते हैं कि हाइबरनेट क्या प्रश्न निष्पादित करता है, उनकी संख्या देखें और इस आइटम को याद रखें।

शहर वर्ग कोड:

package com.codegym.domain;

import jakarta.persistence.*;

@Entity
@Table(schema = "world", name = "city")
public class City {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "country_id")
    private Country country;

    private String district;

    private Integer population;


    //Getters and Setters omitted

}

देशभाषा वर्ग कोड:

package com.codegym.domain;

import jakarta.persistence.*;
import org.hibernate.annotations.Type;

import java.math.BigDecimal;

@Entity
@Table(schema = "world", name = "country_language")
public class CountryLanguage {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @ManyToOne
    @JoinColumn(name = "country_id")
    private Country country;

    private String language;

    @Column(name = "is_official", columnDefinition = "BIT")
    @Type(type = "org.hibernate.type.NumericBooleanType")
    private Boolean isOfficial;

    private BigDecimal percentage;


    //Getters and Setters omitted
}

MySQL से सभी डेटा प्राप्त करने के लिए एक विधि लिखें

मुख्य वर्ग में, हम फ़ील्ड घोषित करते हैं:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

और उन्हें मुख्य वर्ग के निर्माता में प्रारंभ करें:

public Main() {
    sessionFactory = prepareRelationalDb();
    cityDAO = new CityDAO(sessionFactory);
    countryDAO = new CountryDAO(sessionFactory);

    redisClient = prepareRedisClient();
    mapper = new ObjectMapper();
}

जैसा कि आप देख सकते हैं, पर्याप्त विधियाँ और कक्षाएं नहीं हैं - आइए उन्हें लिखें।

एक पैकेज com.codegym.dao घोषित करें और इसमें 2 वर्ग जोड़ें:

package com.codegym.dao;

import com.codegym.domain.Country;
import org.hibernate.SessionFactory;
import org.hibernate.query.Query;

import java.util.List;

public class CountryDAO {
    private final SessionFactory sessionFactory;

    public CountryDAO(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public List<Country> getAll() {
        Query<Country> query = sessionFactory.getCurrentSession().createQuery("select c from Country c", Country.class);
        return query.list();
    }
}
package com.codegym.dao;

import com.codegym.domain.City;
import org.hibernate.SessionFactory;
import org.hibernate.query.Query;

import java.util.List;

public class CityDAO {
    private final SessionFactory sessionFactory;

    public CityDAO(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public List<City> getItems(int offset, int limit) {
        Query<City> query = sessionFactory.getCurrentSession().createQuery("select c from City c", City.class);
        query.setFirstResult(offset);
        query.setMaxResults(limit);
        return query.list();
    }

    public int getTotalCount() {
        Query<Long> query = sessionFactory.getCurrentSession().createQuery("select count(c) from City c", Long.class);
        return Math.toIntExact(query.uniqueResult());
    }
}

अब आप इन 2 वर्गों को मेन में इम्पोर्ट कर सकते हैं। अभी भी दो तरीके याद आ रहे हैं:

private SessionFactory prepareRelationalDb() {
    final SessionFactory sessionFactory;
    Properties properties = new Properties();
    properties.put(Environment.DIALECT, "org.hibernate.dialect.MySQL8Dialect");
    properties.put(Environment.DRIVER, "com.p6spy.engine.spy.P6SpyDriver");
    properties.put(Environment.URL, "jdbc:p6spy:mysql://localhost:3306/world");
    properties.put(Environment.USER, "root");
    properties.put(Environment.PASS, "root");
    properties.put(Environment.CURRENT_SESSION_CONTEXT_CLASS, "thread");
    properties.put(Environment.HBM2DDL_AUTO, "validate");
    properties.put(Environment.STATEMENT_BATCH_SIZE, "100");

    sessionFactory = new Configuration()
            .addAnnotatedClass(City.class)
            .addAnnotatedClass(Country.class)
            .addAnnotatedClass(CountryLanguage.class)
            .addProperties(properties)
            .buildSessionFactory();
    return sessionFactory;
}

हम अभी तक मूली तक नहीं पहुंचे हैं, इसलिए मूली क्लाइंट के इनिशियलाइज़ेशन का कार्यान्वयन अभी के लिए एक आधार बना रहेगा:

private void shutdown() {
    if (nonNull(sessionFactory)) {
        sessionFactory.close();
    }
    if (nonNull(redisClient)) {
        redisClient.shutdown();
    }
}

अंत में, हम एक विधि लिख सकते हैं जिसमें हम सभी शहरों को बाहर निकालते हैं:

private List<City> fetchData(Main main) {
    try (Session session = main.sessionFactory.getCurrentSession()) {
        List<City> allCities = new ArrayList<>();
        session.beginTransaction();

        int totalCount = main.cityDAO.getTotalCount();
        int step = 500;
        for (int i = 0; i < totalCount; i += step) {
            allCities.addAll(main.cityDAO.getItems(i, step));
        }
        session.getTransaction().commit();
        return allCities;
    }
}

कार्यान्वयन सुविधा ऐसी है कि हमें प्रत्येक में 500 शहर मिलते हैं। यह आवश्यक है क्योंकि प्रेषित डेटा की मात्रा पर प्रतिबंध हैं। हां, हमारे मामले में हम उनसे नहीं मिलेंगे, क्योंकि। हमारे पास डेटाबेस में कुल 4079 शहर हैं। लेकिन उत्पादन अनुप्रयोगों में, जब आपको बहुत अधिक डेटा प्राप्त करने की आवश्यकता होती है, तो अक्सर इस तकनीक का उपयोग किया जाता है।

और मुख्य विधि का कार्यान्वयन:

public static void main(String[] args) {
    Main main = new Main();
    List<City> allCities = main.fetchData(main);
    main.shutdown();
}

अब हम अपने एप्लिकेशन को पहली बार डिबग में चला सकते हैं और देख सकते हैं कि यह कैसे काम करता है (या काम नहीं करता - हाँ, यह अक्सर होता है)।

शहर मिल रहे हैं। प्रत्येक शहर को एक देश मिलता है, अगर इसे पहले किसी अन्य शहर के डेटाबेस से घटाया नहीं गया है। आइए मोटे तौर पर गणना करें कि हाइबरनेट डेटाबेस को कितने प्रश्न भेजेगा:

  • शहरों की कुल संख्या का पता लगाने के लिए 1 अनुरोध (कब रुकना है यह जानने के लिए 500 से अधिक शहरों को दोहराने की आवश्यकता है)।
  • 4079/500 = 9 अनुरोध (शहरों की सूची)।
  • प्रत्येक शहर को एक देश मिलता है, अगर इसे पहले घटाया नहीं गया है। चूंकि डेटाबेस में 239 देश हैं, इससे हमें 239 प्रश्न मिलेंगे।

Total 249 requests. और हमने यह भी कहा कि देश के साथ हमें तुरंत भाषाओं का एक समूह प्राप्त होगा, अन्यथा सामान्य रूप से अंधेरा हो जाएगा। लेकिन यह अभी भी बहुत कुछ है, तो आइए व्यवहार को थोड़ा सुधारें। आइए प्रतिबिंबों से शुरू करें: क्या करें, कहां दौड़ें? लेकिन गंभीरता से - इतने अनुरोध क्यों हैं। यदि आप अनुरोध लॉग को देखते हैं, तो हम देखते हैं कि प्रत्येक देश से अलग से अनुरोध किया गया है, इसलिए पहला सरल उपाय: आइए सभी देशों से एक साथ अनुरोध करें, क्योंकि हम पहले से जानते हैं कि इस लेनदेन में हमें उन सभी की आवश्यकता होगी।

FetchData () विधि में, लेन-देन की शुरुआत के तुरंत बाद, निम्न पंक्ति जोड़ें:

List<Country> countries = main.countryDAO.getAll(); 

हम अनुरोधों की गणना करते हैं:

  • 1 - सभी देशों को प्राप्त करें
  • 239 - इसकी राजधानी के प्रत्येक देश के लिए क्वेरी
  • 1 - शहरों की संख्या के लिए अनुरोध
  • 9 - शहरों की सूची के लिए अनुरोध

Total 250. आइडिया अच्छा है, लेकिन काम नहीं आया। समस्या यह है कि देश का संबंध राजधानी (शहर) से है @OneToOne। और ऐसा लिंक डिफ़ॉल्ट रूप से तुरंत लोड हो जाता है ( FetchType.EAGER)। चलो डाल दिया FetchType.LAZY, क्योंकि वैसे भी, हम सभी शहरों को बाद में उसी लेन-देन में लोड करेंगे।

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "capital")
private City city;

राजधानियों का अब अलग से अनुरोध नहीं किया जाता है, लेकिन अनुरोधों की संख्या नहीं बदली है। अब, प्रत्येक देश के लिए, एक अलग क्वेरी द्वारा देशभाषा सूची का अनुरोध किया जाता है । यानी प्रगति हो रही है और हम सही दिशा में बढ़ रहे हैं। यदि आपको याद है, तो व्याख्यान अनुरोध में एक अतिरिक्त जुड़ाव जोड़कर एक अनुरोध में निर्भर डेटा के साथ एक इकाई का अनुरोध करने के लिए "ज्वाइन फ़ेच" समाधान का सुझाव देते हैं। कंट्रीडीएओ में , एचक्यूएल क्वेरी getAll()को विधि में फिर से लिखें:

"select c from Country c join fetch c.languages" 

शुरू करना। हम लॉग देखते हैं, अनुरोधों की गणना करते हैं:

  • 1 - भाषाओं वाले सभी देश
  • 1 - शहरों की संख्या
  • 9 - शहरों की सूची।

Total 11- हम सफल हुए)) यदि आपने न केवल इस पूरे पाठ को पढ़ा, बल्कि एप्लिकेशन को ट्यून करने के प्रत्येक चरण के बाद इसे चलाने का भी प्रयास किया, तो आपको कई बार पूरे एप्लिकेशन के त्वरण पर भी ध्यान देना चाहिए।

डेटा रूपांतरण विधि लिखिए

चलिए एक पैकेज बनाते हैं com.codegym.redisजिसमें हम 2 वर्ग जोड़ते हैं: CityCountry (शहर और देश का डेटा जिसमें यह शहर स्थित है) और भाषा (भाषा पर डेटा)। यहां वे सभी फ़ील्ड हैं जिन्हें अक्सर "ब्रेकिंग अनुरोध" में "कार्य द्वारा" अनुरोध किया जाता है।

package com.codegym.redis;

import com.codegym.domain.Continent;

import java.math.BigDecimal;
import java.util.Set;

public class CityCountry {
    private Integer id;

    private String name;

    private String district;

    private Integer population;

    private String countryCode;

    private String alternativeCountryCode;

    private String countryName;

    private Continent continent;

    private String countryRegion;

    private BigDecimal countrySurfaceArea;

    private Integer countryPopulation;

    private Set<Language> languages;

    //Getters and Setters omitted
}
package com.codegym.redis;

import java.math.BigDecimal;

public class Language {
    private String language;
    private Boolean isOfficial;
    private BigDecimal percentage;

    //Getters and Setters omitted
}

मुख्य विधि में सभी नगरों को प्राप्त करने के बाद रेखा जोड़ें

List<CityCountry>> preparedData = main.transformData(allCities); 

और इस विधि को लागू करें:

private List<CityCountry> transformData(List<City> cities) {
    return cities.stream().map(city -> {
        CityCountry res = new CityCountry();
        res.setId(city.getId());
        res.setName(city.getName());
        res.setPopulation(city.getPopulation());
        res.setDistrict(city.getDistrict());

        Country country = city.getCountry();
        res.setAlternativeCountryCode(country.getAlternativeCode());
        res.setContinent(country.getContinent());
        res.setCountryCode(country.getCode());
        res.setCountryName(country.getName());
        res.setCountryPopulation(country.getPopulation());
        res.setCountryRegion(country.getRegion());
        res.setCountrySurfaceArea(country.getSurfaceArea());
        Set<CountryLanguage> countryLanguages = country.getLanguages();
        Set<Language> languages = countryLanguages.stream().map(cl -> {
            Language language = new Language();
            language.setLanguage(cl.getLanguage());
            language.setOfficial(cl.getOfficial());
            language.setPercentage(cl.getPercentage());
            return language;
        }).collect(Collectors.toSet());
        res.setLanguages(languages);

        return res;
    }).collect(Collectors.toList());
}

मुझे लगता है कि यह तरीका आत्म-व्याख्यात्मक है: हम सिर्फ एक CityCountry इकाई बनाते हैं और इसे City , Country , CountryLanguage के डेटा से भरते हैं ।

रेडिस सर्वर को डॉकटर कंटेनर के रूप में चलाएं

यहाँ 2 विकल्प हैं। यदि आप वैकल्पिक कदम "रेडिस-इनसाइट स्थापित करें, रेडिस में संग्रहीत डेटा देखें", तो कमांड आपके लिए है:

docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest 

यदि आप इस चरण को छोड़ने का निर्णय लेते हैं, तो बस:

docker run -d --name redis -p 6379:6379 redis:latest 

अंतर यह है कि पहले विकल्प में, पोर्ट 8001 को स्थानीय मशीन पर भेज दिया जाता है, जिससे आप बाहरी क्लाइंट के साथ जुड़कर देख सकते हैं कि अंदर क्या संग्रहीत है। और मैं अर्थपूर्ण नाम देता था, इसलिए, redis-stackया redis.

लॉन्च के बाद, आप चल रहे कंटेनरों की सूची देख सकते हैं। ऐसा करने के लिए, कमांड चलाएँ:

docker container ls 

और आपको कुछ ऐसा दिखाई देगा:

यदि आपको कुछ आदेश खोजने की आवश्यकता है, तो आप या तो टर्मिनल में मदद देख सकते हैं (docker help) या google "कैसे करें ..." (उदाहरण के लिए, docker How to remove running कंटेनर)।

और हमने मुख्य कंस्ट्रक्टर में मूली क्लाइंट के इनिशियलाइज़ेशन को भी कहा, लेकिन विधि को ही लागू नहीं किया। कार्यान्वयन जोड़ें:

private RedisClient prepareRedisClient() {
    RedisClient redisClient = RedisClient.create(RedisURI.create("localhost", 6379));
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
        System.out.println("\nConnected to Redis\n");
    }
    return redisClient;
}

शैक्षिक उद्देश्यों के लिए साउथ जोड़ा गया था ताकि लॉन्च लॉग में आप देख सकें कि सब कुछ ठीक है और मूली क्लाइंट के माध्यम से कनेक्शन बिना त्रुटियों के पारित हो गया।

रेडिस को डेटा लिखें

मुख्य विधि में कॉल जोड़ें

main.pushToRedis(preparedData); 

इस पद्धति के कार्यान्वयन के साथ:

private void pushToRedis(List<CityCountry> data) {
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
        RedisStringCommands<String, String> sync = connection.sync();
        for (CityCountry cityCountry : data) {
            try {
                sync.set(String.valueOf(cityCountry.getId()), mapper.writeValueAsString(cityCountry));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }

    }
}

यहां, मूली क्लाइंट के साथ एक तुल्यकालिक कनेक्शन खोला जाता है, और क्रमिक रूप से सिटीकंट्री प्रकार की प्रत्येक वस्तु मूली को लिखी जाती है। चूंकि मूली एक स्ट्रिंग की-वैल्यू स्टोर है , कुंजी (शहर आईडी) एक स्ट्रिंग में परिवर्तित हो जाती है। और मान स्ट्रिंग के लिए भी है, लेकिन JSON प्रारूप में ObjectMapper का उपयोग करना।

यह चलना और जांचना बाकी है कि लॉग में कोई त्रुटि नहीं है। सब कुछ काम कर गया।

रेडिस-इनसाइट स्थापित करें, रेडिस में संग्रहीत डेटा देखें (वैकल्पिक)

लिंक से रेडिस-इनसाइट डाउनलोड करें और इसे इंस्टॉल करें। शुरू करने के बाद, यह तुरंत डॉकटर कंटेनर में हमारे मूली का उदाहरण दिखाता है:

यदि आप लॉग इन करते हैं, तो हम सभी चाबियों की एक सूची देखेंगे:

और आप यह देखने के लिए किसी भी कुंजी पर जा सकते हैं कि उस पर कौन सा डेटा संग्रहीत है:

रेडिस से डाटा प्राप्त करने की विधि लिखिए

परीक्षण के लिए, हम निम्नलिखित परीक्षण का उपयोग करते हैं: हमें 10 सिटीकंट्री रिकॉर्ड मिलते हैं। प्रत्येक एक अलग अनुरोध के साथ, लेकिन एक संबंध में।

मूली से डेटा हमारे मूली ग्राहक के माध्यम से प्राप्त किया जा सकता है। ऐसा करने के लिए, आइए एक विधि लिखें जो प्राप्त करने के लिए आईडी की सूची लेती है।

private void testRedisData(List<Integer> ids) {
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
        RedisStringCommands<String, String> sync = connection.sync();
        for (Integer id : ids) {
            String value = sync.get(String.valueOf(id));
            try {
                mapper.readValue(value, CityCountry.class);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
    }
}

कार्यान्वयन, मुझे लगता है, सहज है: हम एक तुल्यकालिक कनेक्शन खोलते हैं, और प्रत्येक आईडी के लिए हमें एक JSON स्ट्रिंग मिलती है , जिसे हम सिटीकंट्री प्रकार की वस्तु में परिवर्तित करते हैं जिसकी हमें आवश्यकता होती है ।

MySQL से डाटा प्राप्त करने की विधि लिखिए

CityDAO वर्ग में , एक विधि जोड़ें getById(Integer id)जिसमें हम देश के साथ-साथ शहर भी प्राप्त करेंगे:

public City getById(Integer id) {
    Query<City> query = sessionFactory.getCurrentSession().createQuery("select c from City c join fetch c.country where c.id = :ID", City.class);
    query.setParameter("ID", id);
    return query.getSingleResult();
}

पिछले पैराग्राफ के अनुरूप, मुख्य वर्ग में MySQL के लिए एक समान विधि जोड़ें:

private void testMysqlData(List<Integer> ids) {
    try (Session session = sessionFactory.getCurrentSession()) {
        session.beginTransaction();
        for (Integer id : ids) {
            City city = cityDAO.getById(id);
            Set<CountryLanguage> languages = city.getCountry().getLanguages();
        }
        session.getTransaction().commit();
    }
}

सुविधाओं में से, पूर्ण वस्तु (प्रॉक्सी स्टब्स के बिना) सुनिश्चित करने के लिए, हम स्पष्ट रूप से देश से भाषाओं की सूची का अनुरोध करते हैं।

MySQL और Redis से समान डेटा प्राप्त करने की गति की तुलना करें

यहां मैं तुरंत मुख्य विधि का कोड और अपने स्थानीय कंप्यूटर पर प्राप्त परिणाम दूंगा।

public static void main(String[] args) {
    Main main = new Main();
    List<City> allCities = main.fetchData(main);
    List<CityCountry> preparedData = main.transformData(allCities);
    main.pushToRedis(preparedData);

    // close the current session in order to make a query to the database for sure, and not to pull data from the cache
    main.sessionFactory.getCurrentSession().close();

    //choose random 10 id cities
    //since we did not handle invalid situations, use the existing id in the database
    List<Integer> ids = List.of(3, 2545, 123, 4, 189, 89, 3458, 1189, 10, 102);

    long startRedis = System.currentTimeMillis();
    main.testRedisData(ids);
    long stopRedis = System.currentTimeMillis();

    long startMysql = System.currentTimeMillis();
    main.testMysqlData(ids);
    long stopMysql = System.currentTimeMillis();

    System.out.printf("%s:\t%d ms\n", "Redis", (stopRedis - startRedis));
    System.out.printf("%s:\t%d ms\n", "MySQL", (stopMysql - startMysql));

    main.shutdown();
}

परीक्षण करते समय, एक विशेषता है - MySQL से डेटा केवल पढ़ा जाता है, इसलिए इसे हमारे एप्लिकेशन के लॉन्च के बीच पुनः आरंभ नहीं किया जा सकता है। और रेडिस में वे लिखे गए हैं।

यद्यपि जब आप एक ही कुंजी के लिए एक डुप्लिकेट जोड़ने का प्रयास करते हैं, तो डेटा बस अपडेट हो जाएगा, मैं अनुशंसा करता हूं कि आप कंटेनर को रोकने के लिए आदेश चलाएं docker stop redis-stackऔर टर्मिनल में एप्लिकेशन लॉन्च के बीच कंटेनर को हटा दें docker rm redis-stack। उसके बाद, कंटेनर को मूली के साथ फिर से उठाएं docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latestऔर उसके बाद ही हमारे आवेदन को निष्पादित करें।

यहाँ मेरे परीक्षण के परिणाम हैं:

कुल मिलाकर, हमने "ब्रेकिंग फ़्रीक्वेंट" अनुरोध की प्रतिक्रिया के प्रदर्शन में डेढ़ गुना वृद्धि हासिल की है। और यह इस तथ्य को ध्यान में रख रहा है कि परीक्षण में हमने ObjectMapper के माध्यम से सबसे तेज़ डिसेरिएलाइज़ेशन का उपयोग नहीं किया । यदि आप इसे जीएसओएन में बदलते हैं, तो सबसे अधिक संभावना है कि आप थोड़ा और समय "जीत" सकते हैं।

इस समय, मुझे एक प्रोग्रामर और समय के बारे में एक चुटकुला याद है: अपने कोड को लिखने और अनुकूलित करने के बारे में पढ़ें और सोचें।