英文:
persistence jsonb field to h2 using hibernate
问题
I understand your concern. The issue you're facing seems to be related to H2's handling of the jsonb
column type when compared to PostgreSQL. The error is occurring due to a data conversion error in the ValueJson.convertToJson()
method, specifically when the value type is JAVA_OBJECT
. Unfortunately, H2's handling of jsonb
is not as seamless as PostgreSQL's, and this has caused difficulties for your application.
You've already identified the part of the H2 source code that is causing the problem. In the ValueJson.convertToJson()
method, the switch
statement handles various value types, but when the value type is JAVA_OBJECT
, it throws a data conversion error.
It seems that H2 does not have native support for JSON or JSONB types, and therefore, handling them seamlessly like PostgreSQL is challenging. As a result, you might need to take a different approach to work around this issue.
Here are a few possible solutions you could consider:
-
Change Data Type Handling: You could change your approach to how JSON is stored in H2. Instead of trying to map it to a custom Hibernate type, you could store JSON as a string in H2 and manually serialize/deserialize it in your application.
-
Use PostgreSQL for Testing: Since you mentioned that your code works fine with PostgreSQL, you could consider using PostgreSQL as the database for your tests, even if you're primarily using H2 for development. This could help ensure that your code functions correctly with your desired JSONB column handling.
-
Custom Type Handling in H2: If you're comfortable with contributing to open-source projects, you could consider submitting a pull request to the H2 project to enhance their support for JSONB types.
-
Explore Other Embedded Databases: If you find that H2's limitations are hindering your development, you might explore other embedded databases that provide better support for JSON data types.
Remember that your choice of approach will depend on your specific project requirements and constraints. It's also worth keeping an eye on updates to the H2 database project to see if they address these issues in the future.
英文:
I want to use jsonb
column type. When I used postgresql
there is no problem. But when I use H2
I can not persist my entity. Native sql works but when saving EntityManager.persist
i got below error
ERROR: Data conversion error converting "X'aced000574000f7b226b6579223a2276616c7565227d' (json_entities: ""attributes"" ""JSONB"")"; SQL statement:
insert into json_entities (attributes, id) values (?, ?) [22018-200]
javax.persistence.RollbackException: Error while committing the transaction
at org.hibernate.internal.ExceptionConverterImpl.convertCommitException(ExceptionConverterImpl.java:81)
at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:104)
at H2Test.jsonFieldTest(H2Test.java:39)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
Caused by: javax.persistence.PersistenceException: org.hibernate.exception.DataException: could not execute statement
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:154)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
at org.hibernate.internal.ExceptionConverterImpl.convertCommitException(ExceptionConverterImpl.java:65)
... 29 more
Caused by: org.hibernate.exception.DataException: could not execute statement
at org.hibernate.exception.internal.SQLExceptionTypeDelegate.convert(SQLExceptionTypeDelegate.java:52)
at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:42)
at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:113)
at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:99)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:200)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3254)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3779)
at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:107)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604)
at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478)
at java.util.LinkedHashMap.forEach(LinkedHashMap.java:684)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475)
at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:348)
at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40)
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:102)
at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1360)
at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:451)
at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3210)
at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2378)
at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:447)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:183)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$300(JdbcResourceLocalTransactionCoordinatorImpl.java:40)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:281)
at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101)
... 28 more
Caused by: org.h2.jdbc.JdbcSQLDataException: Data conversion error converting "X'aced000574000f7b226b6579223a2276616c7565227d' (json_entities: ""attributes"" ""JSONB"")"; SQL statement:
insert into json_entities (attributes, id) values (?, ?) [22018-200]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:457)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:429)
at org.h2.message.DbException.get(DbException.java:194)
at org.h2.table.Column.getDataConversionError(Column.java:409)
at org.h2.table.Column.validateConvertUpdateSequence(Column.java:381)
at org.h2.table.Table.validateConvertUpdateSequence(Table.java:845)
at org.h2.command.dml.Insert.insertRows(Insert.java:187)
at org.h2.command.dml.Insert.update(Insert.java:151)
at org.h2.command.CommandContainer.update(CommandContainer.java:198)
at org.h2.command.Command.executeUpdate(Command.java:251)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:191)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:152)
at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:197)
... 47 more
Caused by: org.h2.message.DbException: Data conversion error converting "OTHER to JSON" [22018-200]
at org.h2.message.DbException.get(DbException.java:205)
at org.h2.message.DbException.get(DbException.java:181)
at org.h2.value.Value.getDataConversionError(Value.java:1504)
at org.h2.value.Value.convertToJson(Value.java:1439)
at org.h2.value.Value.convertTo(Value.java:861)
at org.h2.value.Value.convertTo(Value.java:772)
at org.h2.value.TypeInfo.cast(TypeInfo.java:515)
at org.h2.table.Column.validateConvertUpdateSequence(Column.java:378)
... 57 more
Caused by: org.h2.jdbc.JdbcSQLDataException: Data conversion error converting "OTHER to JSON" [22018-200]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:457)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:429)
... 65 more
Here is my simple project.
Custom hibernate type for jsonb
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.usertype.UserType;
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
/**
* Created by sanco on 29/09/2020.
* h2jsontest
*/
public class PGJsonType implements UserType {
private final int CUSTOM_TYPE = Types.OTHER;
private final static ObjectMapper jsonMapper = new ObjectMapper();
@Override
public int[] sqlTypes() {
return new int[]{CUSTOM_TYPE};
}
@Override
public Class returnedClass() {
return JsonNode.class;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
return x==null? y==null : ((JsonNode)x).equals((JsonNode)y);
}
@Override
public int hashCode(Object x) throws HibernateException {
return ((JsonNode)x).hashCode();
}
@Override
public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException {
final String cellContent = rs.getString(names[0]);
if (cellContent == null) {
return null;
}
try {
return jsonMapper.readTree(cellContent);
} catch (final Exception ex) {
throw new RuntimeException("Failed to convert jsonb to JsonNode: " + ex.getMessage(), ex);
}
}
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException {
if (value == null) {
st.setNull(index, CUSTOM_TYPE);
return;
}
try {
st.setObject(index, jsonMapper.writeValueAsString(value), CUSTOM_TYPE);
} catch (final Exception ex) {
throw new RuntimeException("Failed to convert JsonNode to jsonb: " + ex.getMessage(), ex);
}
}
@Override
public Object deepCopy(Object value) throws HibernateException {
return value==null? null : ((JsonNode)value).deepCopy();
}
@Override
public boolean isMutable() {
return true;
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
return (Serializable) value;
}
@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return cached;
}
@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return original;
}
}
my entity class
import com.fasterxml.jackson.databind.JsonNode;
import org.hibernate.annotations.Type;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* Created by sanco on 29/09/2020.
* h2jsontest
*/
@Entity
@Table(name="json_entities")
public class JsonEntity {
@Id
private Long id;
@Type(type = "PGJsonType")
@Column(columnDefinition = "jsonb")
private JsonNode attributes;
public void setId(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
public JsonNode getAttributes() {
return attributes;
}
public void setAttributes(JsonNode attributes) {
this.attributes = attributes;
}
}
persistence unit
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="postgre">
<description>
Hibernate using JPA
</description>
<class>JsonEntity</class>
<properties>
<property name="hibernate.connection.url" value="jdbc:postgresql://localhost:5432/postgres"/>
<property name="hibernate.connection.driver_class" value="org.postgresql.Driver"/>
<property name="hibernate.connection.username" value="postgres"/>
<property name="hibernate.connection.password" value="s2351910"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQL10Dialect"/>
<property name="hibernate.temp.use_jdbc_metadata_defaults"
value="false"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
<property name="hibernate.enable_lazy_load_no_trans" value="true"/>
<property name="hibernate.connection.provider_class"
value="org.hibernate.hikaricp.internal.HikariCPConnectionProvider"/>
<property name="hibernate.hikari.minimumIdle" value="5"/>
<property name="hibernate.hikari.maximumPoolSize" value="20"/>
<property name="hibernate.hikari.idleTimeout" value="45000"/>
</properties>
</persistence-unit>
<persistence-unit name="h2">
<description>
Hibernate using JPA
</description>
<class>JsonEntity</class>
<properties>
<property name="hibernate.connection.url" value="jdbc:h2:mem:organization;MODE=PostgreSQL;IGNORECASE=TRUE;DATABASE_TO_LOWER=TRUE;AUTO_RECONNECT=TRUE;INIT=CREATE DOMAIN IF NOT EXISTS jsonb AS other\;CREATE TYPE if not exists &quot;JSONB&quot; AS json;"/>
<property name="hibernate.connection.driver_class" value="org.h2.Driver"/>
<property name="hibernate.connection.username" value="sa"/>
<property name="hibernate.connection.password" value=""/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.temp.use_jdbc_metadata_defaults"
value="false"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
<property name="hibernate.enable_lazy_load_no_trans" value="true"/>
<property name="hibernate.connection.provider_class"
value="org.hibernate.hikaricp.internal.HikariCPConnectionProvider"/>
<property name="hibernate.hikari.minimumIdle" value="5"/>
<property name="hibernate.hikari.maximumPoolSize" value="20"/>
<property name="hibernate.hikari.idleTimeout" value="60000"/>
</properties>
</persistence-unit>
</persistence>
test classes
H2
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
/**
* Created by sanco on 29/09/2020.
* h2jsontest
*/
public class H2Test {
private static EntityManagerFactory factory;
private static ObjectMapper mapper;
@BeforeClass
public static void init(){
factory = Persistence.createEntityManagerFactory("h2");
mapper = new ObjectMapper();
}
@Test
public void jsonFieldTest(){
EntityManager em = factory.createEntityManager();
JsonEntity je = new JsonEntity();
je.setId(1L);
ObjectNode on = mapper.createObjectNode();
on.put("key", "value");
je.setAttributes(on);
try {
em.getTransaction().begin();
em.persist(je);
em.getTransaction().commit();
}catch (Exception e){
e.printStackTrace();
em.getTransaction().rollback();
}
assert (em.createQuery("select j.id from JsonEntity j", Long.class).getSingleResult())==1L;
}
@AfterClass
public static void cleanResource(){
if(factory!=null)
factory.close();
}
}
Postgresql
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
/**
* Created by sanco on 29/09/2020.
* h2jsontest
*/
public class PostgreSQLTest {
private static EntityManagerFactory factory;
private static ObjectMapper mapper;
@BeforeClass
public static void init(){
factory = Persistence.createEntityManagerFactory("postgre");
mapper = new ObjectMapper();
}
@Test
public void jsonFieldTest(){
EntityManager em = factory.createEntityManager();
JsonEntity je = new JsonEntity();
je.setId(1L);
ObjectNode on = mapper.createObjectNode();
on.put("key", "value");
je.setAttributes(on);
try {
em.getTransaction().begin();
em.persist(je);
em.getTransaction().commit();
}catch (Exception e){
e.printStackTrace();
em.getTransaction().rollback();
}
assert (em.createQuery("select j.id from JsonEntity j", Long.class).getSingleResult())==1L;
}
@AfterClass
public static void cleanResource(){
if(factory!=null)
factory.close();
}
}
I read lots of forms but the suggested solution does not work for me. I also debug the H2
source code and see that ValueJson.convertToJson
throw exception because of value type not handle on switch case. Look at the source code of org.h2.value.Value.java
private ValueJson convertToJson() {
switch (getValueType()) {
case BOOLEAN:
return ValueJson.get(getBoolean());
case BYTE:
case SHORT:
case INT:
return ValueJson.get(getInt());
case LONG:
return ValueJson.get(getLong());
case FLOAT:
case DOUBLE:
case DECIMAL:
return ValueJson.get(getBigDecimal());
case BYTES:
case BLOB:
return ValueJson.fromJson(getBytesNoCopy());
case STRING:
case STRING_IGNORECASE:
case STRING_FIXED:
case CLOB:
return ValueJson.get(getString());
case GEOMETRY: {
ValueGeometry vg = (ValueGeometry) this;
return ValueJson.getInternal(GeoJsonUtils.ewkbToGeoJson(vg.getBytesNoCopy(), vg.getDimensionSystem()));
}
default:
throw getDataConversionError(Value.JSON);
}
}
getValueType
return 19
which means JAVA_OBJECT
. How can I handle this problem?
答案1
得分: 1
我使用Spring Boot与Liquibase,以及这个库(https://github.com/vladmihalcea/hibernate-types)来实现此功能。并且使用这篇文章(https://vladmihalcea.com/how-to-map-json-objects-using-generic-hibernate-types/)来完成此操作。
第一步是创建一个Liquibase变更集,仅在测试模式下运行,将JSONB列类型视为JSON。H2支持后者但不支持前者。
这是第一个变更集,仅在上下文为test且目标数据库为H2时运行。
<changeSet id="0" author="psc" context="test" dbms="h2">
<sql>
CREATE TYPE IF NOT EXISTS "JSONB" as json;
</sql>
</changeSet>
application-test.properties
的内容
spring.liquibase.contexts=test
我的测试实体。请查看How to map json
文章以更好地理解所使用的注解。
import com.vladmihalcea.hibernate.type.json.JsonType;
import lombok.*;
import net.energyhub.testcontainers.dto.CustomerData;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;
import javax.persistence.*;
@Entity
@Table(name = "customers")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@TypeDefs({
@TypeDef(name = "json", typeClass = JsonType.class)
})
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
@Type(type = "json")
@Column(columnDefinition = "jsonb")
private CustomerData customerDetails;
}
演示一个测试,测试JSON列。我在这里使用了Spring Boot的DataJPATest测试切片功能。
@DataJpaTest
@ActiveProfiles("test")
public class CustomerServiceH2Test {
@Autowired
private CustomerRepository customerRepository;
@Test
public void testFindAll() {
Customer customer = Customer.builder()
.name("Phil Calouche")
.customerDetails(CustomerData.builder().x("x-value").build())
.build();
customerRepository.save(customer);
assertThat(customerRepository.findAll()).hasSize(4);
}
}
英文:
I'm using Spring Boot with Liquibase and this library (https://github.com/vladmihalcea/hibernate-types) to accomplish this. And this article to accomplish this https://vladmihalcea.com/how-to-map-json-objects-using-generic-hibernate-types/.
The first step is have a Liquibase change set that only runs in test mode to treat JSONB column types as JSON. H2 supports the latter but not the former.
This is the first change set that runs, and it is only targeted if the context is test and if the target DB is H2.
<changeSet id="0" author="psc" context="test" dbms="h2">
<sql>
CREATE TYPE IF NOT EXISTS "JSONB" as json;
</sql>
</changeSet>
Contents of application-test.properties
spring.liquibase.contexts=test
My test entity. See How to map json
article to better understand the annotations that were used.
import com.vladmihalcea.hibernate.type.json.JsonType;
import lombok.*;
import net.energyhub.testcontainers.dto.CustomerData;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;
import javax.persistence.*;
@Entity
@Table(name = "customers")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@TypeDefs({
@TypeDef(name = "json", typeClass = JsonType.class)
})
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
@Type(type = "json")
@Column(columnDefinition = "jsonb")
private CustomerData customerDetails;
}
Example of a test that exercises the JSON column. I'm making use of Spring Boot's DataJPATest test slice feature.
@DataJpaTest
@ActiveProfiles("test")
public class CustomerServiceH2Test {
@Autowired
private CustomerRepository customerRepository;
@Test
public void testFindAll() {
Customer customer = Customer.builder()
.name("Phil Calouche")
.customerDetails(CustomerData.builder().x("x-value").build())
.build();
customerRepository.save(customer);
assertThat(customerRepository.findAll()).hasSize(4);
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论