英文:
Spring Data JPA implementing Persistable results in InvalidDataAccessApiUsageException: detached entity passed to persist
问题
I'm trying to implement Persistable
to delegate entity state detection to the interface and to finally minimize the number of db queries while saving / updating entities. After implementing I get InvalidDataAccessApiUsageException: detached entity passed to persist
while trying to update existing entities.
这是我尝试实现Persistable
接口,以将实体状态检测委托给接口,并最终在保存/更新实体时减少数据库查询次数。但在实现后,当尝试更新现有实体时,我收到了InvalidDataAccessApiUsageException: detached entity passed to persist
异常。
These exceptions occur for every class while using the Spring Data Repository save
method. Code is working fine without Persistable
but with a lot of more queries.
这些异常会在使用Spring Data Repository的save
方法时发生,对于每个类都会发生。如果不使用Persistable
接口,代码运行正常,但会产生更多的查询。
Overall the case is to get rid of many queries generated while using Spring Data JPA save
method. Without implementation, all methods are working Ok.
总的来说,问题是要消除在使用Spring Data JPA save
方法时生成的许多查询。如果不实现Persistable
接口,所有方法都正常工作。
Anybody has any idea on what's going on here?
有人对这里发生的情况有什么想法吗?
Implementation of Persistable
according to the docs.
根据文档的要求实现了Persistable
接口。
Example entity class
示例实体类
Service class
服务类
Repository
仓库
Test case
测试用例
Error
错误
英文:
I'm trying to implement Persistable
to delegate entity state detection to the interface and to finally minimize the number of db queries while saving / updating entities. After implementing I get InvalidDataAccessApiUsageException: detached entity passed to persist
while trying to update existing entities.
These exceptions occur for every class while using the Spring Data Repository save
method. Code is working fine without Persistable
but with a lot of more queries.
Overall the case is to get rid of many queries generated while using Spring Data JPA save
method. Without implementation, all methods are working Ok.
This is Persistable
implementation for a superClass based on the Spring Data JPA docs
Anybody has any idea on what's going on here?
Implementation of Persistable
according to the docs.
@MappedSuperclass
public class BaseEntity implements Persistable<UUID> {
public BaseEntity () {
}
public BaseEntity(UUID id, Long version, Timestamp createdDate, Timestamp lastModifiedDate) {
this.id = id;
this.version = version;
this.createdDate = createdDate;
this.lastModifiedDate = lastModifiedDate;
}
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator")
@Column(updatable = false, nullable = false )
private UUID id;
@Version
private Long version;
@CreationTimestamp
@Column(updatable = false, nullable = false)
private Timestamp createdDate;
@UpdateTimestamp
private Timestamp lastModifiedDate;
public int hashCode () {
return Objects.hash(this.id);
}
public boolean equals (Object that) {
return this == that || that instanceof BaseEntity && Objects.equals(this.id, ((BaseEntity) that).id);
}
public UUID getId() {
return id;
}
@Transient
private boolean isNew = true;
@Override
public boolean isNew() {
return isNew;
}
@PrePersist
@PostLoad
void markNotNew() {
this.isNew = false;
}
public void setId(UUID id) {
this.id = id;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
public Timestamp getCreatedDate() {
return createdDate;
}
public void setCreatedDate(Timestamp createdDate) {
this.createdDate = createdDate;
}
public Timestamp getLastModifiedDate() {
return lastModifiedDate;
}
public void setLastModifiedDate(Timestamp lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
}
Example entity class
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Question extends BaseEntity {
private String contents;
private String mainTech;
private String specificTech;
@Enumerated(EnumType.STRING)
private SkillLevel skillLevel;
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private List<Answer> answers = new ArrayList<>();
@Builder
public Question(UUID id,
Long version,
Timestamp createdDate,
Timestamp lastModifiedDate,
java.lang.String contents,
String mainTech,
java.lang.String specificTech,
ArrayList<Answer> answers,
SkillLevel skillLevel) {
super(id, version, createdDate, lastModifiedDate);
this.contents = contents;
this.mainTech = mainTech;
this.specificTech = specificTech;
this.answers = answers;
this.skillLevel = skillLevel;
}
public void addAnswer(Answer answer) {
this.getAnswers().add(answer);
answer.setQuestion(this);
}
public void removeAnswer(Answer answer) {
this.getAnswers().remove(answer);
answer.setQuestion(null);
}
@Override
public java.lang.String toString() {
return "Question{" +
"id='" + this.getId() + '\'' +
"contents='" + contents + '\'' +
", mainTech=" + mainTech +
", specificTech='" + specificTech + '\'' +
", skillLevel=" + skillLevel +
", answers=" + answers.size() +
'}';
}
}
Service class
@Service
public class QuestionServiceImpl implements QuestionService {
private final QuestionRepository questionRepository;
private final QuestionMapper mapper;
private final EntityManagerFactory emf;
public QuestionServiceImpl(
QuestionRepository questionRepository,
EntityManagerFactory emf,
QuestionMapper mapper
) {
this.questionRepository = questionRepository;
this.mapper = mapper;
this.emf = emf;
}
@Override
public QuestionDto saveOrUpdate(QuestionDto questionDto) {
Question save = questionRepository.save(mapper.dtoToObject(questionDto, contextProvider()));
return mapper.objectToDto(save, contextProvider());
}
public QuestionDto findByUuId(UUID uuid) {
Question question = emf.createEntityManager().createQuery("select q from Question q " +
"left join fetch q.answers " +
"where q.id = :uuid", Question.class)
.setParameter("uuid", uuid)
.getSingleResult();
return mapper.objectToDto(question, contextProvider());
}
//More code ...
}
Repository
@Repository
public interface QuestionRepository extends JpaRepository<Question, UUID> {
@Override
List<Question> findAll();
@Override
<S extends Question> S save(S s);
@Override
Optional<Question> findById(UUID uuid);
Question findByContentsEquals(String contents);
List<Question> findAllByMainTechAndSpecificTech(String mainTech, java.lang.String specificTech);
List<Question> findAllByMainTech(String mainTech);
List<Question> findAllByMainTechAndSkillLevel(String mainTech, SkillLevel skillLevel);
List<Question> findAllByMainTechAndSkillLevelAndSpecificTech(String mainTech, SkillLevel skillLevel, String specificTech);
}
Test case
@RunWith(SpringRunner.class)
@SpringBootTest
class QuestionServiceImplTest {
@Autowired
QuestionRepository repository;
@Autowired
QuestionServiceImpl service;
@Autowired
QuestionMapper mapper;
QuestionDto question1;
QuestionDto question2;
@BeforeEach
void setUp() {
question1 = QuestionDto.builder()
.answers(new ArrayList<>())
.contents("testQuestion1")
.skillLevel(SkillLevel.ENTRY)
.specificTech("Core")
.mainTech("Java")
.build();
question2 = QuestionDto.builder()
.answers(new ArrayList<>())
.contents("testQuestion1")
.skillLevel(SkillLevel.ENTRY)
.specificTech("Core")
.mainTech("Java")
.build();
assertAll(
() -> assertThat(question1.getContents()).isEqualTo(question2.getContents())
);
}
@Test
public void callingSaveOrUpdateMultipleTimesShouldAlwaysReturnSingleEntity() {
// Given
QuestionDto savedQuestion1 = service.saveOrUpdate(question1);
System.out.println("first save 1" + savedQuestion1);
QuestionDto savedQuestion2 = service.saveOrUpdate(question2);
System.out.println("first save 2" + savedQuestion2);
QuestionDto savedQuestion1a = service.saveOrUpdate(savedQuestion1);
QuestionDto savedQuestion1b = service.saveOrUpdate(savedQuestion1a);
QuestionDto savedQuestion1c = service.saveOrUpdate(savedQuestion1b);
System.out.println("last save 1");
QuestionDto savedQuestion2a = service.saveOrUpdate(savedQuestion2);
QuestionDto savedQuestion2b = service.saveOrUpdate(savedQuestion2a);
service.saveOrUpdate(savedQuestion2b);
System.out.println("last save 2");
// When
QuestionDto searchResult = service.findByUuId(savedQuestion1.getId());
List<QuestionDto> all = service.findAll();
// Then
assertAll(
() -> assertThat(searchResult).isNotNull(),
() -> assertThat(searchResult.getId()).isEqualTo(savedQuestion1.getId()),
() -> assertThat(all.size()).isEqualTo(2),
() -> assertThat(all.get(0).getId()).isEqualTo(savedQuestion1.getId()),
() -> assertThat(all.get(1).getId()).isEqualTo(savedQuestion2.getId())
);
}
}
Error
org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: com.github.pawelbialas.testgeneratorapp.entity.question.model.Question; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: com.github.pawelbialas.testgeneratorapp.entity.question.model.Question
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:319)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:255)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:528)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:178)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
at com.sun.proxy.$Proxy156.save(Unknown Source)
at com.github.pawelbialas.testgeneratorapp.entity.question.service.QuestionServiceImpl.saveOrUpdate(QuestionServiceImpl.java:38)
at com.github.pawelbialas.testgeneratorapp.entity.question.service.QuestionServiceImplTest.callingSaveOrUpdateMultipleTimesShouldAlwaysReturnSingleEntity(QuestionServiceImplTest.java:70)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:675)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:125)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:132)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:124)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:74)
at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:104)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:62)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:43)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:35)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:202)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:198)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:229)
at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:197)
at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:211)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
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: org.hibernate.PersistentObjectException: detached entity passed to persist: com.github.pawelbialas.testgeneratorapp.entity.question.model.Question
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:127)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:62)
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:108)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:702)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:688)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:314)
at com.sun.proxy.$Proxy139.persist(Unknown Source)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:554)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:371)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:204)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:657)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:621)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:605)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:366)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:99)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
... 72 more
答案1
得分: 3
为什么您觉得在示例代码上方的标题中已经标明是“用于具有手动分配标识符的实体的基类”,还需要对Persistable
进行自定义定义,并加上带有@GeneratedValue
注解的id呢?
以这种方式实现Persistable
意味着无论何时通过从DTO进行映射来创建实体对象,Spring Data都会将其视为新对象。因此,JpaRepository.save()
将在内部调用EntityManager.persist()
。这意味着在测试中对QuestionDto
的service.saveOrUpdate(...)
的所有后续调用(初始调用之后)将尝试持久化现有对象(而不是将其合并到上下文中),因此会出现错误。
如果您想要自定义的Persistable
实现起作用,您需要拥有单独的save
和update
方法,其中update
类似于以下内容:
questionRepository.findById(questionDto.getId())
.ifPresent(dbQuestionVersion ->
mapper.mapDtoOntoObject(questionDto, dbQuestionVersion, contextProvider()))
不过,我猜这可能不是您想要的解决方案。
英文:
Why do you feel you need a custom definition of Persistable
with @GeneratedValue
-annotated id when the caption above the sample code reads: 'A base class for entities with manually assigned identifiers'?
Implementing Persistable
that way means that whenever you create the entity object by mapping from the DTO, it will be considered new by Spring Data. Consequently, JpaRepository.save()
will call EntityManager.persist()
internally. This implies all the subsequent calls to service.saveOrUpdate(...)
on a QuestionDto
in your test (after the initial one) will try to persist existing objects (instead of merging them into the context), hence the error.
If you want the custom Persistable
implementation to work, you would need separate save
and update
methods where update
looks sth like:
questionRepository.findById(questionDto.getId())
.ifPresent(dbQuestionVersion ->
mapper.mapDtoOntoObject(questionDto, dbQuestionVersion, contextProvider()))`
I'm guessing that's not what you want, though.
答案2
得分: 0
以下代码在BaseEntity中不可用。无论何时你使用OneToMany或ManyToOne,都应该在两个实体中声明。
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "foreign_key")
private Question question;
英文:
Below code not available in BaseEntiy. When ever you are OneToMany or ManyToOne you should declare in both entity
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "foreign_key")
private Question question;
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论