JavaRush /בלוג Java /Random-HE /הוספת מסד נתונים PostgreSQL לשירות RESTful ב-Spring Boot....
Artur
רָמָה
Tallinn

הוספת מסד נתונים PostgreSQL לשירות RESTful ב-Spring Boot. חלק 2

פורסם בקבוצה
הוספת מסד נתונים PostgreSQL לשירות RESTful ב-Spring Boot. חלק 1 הוספת מסד נתונים PostgreSQL לשירות RESTful ב-Spring Boot.  חלק 2 - 1 אז, בחלק האחרון למדנו איך להתקין מסד נתונים PostgresSQL במחשב, ליצור מסד נתונים ב-pgAdmin, וגם ליצור ולמחוק בו טבלאות באופן ידני ותכנותי. בחלק זה נשכתב את התוכנית שלנו כך שהיא תלמד לעבוד עם מסד הנתונים והטבלאות הללו. למה אנחנו? כי אני בעצמי לומד איתך מהחומר הזה. ואז לא רק נפתור את המשימה שלפנינו, אלא גם נתקן שגיאות שעולות תוך כדי תנועה, בעזרת עצות של מתכנתים מנוסים יותר. כביכול, נלמד לעבוד בצוות ;) ראשית, בואו ניצור com.javarush.lectures.rest_exampleחבילה חדשה בתיקייה ונקרא לה repository. בחבילה זו ניצור ממשק חדש ClientRepository:
package com.javarush.lectures.rest_example.repository;

import com.javarush.lectures.rest_example.model.Client;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ClientRepository extends JpaRepository<Client, Integer>  {
}
ממשק זה יקיים אינטראקציה "באופן קסם" עם מסדי הנתונים והטבלאות שלנו. למה בקסם? כי לא נצטרך לכתוב את היישום שלו, ומסגרת האביב תספק לנו אותו. אתה רק צריך ליצור ממשק כזה, ואתה כבר יכול להשתמש ב"קסם" הזה. השלב הבא הוא לערוך את המחלקה Clientכך:
package com.javarush.lectures.rest_example.model;

import javax.persistence.*;

@Entity
@Table(name = "clients")
public class Client {
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

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

    @Column(name = "email")
    private String email;

    @Column(name = "phone")
    private String phone;

    //… getters and setters
}
כל מה שעשינו בשיעור הזה היה רק ​​להוסיף כמה הערות. בואו נעבור עליהם:
  • @Entity - מציין ששעועית (מחלקה) זו היא ישות.
  • @טבלה - מציין את שם הטבלה שתוצג בישות זו.
  • @Id - column id (מפתח ראשי - הערך שישמש להבטחת ייחודיות הנתונים בטבלה הנוכחית. הערה: אנדריי )
  • @Column - מציין את שם העמודה הממופת למאפיין הישות.
  • @GeneratedValue - מציין שמאפיין זה ייווצר בהתאם לאסטרטגיה שצוינה.
שמות שדות הטבלה אינם חייבים להתאים לשמות המשתנים במחלקה. לדוגמה, אם יש לנו משתנה firstName, אז נעניק שם לשדה בטבלה first_name. ניתן להגדיר הערות אלה הן ישירות על השדות והן על ה-Gutters שלהם. אבל אם תבחר באחת מהשיטות הללו, נסה לשמור על סגנון זה לאורך כל התוכנית שלך. השתמשתי בשיטה הראשונה רק כדי לקצר את הרישומים. רשימה מלאה יותר של הערות לעבודה עם מסדי נתונים ניתן למצוא כאן . כעת נלך לכיתה ClientServiceImplונכתוב אותה מחדש באופן הבא:
package com.javarush.lectures.rest_example.service;

import com.javarush.lectures.rest_example.model.Client;
import com.javarush.lectures.rest_example.repository.ClientRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ClientServiceImpl implements ClientService {

    @Autowired
    private ClientRepository clientRepository;

    @Override
    public void create(Client client) {
        clientRepository.save(client);
    }

    @Override
    public List<Client>  readAll() {
        return clientRepository.findAll();
    }

    @Override
    public Client read(int id) {
        return clientRepository.getOne(id);
    }

    @Override
    public boolean update(Client client, int id) {
        if (clientRepository.existsById(id)) {
            client.setId(id);
            clientRepository.save(client);
            return true;
        }

        return false;
    }

    @Override
    public boolean delete(int id) {
        if (clientRepository.existsById(id)) {
            clientRepository.deleteById(id);
            return true;
        }
        return false;
    }
}
כפי שניתן לראות מהרשימה, כל מה שעשינו היה למחוק את השורות שכבר לא היינו צריכים:
// Хранorще клиентов
private static final Map<Integer, Client>  CLIENT_REPOSITORY_MAP = new HashMap<>();

