Support inserts for id only entities.

This broke in the past for some databases that do not support empty value lists.

Closes #777
Original pull request #1047
This commit is contained in:
Mikhail-Polivakha
2021-09-14 17:11:25 +03:00
committed by Jens Schauder
parent a2a5953cda
commit 7551cbd763
23 changed files with 263 additions and 62 deletions

View File

@@ -50,6 +50,7 @@ import java.util.stream.Collectors;
* @author Tyler Van Gorder
* @author Milan Milanov
* @author Myeonghyeon Lee
* @author Mikhail Polivakha
*/
class SqlGenerator {
@@ -92,12 +93,14 @@ class SqlGenerator {
SqlGenerator(RelationalMappingContext mappingContext, JdbcConverter converter, RelationalPersistentEntity<?> entity,
Dialect dialect) {
final RenderContextFactory renderContextFactory = new RenderContextFactory(dialect);
this.mappingContext = mappingContext;
this.entity = entity;
this.sqlContext = new SqlContext(entity);
this.sqlRenderer = SqlRenderer.create(new RenderContextFactory(dialect).createRenderContext());
this.sqlRenderer = SqlRenderer.create(renderContextFactory.createRenderContext());
this.columns = new Columns(entity, mappingContext, converter);
this.renderContext = new RenderContextFactory(dialect).createRenderContext();
this.renderContext = renderContextFactory.createRenderContext();
}
/**

View File

@@ -80,6 +80,7 @@ import org.springframework.transaction.annotation.Transactional;
* @author Tyler Van Gorder
* @author Clemens Hahn
* @author Milan Milanov
* @author Mikhail Polivakha
*/
@ContextConfiguration
@Transactional
@@ -887,6 +888,12 @@ public class JdbcAggregateTemplateIntegrationTests {
assertThat(loaded.testTime).isEqualTo(entity.testTime);
}
@Test // DATAJDBC-557
public void insertWithIdOnly() {
WithIdOnly entity = new WithIdOnly();
assertThat(template.save(entity).id).isNotNull();
}
private <T extends Number> void saveAndUpdateAggregateWithVersion(VersionedAggregate aggregate,
Function<Number, T> toConcreteNumber) {
saveAndUpdateAggregateWithVersion(aggregate, toConcreteNumber, 0);
@@ -1254,6 +1261,11 @@ public class JdbcAggregateTemplateIntegrationTests {
LocalDateTime testTime;
}
@Table
class WithIdOnly {
@Id Long id;
}
@Configuration
@Import(TestConfiguration.class)
static class Config {

View File

@@ -39,6 +39,8 @@ import org.springframework.data.jdbc.core.mapping.PersistentPropertyPathTestUtil
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.dialect.AnsiDialect;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.dialect.PostgresDialect;
import org.springframework.data.relational.core.dialect.SqlServerDialect;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.NamingStrategy;
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
@@ -61,6 +63,7 @@ import org.springframework.data.relational.core.sql.Table;
* @author Tom Hombergs
* @author Milan Milanov
* @author Myeonghyeon Lee
* @author Mikhail Polivakha
*/
class SqlGeneratorUnitTests {
@@ -391,13 +394,22 @@ class SqlGeneratorUnitTests {
}
@Test // DATAJDBC-264
void getInsertForEmptyColumnList() {
void getInsertForEmptyColumnListPostgres() {
SqlGenerator sqlGenerator = createSqlGenerator(IdOnlyEntity.class);
SqlGenerator sqlGenerator = createSqlGenerator(IdOnlyEntity.class, PostgresDialect.INSTANCE);
String insert = sqlGenerator.getInsert(emptySet());
String insertSqlStatement = sqlGenerator.getInsert(emptySet());
assertThat(insert).endsWith("()");
assertThat(insertSqlStatement).endsWith(" VALUES (DEFAULT) ");
}
@Test //DATAJDBC-557
void gerInsertForEmptyColumnListMsSqlServer() {
SqlGenerator sqlGenerator = createSqlGenerator(IdOnlyEntity.class, SqlServerDialect.INSTANCE);
String insertSqlStatement = sqlGenerator.getInsert(emptySet());
assertThat(insertSqlStatement).endsWith(" DEFAULT VALUES ");
}
@Test // DATAJDBC-334

View File

@@ -37,6 +37,8 @@ DROP TABLE WITH_READ_ONLY;
DROP TABLE VERSIONED_AGGREGATE;
DROP TABLE WITH_LOCAL_DATE_TIME;
DROP TABLE WITH_ID_ONLY;
CREATE TABLE LEGO_SET
(
"id1" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY,
@@ -350,4 +352,9 @@ CREATE TABLE WITH_LOCAL_DATE_TIME
(
ID BIGINT NOT NULL PRIMARY KEY,
TEST_TIME TIMESTAMP(9)
);
CREATE TABLE WITH_ID_ONLY
(
ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY
);

View File

@@ -321,4 +321,9 @@ CREATE TABLE WITH_LOCAL_DATE_TIME
(
ID BIGINT PRIMARY KEY,
TEST_TIME TIMESTAMP(9) WITHOUT TIME ZONE
);
CREATE TABLE WITH_ID_ONLY
(
ID SERIAL PRIMARY KEY
);

View File

@@ -323,4 +323,9 @@ CREATE TABLE WITH_LOCAL_DATE_TIME
(
ID BIGINT PRIMARY KEY,
TEST_TIME TIMESTAMP(9)
);
);
CREATE TABLE WITH_ID_ONLY
(
ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY
)

View File

@@ -296,4 +296,9 @@ CREATE TABLE WITH_LOCAL_DATE_TIME
(
ID BIGINT PRIMARY KEY,
TEST_TIME TIMESTAMP(6)
);
CREATE TABLE WITH_ID_ONLY
(
ID BIGINT AUTO_INCREMENT PRIMARY KEY
);

View File

@@ -324,4 +324,11 @@ CREATE TABLE WITH_LOCAL_DATE_TIME
(
ID BIGINT PRIMARY KEY,
TEST_TIME datetime2(7)
);
DROP TABLE IF EXISTS WITH_ID_ONLY;
CREATE TABLE WITH_ID_ONLY
(
ID BIGINT IDENTITY PRIMARY KEY
);

View File

@@ -301,4 +301,9 @@ CREATE TABLE WITH_LOCAL_DATE_TIME
(
ID BIGINT PRIMARY KEY,
TEST_TIME TIMESTAMP(6)
);
CREATE TABLE WITH_ID_ONLY
(
ID BIGINT AUTO_INCREMENT PRIMARY KEY
);

View File

@@ -28,6 +28,7 @@ DROP TABLE NO_ID_MAP_CHAIN4 CASCADE CONSTRAINTS PURGE;
DROP TABLE VERSIONED_AGGREGATE CASCADE CONSTRAINTS PURGE;
DROP TABLE WITH_READ_ONLY CASCADE CONSTRAINTS PURGE;
DROP TABLE WITH_LOCAL_DATE_TIME CASCADE CONSTRAINTS PURGE;
DROP TABLE WITH_ID_ONLY CASCADE CONSTRAINTS PURGE;
CREATE TABLE LEGO_SET
(
@@ -332,4 +333,9 @@ CREATE TABLE WITH_LOCAL_DATE_TIME
(
ID NUMBER PRIMARY KEY,
TEST_TIME TIMESTAMP(9)
);
CREATE TABLE WITH_ID_ONLY
(
ID NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY
);

View File

@@ -12,6 +12,7 @@ DROP TABLE CHAIN2;
DROP TABLE CHAIN1;
DROP TABLE CHAIN0;
DROP TABLE WITH_READ_ONLY;
DROP TABLE WITH_ID_ONLY;
CREATE TABLE LEGO_SET
(
@@ -335,4 +336,9 @@ CREATE TABLE WITH_LOCAL_DATE_TIME
(
ID BIGINT PRIMARY KEY,
TEST_TIME TIMESTAMP(9) WITHOUT TIME ZONE
);
CREATE TABLE WITH_ID_ONLY
(
ID SERIAL PRIMARY KEY
);

View File

@@ -32,6 +32,7 @@ import org.springframework.data.relational.core.sql.render.SelectRenderContext;
* @author Jens Schauder
* @author Myeonghyeon Lee
* @author Christoph Strobl
* @author Mikhail Polivakha
* @since 1.1
*/
public interface Dialect {
@@ -109,4 +110,13 @@ public interface Dialect {
default Set<Class<?>> simpleTypes() {
return Collections.emptySet();
}
/**
* @return an appropriate {@link InsertWithDefaultValues } for that specific dialect.
* for most of the Dialects the default implementation will be valid, but, for
* example, in case of {@link SqlServerDialect} it is not
*/
default InsertWithDefaultValues getSqlInsertWithDefaultValues() {
return new InsertWithDefaultValues() {};
}
}

View File

@@ -0,0 +1,17 @@
package org.springframework.data.relational.core.dialect;
import org.springframework.data.relational.core.mapping.InsertDefaultValues;
/**
* This interface aggregates information about an Insert with default values statement.
* @author Mikhail Polivakha
*/
public interface InsertWithDefaultValues {
/**
* @return the part of the sql statement, that follows after <b>INSERT INTO table</b>
*/
default String getDefaultInsertPart() {
return InsertDefaultValues.DEFAULT.getDefaultInsertPart();
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.relational.core.dialect;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.render.InsertRenderContext;
import org.springframework.data.relational.core.sql.render.NamingStrategies;
import org.springframework.data.relational.core.sql.render.RenderContext;
import org.springframework.data.relational.core.sql.render.RenderNamingStrategy;
@@ -26,6 +27,7 @@ import org.springframework.util.Assert;
* Factory for {@link RenderContext} based on {@link Dialect}.
*
* @author Mark Paluch
* @author Mikhail Polivakha
* @since 1.1
*/
public class RenderContextFactory {
@@ -65,9 +67,9 @@ public class RenderContextFactory {
*/
public RenderContext createRenderContext() {
SelectRenderContext select = dialect.getSelectContext();
SelectRenderContext selectRenderContext = dialect.getSelectContext();
return new DialectRenderContext(namingStrategy, dialect.getIdentifierProcessing(), select);
return new DialectRenderContext(namingStrategy, dialect, selectRenderContext);
}
/**
@@ -76,17 +78,18 @@ public class RenderContextFactory {
static class DialectRenderContext implements RenderContext {
private final RenderNamingStrategy renderNamingStrategy;
private final IdentifierProcessing identifierProcessing;
private final SelectRenderContext selectRenderContext;
private final Dialect renderingDialect;
DialectRenderContext(RenderNamingStrategy renderNamingStrategy, IdentifierProcessing identifierProcessing, SelectRenderContext selectRenderContext) {
DialectRenderContext(RenderNamingStrategy renderNamingStrategy, Dialect renderingDialect, SelectRenderContext selectRenderContext) {
Assert.notNull(renderNamingStrategy, "RenderNamingStrategy must not be null");
Assert.notNull(identifierProcessing, "IdentifierProcessing must not be null");
Assert.notNull(renderingDialect, "renderingDialect must not be null");
Assert.notNull(renderingDialect.getIdentifierProcessing(), "IdentifierProcessing of renderingDialect must not be null");
Assert.notNull(selectRenderContext, "SelectRenderContext must not be null");
this.renderNamingStrategy = renderNamingStrategy;
this.identifierProcessing = identifierProcessing;
this.renderingDialect = renderingDialect;
this.selectRenderContext = selectRenderContext;
}
@@ -105,7 +108,7 @@ public class RenderContextFactory {
*/
@Override
public IdentifierProcessing getIdentifierProcessing() {
return identifierProcessing;
return renderingDialect.getIdentifierProcessing();
}
/*
@@ -113,8 +116,18 @@ public class RenderContextFactory {
* @see org.springframework.data.relational.core.sql.render.RenderContext#getSelect()
*/
@Override
public SelectRenderContext getSelect() {
public SelectRenderContext getSelectRenderContext() {
return selectRenderContext;
}
@Override
public InsertRenderContext getInsertRenderContext() {
return new InsertRenderContext() {
@Override
public String getInsertDefaultValuesPartSQL() {
return renderingDialect.getSqlInsertWithDefaultValues().getDefaultInsertPart();
}
};
}
}
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.relational.core.dialect;
import org.springframework.data.relational.core.mapping.InsertDefaultValues;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.LockOptions;
import org.springframework.data.relational.core.sql.render.SelectRenderContext;
@@ -26,6 +27,7 @@ import org.springframework.data.util.Lazy;
* @author Mark Paluch
* @author Myeonghyeon Lee
* @author Jens Schauder
* @author Mikhail Polivakha
* @since 1.1
*/
public class SqlServerDialect extends AbstractDialect {
@@ -150,4 +152,14 @@ public class SqlServerDialect extends AbstractDialect {
public IdentifierProcessing getIdentifierProcessing() {
return IdentifierProcessing.NONE;
}
@Override
public InsertWithDefaultValues getSqlInsertWithDefaultValues() {
return new InsertWithDefaultValues() {
@Override
public String getDefaultInsertPart() {
return InsertDefaultValues.MS_SQL_SERVER.getDefaultInsertPart();
}
};
}
}

View File

@@ -0,0 +1,28 @@
package org.springframework.data.relational.core.mapping;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.dialect.InsertWithDefaultValues;
/**
* In the scope of Insert with default values SQL statement, for example
* <b>INSERT INTO SCHEMA.TABLE VALUES (DEFAULT)</b>
* this enum represents the default values part in different {@link Dialect}s
*
* @author Mikhail Polivakha
* @see InsertWithDefaultValues
*/
public enum InsertDefaultValues {
DEFAULT(" VALUES (DEFAULT) "),
MS_SQL_SERVER(" DEFAULT VALUES ");
private final String defaultInsertPart;
InsertDefaultValues(String defaultInsertPart) {
this.defaultInsertPart = defaultInsertPart;
}
public String getDefaultInsertPart() {
return defaultInsertPart;
}
}

View File

@@ -0,0 +1,18 @@
package org.springframework.data.relational.core.sql.render;
import org.springframework.data.relational.core.mapping.InsertDefaultValues;
import org.springframework.data.relational.core.sql.Insert;
/**
* This interface encapsulates the details about how to
* process {@link Insert} SQL statement
*
* @see RenderContext
* @author Mikhail Polivakha
*/
public interface InsertRenderContext {
default String getInsertDefaultValuesPartSQL() {
return InsertDefaultValues.DEFAULT.getDefaultInsertPart();
}
}

View File

@@ -15,51 +15,42 @@
*/
package org.springframework.data.relational.core.sql.render;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.Insert;
import org.springframework.data.relational.core.sql.Into;
import org.springframework.data.relational.core.sql.Values;
import org.springframework.data.relational.core.sql.Visitable;
import org.springframework.util.Assert;
/**
* {@link PartRenderer} for {@link Insert} statements.
*
* @author Mark Paluch
* @author Jens Schauder
* @author Mikhail Polivakha
* @since 1.1
*/
class InsertStatementVisitor extends DelegatingVisitor implements PartRenderer {
private StringBuilder builder = new StringBuilder();
private StringBuilder into = new StringBuilder();
private StringBuilder columns = new StringBuilder();
private StringBuilder values = new StringBuilder();
private final StringBuilder builder = new StringBuilder();
private final StringBuilder into = new StringBuilder();
private final StringBuilder columns = new StringBuilder();
private final StringBuilder values = new StringBuilder();
private IntoClauseVisitor intoClauseVisitor;
private ColumnVisitor columnVisitor;
private ValuesVisitor valuesVisitor;
private final IntoClauseVisitor intoClauseVisitor;
private final ColumnVisitor columnVisitor;
private final ValuesVisitor valuesVisitor;
private final RenderContext renderContext;
InsertStatementVisitor(RenderContext context) {
InsertStatementVisitor(RenderContext renderContext) {
this.intoClauseVisitor = new IntoClauseVisitor(context, it -> {
Assert.notNull(renderContext, "renderContext must not be null!");
if (into.length() != 0) {
into.append(", ");
}
into.append(it);
});
this.columnVisitor = new ColumnVisitor(context, false, it -> {
if (columns.length() != 0) {
columns.append(", ");
}
columns.append(it);
});
this.valuesVisitor = new ValuesVisitor(context, values::append);
this.renderContext = renderContext;
this.intoClauseVisitor = createIntoClauseVisitor(renderContext);
this.columnVisitor = createColumnVisitor(renderContext);
this.valuesVisitor = new ValuesVisitor(renderContext, values::append);
}
/*
@@ -97,11 +88,9 @@ class InsertStatementVisitor extends DelegatingVisitor implements PartRenderer {
builder.append(" INTO ").append(into);
if (columns.length() != 0) {
builder.append(" (").append(columns).append(")");
}
addInsertColumnsIfPresent();
builder.append(" VALUES (").append(values).append(")");
addInsertValuesIfPresentElseDefault();
return Delegation.leave();
}
@@ -109,6 +98,24 @@ class InsertStatementVisitor extends DelegatingVisitor implements PartRenderer {
return Delegation.retain();
}
private void addInsertValuesIfPresentElseDefault() {
if (values.length() != 0) {
builder.append(" VALUES (").append(values).append(")");
} else {
addInsertWithDefaultValuesToBuilder();
}
}
private void addInsertColumnsIfPresent() {
if (columns.length() != 0) {
builder.append(" (").append(columns).append(")");
}
}
private void addInsertWithDefaultValuesToBuilder() {
builder.append(renderContext.getInsertRenderContext().getInsertDefaultValuesPartSQL());
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.render.PartRenderer#getRenderedPart()
@@ -117,4 +124,28 @@ class InsertStatementVisitor extends DelegatingVisitor implements PartRenderer {
public CharSequence getRenderedPart() {
return builder;
}
@NotNull
private ColumnVisitor createColumnVisitor(RenderContext context) {
return new ColumnVisitor(context, false, it -> {
if (columns.length() != 0) {
columns.append(", ");
}
columns.append(it);
});
}
@NotNull
private IntoClauseVisitor createIntoClauseVisitor(RenderContext context) {
return new IntoClauseVisitor(context, it -> {
if (into.length() != 0) {
into.append(", ");
}
into.append(it);
});
}
}

View File

@@ -21,6 +21,7 @@ import org.springframework.data.relational.core.sql.IdentifierProcessing;
* Render context providing {@link RenderNamingStrategy} and other resources that are required during rendering.
*
* @author Mark Paluch
* @author Mikhail Polivakha
* @since 1.1
*/
public interface RenderContext {
@@ -43,5 +44,10 @@ public interface RenderContext {
/**
* @return the {@link SelectRenderContext}.
*/
SelectRenderContext getSelect();
SelectRenderContext getSelectRenderContext();
/**
* @return the {@link InsertRenderContext}
*/
InsertRenderContext getInsertRenderContext();
}

View File

@@ -50,7 +50,7 @@ class SelectStatementVisitor extends DelegatingVisitor implements PartRenderer {
SelectStatementVisitor(RenderContext context) {
this.context = context;
this.selectRenderContext = context.getSelect();
this.selectRenderContext = context.getSelectRenderContext();
this.selectListVisitor = new SelectListVisitor(context, selectList::append);
this.orderByClauseVisitor = new OrderByClauseVisitor(context);
this.fromClauseVisitor = new FromClauseVisitor(context, it -> {

View File

@@ -37,10 +37,15 @@ final class SimpleRenderContext implements RenderContext {
}
@Override
public SelectRenderContext getSelect() {
public SelectRenderContext getSelectRenderContext() {
return DefaultSelectRenderContext.INSTANCE;
}
@Override
public InsertRenderContext getInsertRenderContext() {
return new InsertRenderContext() {};
}
public RenderNamingStrategy getNamingStrategy() {
return this.namingStrategy;
}

View File

@@ -69,16 +69,6 @@ public class SqlRenderer implements Renderer {
return create().render(select);
}
/**
* Renders a {@link Insert} statement into its SQL representation.
*
* @param insert must not be {@literal null}.
* @return the rendered statement.
*/
public static String toString(Insert insert) {
return create().render(insert);
}
/**
* Renders a {@link Update} statement into its SQL representation.
*
@@ -120,10 +110,8 @@ public class SqlRenderer implements Renderer {
*/
@Override
public String render(Insert insert) {
InsertStatementVisitor visitor = new InsertStatementVisitor(context);
insert.visit(visitor);
return visitor.getRenderedPart().toString();
}

View File

@@ -68,7 +68,7 @@ public class InsertRendererUnitTests {
Insert insert = Insert.builder().into(bar).build();
assertThat(SqlRenderer.toString(insert)).isEqualTo("INSERT INTO bar VALUES ()");
assertThat(SqlRenderer.toString(insert)).isEqualTo("INSERT INTO bar VALUES (DEFAULT)");
}
}