Musim semi boot kolom mysql json

Cara mengimplementasikan objek domain Spring Boot/entitas JPA yang menangani panggilan balik acara webhook Stripe, menggunakan @Convert khusus untuk menyimpan ke database MySQL sebagai kolom tipe json

(Itu harus mencakup kata kunci, umpan Google, dan menakut-nakuti siapa pun yang tidak peduli. )

Musim semi boot kolom mysql json

pengantar

Saya memiliki aplikasi web yang sedang diperbarui untuk menggunakan API Pembayaran webhook Stripe (https. //garis. com/dokumen/webhook)

Aplikasi ini menggunakan Spring Boot, JPA dan MySQL. Pendekatannya adalah menyimpan callback acara webhook di database saat diterima

Ada perpustakaan Stripe Java (https. //github. com/stripe/stripe-java) yang dapat digunakan untuk permintaan panggilan balik acara webhook. Bagi banyak orang ini adalah pilihan yang layak dan mungkin lebih baik. Namun saya tidak memerlukan akses ke objek data yang kompleks dan lebih suka menangani lebih sedikit ketergantungan dan memutar domain/entitas saya sendiri. Caveat emptor dll

Callback acara webhook (https. //garis. com/docs/api/events) adalah payload JSON dengan atribut sederhana yang konsisten dan dua objek atribut kompleks, data dan request

Mendekati

Pendekatannya adalah deserialize permintaan JSON melalui Jackson ke objek domain tunggal, menyimpan objek data dan request sebagai objek java JsonNode dan tipe json di MySQL

Dengan mempertahankan struktur JSON, model domain dapat disederhanakan menjadi satu objek yang menangani semua kemungkinan jenis permintaan callback webhook acara, sementara data JSON masih tersedia untuk aplikasi di domain Java dan melalui kueri SQL-JSON

Trade-off dan Pertimbangan

Sekarang adalah saat yang tepat untuk membahas pertukaran dan pertimbangan

Tipe MySQL JSON tidak secepat tipe data yang lebih tradisional. Performa akan lambat saat menanyakan kolom jenis JSON vs tabel yang lebih tradisional dengan indeks yang disetel. (Saya tidak peduli jika itu menyakiti perasaan tim NoSQL, itu benar. )

Sintaks kueri untuk tipe JSON canggung. Jika kueri ekstensif direncanakan untuk data, pastikan untuk menguji kueri tersebut untuk performa. Untuk aplikasi ini, kekurangan tersebut tidak menjadi perhatian

Pustaka Stripe Java tampaknya direkayasa dengan baik dan mencakup semua seluk-beluk permintaan callback acara webhook. Jika Anda menemukan diri Anda menggunakan objek acara untuk logika bisnis yang kompleks, mungkin pendekatan yang lebih baik untuk menggunakan pustaka Stripe Java

Detail

Diuji dengan versi berikut

  • Versi Jawa 1. 8. 0_111
  • Boot Musim Semi 2. 2. 0. MELEPASKAN
  • Fasterxml Jackson Core 2. 10. 0
  • Server MySQL5. 7. 29

API untuk definisi objek permintaan callback event webhook
https. //garis. com/docs/api/events/object

Objek acara ini mendefinisikan objek entitas domain Java dan tabel database MySQL

Perpustakaan Jenis Hibernasi

Salah satu tantangannya adalah menemukan 'prior art' atau pemetaan implementasi dari objek Java JsonNode di entitas domain ke tipe json database MySQL. Saya berasumsi bahwa ini adalah masalah yang terpecahkan

Banyak pencarian mengarah ke posting Stack Overflow di mana solusinya adalah menggunakan perpustakaan Hibernate Types (https. //github. com/vladmihalcea/hibernate-types)

Saya menambahkan dependensi, Jenis, dan konfigurasi, tetapi tidak dapat menyelesaikan kesalahan berikut

SqlExceptionHelper: SQL Error: 3144, SQLState: 22001
SqlExceptionHelper: Data truncation: Cannot create a JSON value from a string with CHARACTER SET 'binary'.

Meskipun saya yakin ini berfungsi untuk beberapa orang dan saya yakin saya membuat beberapa kesalahan bodoh di suatu tempat, saya akhirnya tidak menggunakan pustaka Hibernate Types

Solusi yang Diimplementasikan

Solusi yang diimplementasikan adalah konverter khusus sederhana, menerapkan konverter ke objek entitas domain Java dengan anotasi @Convert

Objek Entitas Domain Java

Langkah pertama adalah menentukan objek permintaan panggilan balik acara webhook. Objeknya adalah kelas yang sangat khas, satu hal yang perlu diperhatikan adalah anotasi

// Imports ommitted

/**
 *
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Entity
@Table(name = "WEBHOOK_EVENT")
public class WebhookEvent
{
  @JsonProperty("id")
  @Id
  @Column(name = "ID")
  private String id;

  @JsonProperty("object")
  @Column(name = "OBJECT")
  private String object;

  @JsonProperty("api_version")
  @Column(name = "API_VERSION")
  private String apiVersion;

  @JsonProperty("created")
  @Column(name = "CREATED")
  private Long created;

  @JsonProperty("data")
  @Column(name = "DATA")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode data;

  @JsonProperty("livemode")
  @Column(name = "LIVEMODE")
  @Type(type = "org.hibernate.type.NumericBooleanType")
  private boolean livemode;

  @JsonProperty("pending_webhooks")
  @Column(name = "PENDING_WEBHOOKS")
  private Integer pendingWebhooks;

  @JsonProperty("request")
  @Column(name = "REQUEST")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode request;

  @JsonProperty("type")
  @Column(name = "TYPE")
  private String type;

  // Constructors and accessors ommitted

}
0

// Imports ommitted

/**
 *
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Entity
@Table(name = "WEBHOOK_EVENT")
public class WebhookEvent
{
  @JsonProperty("id")
  @Id
  @Column(name = "ID")
  private String id;

  @JsonProperty("object")
  @Column(name = "OBJECT")
  private String object;

  @JsonProperty("api_version")
  @Column(name = "API_VERSION")
  private String apiVersion;

  @JsonProperty("created")
  @Column(name = "CREATED")
  private Long created;

  @JsonProperty("data")
  @Column(name = "DATA")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode data;

  @JsonProperty("livemode")
  @Column(name = "LIVEMODE")
  @Type(type = "org.hibernate.type.NumericBooleanType")
  private boolean livemode;

  @JsonProperty("pending_webhooks")
  @Column(name = "PENDING_WEBHOOKS")
  private Integer pendingWebhooks;

  @JsonProperty("request")
  @Column(name = "REQUEST")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode request;

  @JsonProperty("type")
  @Column(name = "TYPE")
  private String type;

  // Constructors and accessors ommitted

}
_

Implementasi AttributeConverter

Kelas JsonNodeConverter mengimplementasikan

// Imports ommitted

/**
 *
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Entity
@Table(name = "WEBHOOK_EVENT")
public class WebhookEvent
{
  @JsonProperty("id")
  @Id
  @Column(name = "ID")
  private String id;

  @JsonProperty("object")
  @Column(name = "OBJECT")
  private String object;

  @JsonProperty("api_version")
  @Column(name = "API_VERSION")
  private String apiVersion;

  @JsonProperty("created")
  @Column(name = "CREATED")
  private Long created;

  @JsonProperty("data")
  @Column(name = "DATA")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode data;

  @JsonProperty("livemode")
  @Column(name = "LIVEMODE")
  @Type(type = "org.hibernate.type.NumericBooleanType")
  private boolean livemode;

  @JsonProperty("pending_webhooks")
  @Column(name = "PENDING_WEBHOOKS")
  private Integer pendingWebhooks;

  @JsonProperty("request")
  @Column(name = "REQUEST")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode request;

  @JsonProperty("type")
  @Column(name = "TYPE")
  private String type;

  // Constructors and accessors ommitted

}
1, khususnya metode
// Imports ommitted

/**
 *
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Entity
@Table(name = "WEBHOOK_EVENT")
public class WebhookEvent
{
  @JsonProperty("id")
  @Id
  @Column(name = "ID")
  private String id;

  @JsonProperty("object")
  @Column(name = "OBJECT")
  private String object;

  @JsonProperty("api_version")
  @Column(name = "API_VERSION")
  private String apiVersion;

  @JsonProperty("created")
  @Column(name = "CREATED")
  private Long created;

  @JsonProperty("data")
  @Column(name = "DATA")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode data;

  @JsonProperty("livemode")
  @Column(name = "LIVEMODE")
  @Type(type = "org.hibernate.type.NumericBooleanType")
  private boolean livemode;

  @JsonProperty("pending_webhooks")
  @Column(name = "PENDING_WEBHOOKS")
  private Integer pendingWebhooks;

  @JsonProperty("request")
  @Column(name = "REQUEST")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode request;

  @JsonProperty("type")
  @Column(name = "TYPE")
  private String type;

  // Constructors and accessors ommitted

}
2 dan
// Imports ommitted

/**
 *
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Entity
@Table(name = "WEBHOOK_EVENT")
public class WebhookEvent
{
  @JsonProperty("id")
  @Id
  @Column(name = "ID")
  private String id;

  @JsonProperty("object")
  @Column(name = "OBJECT")
  private String object;

  @JsonProperty("api_version")
  @Column(name = "API_VERSION")
  private String apiVersion;

  @JsonProperty("created")
  @Column(name = "CREATED")
  private Long created;

  @JsonProperty("data")
  @Column(name = "DATA")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode data;

  @JsonProperty("livemode")
  @Column(name = "LIVEMODE")
  @Type(type = "org.hibernate.type.NumericBooleanType")
  private boolean livemode;

  @JsonProperty("pending_webhooks")
  @Column(name = "PENDING_WEBHOOKS")
  private Integer pendingWebhooks;

  @JsonProperty("request")
  @Column(name = "REQUEST")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode request;

  @JsonProperty("type")
  @Column(name = "TYPE")
  private String type;

  // Constructors and accessors ommitted

}
3. Kedua metode ini melakukan konversi antar domain, ke/dari JsonNode dan String. String kemudian dimasukkan ke dalam kolom MySQL json

package com.gordonturner.app.converter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import javax.persistence.AttributeConverter;

public class JsonNodeConverter implements AttributeConverter<JsonNode, String>
{
  private static final Logger logger = LoggerFactory.getLogger( JsonNodeConverter.class );

  /**
   * @param jsonNode
   * @return
   */
  @Override
  public String convertToDatabaseColumn(JsonNode jsonNode)
  {
    if( jsonNode == null)
    {
      logger.warn( "jsonNode input is null, returning null" );
      return null;
    }

    String jsonNodeString = jsonNode.toPrettyString();
    return jsonNodeString;
  }

  /**
   * @param jsonNodeString
   * @return
   */
  @Override
  public JsonNode convertToEntityAttribute(String jsonNodeString) {

    if ( StringUtils.isEmpty(jsonNodeString) )
    {
      logger.warn( "jsonNodeString input is empty, returning null" );
      return null;
    }

    ObjectMapper mapper = new ObjectMapper();
    try
    {
      return mapper.readTree( jsonNodeString );
    }
    catch( JsonProcessingException e )
    {
      logger.error( "Error parsing jsonNodeString", e );
    }
    return null;
  }

}
_

Basis data

Tabel database dikelola melalui liquibase, tetapi pernyataan SQL create untuk tabel webhook adalah

CREATE TABLE `WEBHOOK_EVENT` (
  `ID` varchar(255) NOT NULL,
  `OBJECT` varchar(255) NOT NULL,
  `API_VERSION` varchar(20) NOT NULL,
  `CREATED` bigint(20) NOT NULL,
  `DATA` json DEFAULT NULL,
  `LIVEMODE` int(11) NOT NULL DEFAULT '0',
  `PENDING_WEBHOOKS` int(11) DEFAULT NULL,
  `REQUEST` json DEFAULT NULL,
  `TYPE` varchar(255) NOT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Dua catatan yang perlu diperhatikan adalah

// Imports ommitted

/**
 *
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Entity
@Table(name = "WEBHOOK_EVENT")
public class WebhookEvent
{
  @JsonProperty("id")
  @Id
  @Column(name = "ID")
  private String id;

  @JsonProperty("object")
  @Column(name = "OBJECT")
  private String object;

  @JsonProperty("api_version")
  @Column(name = "API_VERSION")
  private String apiVersion;

  @JsonProperty("created")
  @Column(name = "CREATED")
  private Long created;

  @JsonProperty("data")
  @Column(name = "DATA")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode data;

  @JsonProperty("livemode")
  @Column(name = "LIVEMODE")
  @Type(type = "org.hibernate.type.NumericBooleanType")
  private boolean livemode;

  @JsonProperty("pending_webhooks")
  @Column(name = "PENDING_WEBHOOKS")
  private Integer pendingWebhooks;

  @JsonProperty("request")
  @Column(name = "REQUEST")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode request;

  @JsonProperty("type")
  @Column(name = "TYPE")
  private String type;

  // Constructors and accessors ommitted

}
_4 dan
// Imports ommitted

/**
 *
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Entity
@Table(name = "WEBHOOK_EVENT")
public class WebhookEvent
{
  @JsonProperty("id")
  @Id
  @Column(name = "ID")
  private String id;

  @JsonProperty("object")
  @Column(name = "OBJECT")
  private String object;

  @JsonProperty("api_version")
  @Column(name = "API_VERSION")
  private String apiVersion;

  @JsonProperty("created")
  @Column(name = "CREATED")
  private Long created;

  @JsonProperty("data")
  @Column(name = "DATA")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode data;

  @JsonProperty("livemode")
  @Column(name = "LIVEMODE")
  @Type(type = "org.hibernate.type.NumericBooleanType")
  private boolean livemode;

  @JsonProperty("pending_webhooks")
  @Column(name = "PENDING_WEBHOOKS")
  private Integer pendingWebhooks;

  @JsonProperty("request")
  @Column(name = "REQUEST")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode request;

  @JsonProperty("type")
  @Column(name = "TYPE")
  private String type;

  // Constructors and accessors ommitted

}
5, yang bertipe `json`

Permintaan JSON MySQL

Sebagai contoh cepat, berikut adalah cara memilih

// Imports ommitted

/**
 *
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Entity
@Table(name = "WEBHOOK_EVENT")
public class WebhookEvent
{
  @JsonProperty("id")
  @Id
  @Column(name = "ID")
  private String id;

  @JsonProperty("object")
  @Column(name = "OBJECT")
  private String object;

  @JsonProperty("api_version")
  @Column(name = "API_VERSION")
  private String apiVersion;

  @JsonProperty("created")
  @Column(name = "CREATED")
  private Long created;

  @JsonProperty("data")
  @Column(name = "DATA")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode data;

  @JsonProperty("livemode")
  @Column(name = "LIVEMODE")
  @Type(type = "org.hibernate.type.NumericBooleanType")
  private boolean livemode;

  @JsonProperty("pending_webhooks")
  @Column(name = "PENDING_WEBHOOKS")
  private Integer pendingWebhooks;

  @JsonProperty("request")
  @Column(name = "REQUEST")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode request;

  @JsonProperty("type")
  @Column(name = "TYPE")
  private String type;

  // Constructors and accessors ommitted

}
6 dari kolom
// Imports ommitted

/**
 *
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Entity
@Table(name = "WEBHOOK_EVENT")
public class WebhookEvent
{
  @JsonProperty("id")
  @Id
  @Column(name = "ID")
  private String id;

  @JsonProperty("object")
  @Column(name = "OBJECT")
  private String object;

  @JsonProperty("api_version")
  @Column(name = "API_VERSION")
  private String apiVersion;

  @JsonProperty("created")
  @Column(name = "CREATED")
  private Long created;

  @JsonProperty("data")
  @Column(name = "DATA")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode data;

  @JsonProperty("livemode")
  @Column(name = "LIVEMODE")
  @Type(type = "org.hibernate.type.NumericBooleanType")
  private boolean livemode;

  @JsonProperty("pending_webhooks")
  @Column(name = "PENDING_WEBHOOKS")
  private Integer pendingWebhooks;

  @JsonProperty("request")
  @Column(name = "REQUEST")
  @Convert(converter = JsonNodeConverter.class)
  private JsonNode request;

  @JsonProperty("type")
  @Column(name = "TYPE")
  private String type;

  // Constructors and accessors ommitted

}
4

SELECT ID,
DATA->"$.object.status" AS `OBJECT`
FROM `WEBHOOK_EVENT`;

Kesimpulan

Penemuan cara memetakan dari JsonNode di domain Java ke tipe json MySQL agak sulit

Saya puas dengan hasilnya sambil meminimalkan perpustakaan pihak ketiga. Saya suka ide untuk menjaga objek JSON 'kompleks' tetap utuh sambil memetakan atribut respons sederhana dasar

Bagaimana cara menyimpan objek JSON di kolom MySQL menggunakan Spring Boot Java?

1 Jawaban .
Tambahkan variabel lain di kelas Pengguna katakanlah Private String jsonData
Dalam metode @PrePersist, tulis logika serialisasi
Tandai atribut lain dengan @JsonInclude() - untuk disertakan dalam Jackson @Transient - untuk diabaikan dalam persistensi di kolom terpisah

Bagaimana cara menyimpan data JSON di MySQL menggunakan JPA?

Hibernate Types adalah pustaka tipe tambahan yang tidak didukung oleh Hibernate Core secara default. Ini adalah aplikasi Spring Boot yang menggunakan pustaka ini untuk mempertahankan data JSON (JSON Java Object ) di kolom MySQL json dan untuk menanyakan data JSON dari kolom MySQL json ke Objek JSON Java .

Bagaimana cara memetakan kolom JSON MySQL ke properti entitas Java?

Ini sebagian kecil dari kode saya. @Entity @Table(name = "some_table_name") public class MyCustomEntity mengimplementasikan Serializable { private static final long serialVersionUID = 1L; . .
mysql
hibernasi

Bagaimana cara menyimpan string JSON di MySQL?

Menambahkan Data JSON .
JSON_ARRAY(), yang membuat array. Sebagai contoh. -- mengembalikan [1, 2, "abc"]. SELECT JSON_ARRAY(1, 2, 'abc');
Fungsi JSON_OBJECT(), yang membuat objek. Sebagai contoh. .
Fungsi JSON_QUOTE(), yang mengutip string sebagai nilai JSON. Sebagai contoh. .
atau Anda bisa (CAST anyValue AS JSON)