// Переменная для генерации ID клиента
private static final AtomicInteger CLIENT_ID_HOLDER = new AtomicInteger();
הכרזנו במקום זאת על הממשק שלנו ClientRepositoryוגם הצבנו מעליו את ההערה @Autowired כך ש-Spring יוסיף אוטומטית את התלות הזו לכיתה שלנו. האצלנו גם את כל העבודה לממשק הזה, או ליתר דיוק את היישום שלו, ש-Spring יוסיף. נעבור לשלב האחרון והמעניין ביותר - בדיקת האפליקציה שלנו. בואו נפתח את תוכנית Postman (ראה כיצד להשתמש בה כאן ) ונשלח בקשת GET לכתובת זו: http://localhost:8080/clients. אנחנו מקבלים את התשובה הזו:
[
    {
        "id": 1,
        "name": "Vassily Petrov",
        "email": "vpetrov@jr.com",
        "phone": "+7 (191) 322-22-33)"
    },
    {
        "id": 2,
        "name": "Pjotr Vasechkin",
        "email": "pvasechkin@jr.com",
        "phone": "+7 (191) 223-33-22)"
    }
]
אנו שולחים בקשת POST:
{
  "name" : "Amigo",
  "email" : "amigo@jr.com",
  "phone" : "+7 (191) 746-43-23"
}
ו... אנחנו תופסים את הבאג הראשון שלנו בתוכנית:
{
    "timestamp": "2020-03-06T13:21:12.180+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement",
    "path": "/clients"
}
הוספת מסד נתונים PostgreSQL לשירות RESTful ב-Spring Boot.  חלק 2 - 2 אנו מסתכלים על היומנים ומוצאים את השגיאה הבאה:

org.postgresql.util.PSQLException: ОШИБКА: повторяющееся meaning ключа нарушает ограничение уникальности "clients_pkey"
  Detail: Ключ "(id)=(1)" уже существует.
אנחנו שולחים שוב את אותה בקשת POST, התוצאה זהה, אבל עם ההבדל הזה: Ключ "(id)=(2)" уже существует. אנחנו שולחים את אותה בקשה בפעם השלישית, ומקבלים סטטוס: 201 נוצר. אנו שולחים שוב את בקשת GET ומקבלים תשובה:
[
    {
        "id": 1,
        "name": "Vassily Petrov",
        "email": "vpetrov@jr.com",
        "phone": "+7 (191) 322-22-33)"
    },
    {
        "id": 2,
        "name": "Pjotr Vasechkin",
        "email": "pvasechkin@jr.com",
        "phone": "+7 (191) 223-33-22)"
    },
    {
        "id": 3,
        "name": "Amigo",
        "email": "amigo@jr.com",
        "phone": "+7 (191) 746-43-23"
    }
]
זה מצביע על כך שהתוכנית שלנו מתעלמת מהעובדה שהטבלה הזו כבר אוכלסה מראש והיא מקצה שוב מזהה החל מאחד. ובכן, באג הוא רגע עבודה, אל תתייאשו, זה קורה לעתים קרובות. לכן, אפנה לקולגות מנוסים יותר לעזרה: "עמיתים יקרים, אנא יעצו בהערות כיצד לתקן זאת כך שהתוכנית תעבוד כרגיל." העזרה לא לקחה הרבה זמן להגיע, וסטאס פסינקוב אמר לי בתגובות לאיזה כיוון אני צריך להסתכל. תודה מיוחדת לו על כך! אבל העניין היה שבשיעור Clientציינתי בצורה לא נכונה את האסטרטגיה של ההערה @GeneratedValue(strategy = GenerationType.IDENTITY)לשדה id. הוספת מסד נתונים PostgreSQL לשירות RESTful ב-Spring Boot.  חלק 2 - 3 אסטרטגיה זו מתאימה ל-MySQL. אם אנחנו עובדים עם Oracle או PostrgeSQL, אז אנחנו צריכים לקבוע אסטרטגיה אחרת. תוכל לקרוא עוד על אסטרטגיות למפתחות ראשיים כאן . בחרתי באסטרטגיית GenerationType.SEQUENCE. כדי ליישם אותו, נצטרך לשכתב מעט את הקובץ initDB.sql, וכמובן, את ההערות של שדה ה-ID של מחלקה Client. כתוב מחדש את initDB.sql:
CREATE TABLE IF NOT EXISTS clients
(
    id    INTEGER PRIMARY KEY ,
    name  VARCHAR(200) NOT NULL ,
    email VARCHAR(254) NOT NULL ,
    phone VARCHAR(50)  NOT NULL
);
CREATE SEQUENCE clients_id_seq START WITH 3 INCREMENT BY 1;
מה השתנה: סוג עמודת המזהה בטבלה שלנו השתנה, אבל על כך בהמשך. הוספנו שורה למטה בה אנו יוצרים רצף חדש clients_id_seq, מציינים שהוא צריך להתחיל בשלושה (מכיוון שה-id האחרון בקובץ populateDB.sql הוא 2), ומציינים שהעלייה צריכה להתרחש באחד. נחזור לסוג העמודה id. כאן ציינו את INTEGER, כי אם נעזוב את SERIAL, הרצף ייווצר אוטומטית, עם אותו שם clients_id_seq, אבל יתחיל מאחד (מה שהוביל לבאג התוכנית). עם זאת, כעת אם ברצונך למחוק טבלה, תצטרך למחוק בנוסף את הרצף הזה או ידנית דרך ממשק pgAdmin או דרך קובץ .sql באמצעות הפקודות הבאות:
DROP TABLE IF EXISTS clients;
DROP SEQUENCE IF EXISTS clients_id_seq
אבל אם אתה לא משתמש בקובץ כמו populateDB.sql כדי לאכלס את הטבלה בהתחלה, אז אתה יכול להשתמש בסוגים SERIAL או BIGSERIAL עבור המפתח הראשי, ואתה לא צריך ליצור את הרצף באופן ידני, ולכן לא צריך למחוק זה בנפרד. תוכל לקרוא עוד על הרצפים באתר של. תיעוד PostgreSQL . הבה נעבור להערות שדה idהכיתה Clientונעצב אותן באופן הבא:
@Id
@Column(name = "id")
@SequenceGenerator(name = "clientsIdSeq", sequenceName = "clients_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "clientsIdSeq")
private Integer id;
מה עשינו: התקנו ביאור חדש @SequenceGeneratorליצירת מחולל רצף, הקצינו לו שם clientsIdSeq, ציינו שזה מחולל לרצף clients_id_seq, והוספנו תכונה. allocationSize = 1 זו תכונה אופציונלית, אבל אם לא נעשה זאת, נקבל את השגיאה הבאה כשנריץ את התוכנית:

org.hibernate.MappingException: The increment size of the [clients_id_seq] sequence is set to [50] in the entity mapping while the associated database sequence increment size is [1]
הנה מה שהמשתמש אנדריי כותב על כך בהערות: allocationSize נועדה בעיקר לצמצם את הנסיעה של Hibernate למסד הנתונים עבור "מזהה חדש". אם הערך == 1, תרדמה עבור כל ישות חדשה, לפני שמירתה במסד הנתונים, "רוץ" למסד הנתונים של המזהה. אם הערך הוא > 1 (לדוגמה, 5), hibernate תיצור קשר עם מסד הנתונים עבור מזהה "חדש" לעתים רחוקות יותר (לדוגמה, 5 פעמים), ובפנייה, hibernate יבקש ממסד הנתונים לשמור את המספר הזה (בשננו מקרה, 5) ערכים. השגיאה שתיארת מרמזת ש-Hibernate רוצה לקבל 50 מזהי ברירת מחדל, אבל במסד הנתונים ציינת שאתה מוכן להנפיק מזהה עבור ישות זו רק לפי הראשון . באג נוסף נתפס על ידי המשתמש Nikolya Kudryashov : אם תפעיל בקשה מהמאמר המקורי http://localhost:8080/clients/1, השגיאה תוחזר:
{
    "timestamp": "2020-04-02T19:20:16.073+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.javarush.lectures.rest_example.model.Client$HibernateProxy$ZA2m7agZ[\"hibernateLazyInitializer\"])",
    "path": "/clients/1"
}
שגיאה זו קשורה לאתחול העצל של Hibernate, וכדי להיפטר ממנה, עלינו להוסיף הערה נוספת למחלקה Client:
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
בדרך זו:
@Entity
@Table(name = "clients")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Client {.....}
עכשיו בואו נריץ את התוכנית שלנו (לאחר הסרת טבלת הלקוחות ממסד הנתונים אם היא נשארה שם מהפעם הקודמת), ונעיר 3 שורות מקובץ application.properties:
#spring.datasource.initialization-mode=ALWAYS
#spring.datasource.schema=classpath*:database/initDB.sql
#spring.datasource.data=classpath*:database/populateDB.sql
בפעם הקודמת הערנו רק את השורה האחרונה, אבל... מכיוון שכבר יצרנו ומילאנו את הטבלה, זה נראה לי הגיוני יותר כרגע. נעבור לבדיקות, נבצע בקשות GET, POST, PUT ו-DELETE דרך Postman, ונראה שהבאגים נעלמו והכל עובד כשורה. זהו, העבודה בוצעה. כעת נוכל לסכם בקצרה ולשקול את מה שלמדנו:
  • התקן את PostgreSQL במחשב שלך
  • צור מסדי נתונים ב-pgAdmin
  • צור ומחק טבלאות באופן ידני ותכנותי
  • אכלס טבלאות באמצעות קבצי .sql
  • למדנו קצת על ממשק ה-JpaRepository ה"קסם" של ה-Spring framework
  • למדנו על כמה באגים שעלולים להתעורר בעת יצירת תוכנית כזו
  • הבנו שאסור לנו להתבייש לבקש עצות מעמיתים
  • אנו משוכנעים שקהילת JavaRush היא כוח שתמיד יבוא להציל ;)
אנחנו יכולים לסיים כאן לעת עתה. הוספת מסד נתונים PostgreSQL לשירות RESTful ב-Spring Boot.  חלק 2 - 4תודה לכל מי שהקדיש זמן לקרוא את החומר הזה. אשמח לראות את הערותיכם, הערותיכם, ההוספות והביקורת הבונה שלכם. אולי תציעו פתרונות אלגנטיים יותר לבעיה זו, אותם אני מבטיח להוסיף למאמר זה באמצעות "מילת המפתח" של UPD, עם אזכור אותך כמחבר, כמובן. ובכן, באופן כללי, כתבו האם אהבתם את המאמר הזה ואת סגנון הצגת החומר הזה, ובכלל, האם כדאי לי להמשיך לכתוב מאמרים ב-JR. להלן התוספות: UPD1: המשתמש Justinian המליץ ​​בחום לשנות את שם החבילה com.javarush.lectures.rest_exampleל- com.javarush.lectures.rest.example, ואת שם הפרויקט, כדי לא להפר את מוסכמות השמות ב-Java. משתמש UPD2 Alexander Pyanov הציע שכדי לאתחל שדה ClientRepositoryבמחלקה ClientServiceImplעדיף להשתמש בבנאי מאשר בהערה @Autowired. זה מוסבר על ידי העובדה שבמקרים נדירים אתה יכול לקבל NullPointerException, ובאופן כללי, זהו שיטות עבודה מומלצות, ואני מסכים איתו. מבחינה לוגית, אם נדרש שדה לפונקציונליות הראשונית של אובייקט, אז עדיף לאתחל אותו בבנאי, כי מחלקה ללא בנאי לא תורכב לאובייקט, לכן, שדה זה יאותחל בשלב של יצירת אובייקט. אני אוסיף קטע קוד עם תיקונים (מה צריך להחליף במה):
@Autowired
private ClientRepository clientRepository;

private final ClientRepository clientRepository;

public ClientServiceImpl(ClientRepository clientRepository) {
   this.clientRepository = clientRepository;
}
קישור לחלק הראשון: הוספת מסד נתונים PostgreSQL לשירות RESTful ב-Spring Boot. חלק 1 נ.ב. אם מישהו מכם רוצה להמשיך ולפתח את האפליקציה החינוכית הזו, אשמח להוסיף קישור למדריכים שלכם למאמר זה. אולי מתישהו התוכנית הזו תגדל למשהו שדומה ליישום עסקי אמיתי שתוכל להוסיף עליו עבודה לתיק העבודות שלך. PPS לגבי המאמר הצנוע הזה, החלטתי להקדיש את מבחן העט הזה לנערות, נשים וגברות יקרות שלנו. מי יודע, אולי עכשיו לא היה ג'אווה, לא JavaRush, אין תכנות בטבע, אלמלא האישה הזו . מזל טוב לחג שלך, אנשים חכמים יקרים שלנו! 8 במרץ שמח! תהיה שמח ויפה!
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION