Refine table and column name generation.

We revised the table and column name generation by unifying the generation code into CqlIdentifierGenerator. We also properly distinguish between generated names that are generated and those provided by the application (i.e. through annotations).

Closes #1263
This commit is contained in:
Mark Paluch
2022-07-05 13:36:02 +02:00
parent c64978e73d
commit c120e8f050
11 changed files with 216 additions and 137 deletions

View File

@@ -37,7 +37,6 @@ import org.springframework.data.cassandra.core.mapping.SimpleUserTypeResolver;
import org.springframework.data.cassandra.core.mapping.Table;
import org.springframework.data.cassandra.core.mapping.UserTypeResolver;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.domain.ManagedTypes;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.lang.Nullable;
@@ -101,7 +100,7 @@ public abstract class AbstractCassandraConfiguration extends AbstractSessionConf
* Return the {@link MappingContext} instance to map Entities to {@link Object Java Objects}.
*
* @see org.springframework.data.cassandra.core.mapping.CassandraMappingContext
* @deprecated since 4.0, use {@link #cassandraMappingContext(ManagedTypes)} instead.
* @deprecated since 4.0, use {@link #cassandraMappingContext(CassandraManagedTypes)} instead.
*/
@Deprecated(since = "4.0", forRemoval = true)
public CassandraMappingContext cassandraMapping() throws ClassNotFoundException {

View File

@@ -15,15 +15,17 @@
*/
package org.springframework.data.cassandra.core.mapping;
import java.lang.annotation.Annotation;
import java.util.Comparator;
import java.util.Optional;
import java.util.function.BiFunction;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.BeanFactoryAccessor;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.data.cassandra.util.SpelUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.MappingException;
@@ -32,7 +34,6 @@ import org.springframework.data.util.TypeInformation;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.datastax.oss.driver.api.core.CqlIdentifier;
@@ -49,14 +50,14 @@ public class BasicCassandraPersistentEntity<T> extends BasicPersistentEntity<T,
private static final CassandraPersistentEntityMetadataVerifier DEFAULT_VERIFIER = new CompositeCassandraPersistentEntityMetadataVerifier();
private final CqlIdentifierGenerator namingAccessor = new CqlIdentifierGenerator();
private Boolean forceQuote;
private CassandraPersistentEntityMetadataVerifier verifier = DEFAULT_VERIFIER;
private CqlIdentifier tableName;
private NamingStrategy namingStrategy = NamingStrategy.INSTANCE;
private @Nullable StandardEvaluationContext spelContext;
/**
@@ -101,27 +102,20 @@ public class BasicCassandraPersistentEntity<T> extends BasicPersistentEntity<T,
}
protected CqlIdentifier determineTableName() {
Table annotation = findAnnotation(Table.class);
if (annotation != null) {
return determineName(annotation.value(), annotation.forceQuote());
}
return IdentifierFactory.create(getNamingStrategy().getTableName(this), false);
return determineTableName(NamingStrategy::getTableName, findAnnotation(Table.class));
}
CqlIdentifier determineName(String value, boolean forceQuote) {
CqlIdentifier determineTableName(
BiFunction<NamingStrategy, CassandraPersistentEntity<?>, String> defaultNameGenerator,
@Nullable Annotation annotation) {
if (!StringUtils.hasText(value)) {
return IdentifierFactory.create(getNamingStrategy().getTableName(this), forceQuote);
if (annotation != null) {
return this.namingAccessor.generate((String) AnnotationUtils.getValue(annotation),
(Boolean) AnnotationUtils.getValue(annotation, "forceQuote"), defaultNameGenerator, this, this.spelContext);
}
String name = Optional.ofNullable(this.spelContext).map(it -> SpelUtils.evaluate(value, it)).orElse(value);
Assert.state(name != null, () -> String.format("Cannot determine default name for %s", this));
return IdentifierFactory.create(name, forceQuote);
return this.namingAccessor.generate(null, forceQuote != null ? forceQuote : false, defaultNameGenerator, this,
this.spelContext);
}
@Override
@@ -168,7 +162,7 @@ public class BasicCassandraPersistentEntity<T> extends BasicPersistentEntity<T,
this.forceQuote = forceQuote;
if (changed) {
setTableName(IdentifierFactory.create(getTableName().asInternal(), forceQuote));
setTableName(CqlIdentifierGenerator.createIdentifier(getTableName().asInternal(), forceQuote));
}
}
@@ -187,14 +181,7 @@ public class BasicCassandraPersistentEntity<T> extends BasicPersistentEntity<T,
* @since 3.0
*/
public void setNamingStrategy(NamingStrategy namingStrategy) {
Assert.notNull(namingStrategy, "NamingStrategy must not be null");
this.namingStrategy = namingStrategy;
}
NamingStrategy getNamingStrategy() {
return namingStrategy;
this.namingAccessor.setNamingStrategy(namingStrategy);
}
@Override

View File

@@ -22,7 +22,6 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -31,7 +30,6 @@ import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.cassandra.core.cql.Ordering;
import org.springframework.data.cassandra.core.cql.PrimaryKeyType;
import org.springframework.data.cassandra.util.SpelUtils;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty;
import org.springframework.data.mapping.model.Property;
@@ -43,7 +41,6 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import com.datastax.oss.driver.api.core.CqlIdentifier;
@@ -60,13 +57,13 @@ import com.datastax.oss.driver.api.core.CqlIdentifier;
public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentProperty<CassandraPersistentProperty>
implements CassandraPersistentProperty, ApplicationContextAware {
private final CqlIdentifierGenerator namingAccessor = new CqlIdentifierGenerator();
// Indicates whether this property has been explicitly instructed to force quoted column names.
private Boolean forceQuote;
private @Nullable CqlIdentifier columnName;
private NamingStrategy namingStrategy = NamingStrategy.INSTANCE;
private @Nullable StandardEvaluationContext spelContext;
/**
@@ -168,8 +165,6 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP
return null;
}
Supplier<String> defaultName = () -> getNamingStrategy().getColumnName(this);
String overriddenName = null;
boolean forceQuote = false;
@@ -200,7 +195,7 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP
}
}
return createColumnName(defaultName, overriddenName, forceQuote);
return namingAccessor.generate(overriddenName, forceQuote, NamingStrategy::getColumnName, this, this.spelContext);
}
@Override
@@ -229,21 +224,6 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP
}
}
@Nullable
private CqlIdentifier createColumnName(Supplier<String> defaultName, @Nullable String overriddenName,
boolean forceQuote) {
String name;
if (StringUtils.hasText(overriddenName)) {
name = this.spelContext != null ? SpelUtils.evaluate(overriddenName, this.spelContext) : overriddenName;
} else {
name = defaultName.get();
}
return name != null ? IdentifierFactory.create(name, forceQuote) : null;
}
@Override
public void setColumnName(CqlIdentifier columnName) {
@@ -259,14 +239,7 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP
* @since 3.0
*/
public void setNamingStrategy(NamingStrategy namingStrategy) {
Assert.notNull(namingStrategy, "NamingStrategy must not be null");
this.namingStrategy = namingStrategy;
}
NamingStrategy getNamingStrategy() {
return this.namingStrategy;
this.namingAccessor.setNamingStrategy(namingStrategy);
}
@Override
@@ -277,7 +250,7 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP
this.forceQuote = forceQuote;
if (changed) {
setColumnName(IdentifierFactory.create(getRequiredColumnName().asInternal(), forceQuote));
setColumnName(CqlIdentifierGenerator.createIdentifier(getRequiredColumnName().asInternal(), forceQuote));
}
}

View File

@@ -73,7 +73,7 @@ public class CassandraMappingContext
private Mapping mapping = new Mapping();
private NamingStrategy namingStrategy = NamingStrategy.INSTANCE;
private @Nullable NamingStrategy namingStrategy;
private @Deprecated @Nullable UserTypeResolver userTypeResolver;
@@ -128,7 +128,8 @@ public class CassandraMappingContext
String entityTableName = entityMapping.getTableName();
if (StringUtils.hasText(entityTableName)) {
entity.setTableName(IdentifierFactory.create(entityTableName, Boolean.valueOf(entityMapping.getForceQuote())));
entity.setTableName(
CqlIdentifierGenerator.createIdentifier(entityTableName, Boolean.valueOf(entityMapping.getForceQuote())));
}
processMappingOverrides(entity, entityMapping);
@@ -160,7 +161,7 @@ public class CassandraMappingContext
property.setForceQuote(forceQuote);
if (StringUtils.hasText(mapping.getColumnName())) {
property.setColumnName(IdentifierFactory.create(mapping.getColumnName(), forceQuote));
property.setColumnName(CqlIdentifierGenerator.createIdentifier(mapping.getColumnName(), forceQuote));
}
}
@@ -191,7 +192,7 @@ public class CassandraMappingContext
/**
* @deprecated since 3.0. Use custom conversion through
* {@link org.springframework.data.cassandra.core.convert.MappingCassandraConverter}.
* {@link org.springframework.data.cassandra.core.convert.MappingCassandraConverter}.
*/
@Deprecated
public CustomConversions getCustomConversions() {
@@ -246,7 +247,7 @@ public class CassandraMappingContext
/**
* @deprecated since 3.0. Retrieve {@link CodecRegistry} directly from
* {@link org.springframework.data.cassandra.core.convert.CassandraConverter}.
* {@link org.springframework.data.cassandra.core.convert.CassandraConverter}.
*/
@Deprecated
public CodecRegistry getCodecRegistry() {
@@ -295,7 +296,7 @@ public class CassandraMappingContext
/**
* @deprecated since 3.0. Retrieve {@link UserTypeResolver} directly from
* {@link org.springframework.data.cassandra.core.convert.CassandraConverter}.
* {@link org.springframework.data.cassandra.core.convert.CassandraConverter}.
*/
@Nullable
@Deprecated
@@ -340,7 +341,6 @@ public class CassandraMappingContext
if (!entity.isUserDefinedType() && !entity.isTupleType() && entity.isAnnotationPresent(Table.class)) {
this.tableEntities.add(entity);
}
});
return optional;
@@ -357,11 +357,12 @@ public class CassandraMappingContext
BasicCassandraPersistentEntity<T> entity = isUserDefinedType(typeInformation)
? new CassandraUserTypePersistentEntity<>(typeInformation, getVerifier())
: isTuple(typeInformation)
? new BasicCassandraPersistentTupleEntity<>(typeInformation)
: new BasicCassandraPersistentEntity<>(typeInformation, getVerifier());
: isTuple(typeInformation) ? new BasicCassandraPersistentTupleEntity<>(typeInformation)
: new BasicCassandraPersistentEntity<>(typeInformation, getVerifier());
entity.setNamingStrategy(this.namingStrategy);
if (this.namingStrategy != null) {
entity.setNamingStrategy(this.namingStrategy);
}
Optional.ofNullable(this.applicationContext).ifPresent(entity::setApplicationContext);
return entity;
@@ -383,7 +384,10 @@ public class CassandraMappingContext
? new BasicCassandraPersistentTupleProperty(property, owner, simpleTypeHolder)
: new CachingCassandraPersistentProperty(property, owner, simpleTypeHolder);
persistentProperty.setNamingStrategy(this.namingStrategy);
if (this.namingStrategy != null) {
persistentProperty.setNamingStrategy(this.namingStrategy);
}
Optional.ofNullable(this.applicationContext).ifPresent(persistentProperty::setApplicationContext);
return persistentProperty;
@@ -424,9 +428,7 @@ public class CassandraMappingContext
return getPersistentEntities().stream().flatMap(entity -> StreamSupport.stream(entity.spliterator(), false))
.flatMap(it -> Optionals.toStream(Optional.ofNullable(it.findAnnotation(CassandraType.class))))
.map(CassandraType::userTypeName)
.filter(StringUtils::hasText)
.map(CqlIdentifier::fromCql)
.map(CassandraType::userTypeName).filter(StringUtils::hasText).map(CqlIdentifier::fromCql)
.anyMatch(identifier::equals);
}
}

View File

@@ -43,14 +43,7 @@ public class CassandraUserTypePersistentEntity<T> extends BasicCassandraPersiste
@Override
protected CqlIdentifier determineTableName() {
UserDefinedType annotation = findAnnotation(UserDefinedType.class);
if (annotation != null) {
return determineName(annotation.value(), annotation.forceQuote());
}
return IdentifierFactory.create(getNamingStrategy().getUserDefinedTypeName(this), false);
return determineTableName(NamingStrategy::getUserDefinedTypeName, findAnnotation(UserDefinedType.class));
}
@Override

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.cassandra.core.mapping;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.springframework.data.cassandra.util.SpelUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.internal.core.util.Strings;
/**
* Strategy class to generate {@link CqlIdentifier identifier names} using {@link NamingStrategy} and contextual details
* from entities and properties.
*
* @author Mark Paluch
* @since 3.4.2
*/
class CqlIdentifierGenerator {
private @Nullable NamingStrategy namingStrategy;
static CqlIdentifier createIdentifier(String simpleName, boolean forceQuote) {
if (Strings.isDoubleQuoted(simpleName)) {
return CqlIdentifier.fromCql(simpleName);
}
if (forceQuote || Strings.needsDoubleQuotes(simpleName)) {
return CqlIdentifier.fromInternal(simpleName);
}
return CqlIdentifier.fromCql(simpleName);
}
/**
* Generate a {@link CqlIdentifier name} using the provided name or fall back to the default {@link Function name
* generator} using a {@link NamingStrategy}.
*
* @param providedName the name to use if provided.
* @param forceQuote whether to enforce quoting.
* @param defaultNameGenerator the default name generator.
* @param source source to be used for name generation.
* @param spelContext the SpEL evaluation context for evaluating SpEL expressions provided through
* {@code providedName}.
* @return the generated name.
*/
public <T> CqlIdentifier generate(@Nullable String providedName, boolean forceQuote,
BiFunction<NamingStrategy, T, String> defaultNameGenerator, T source, @Nullable EvaluationContext spelContext) {
String name;
boolean useForceQuote = forceQuote;
if (StringUtils.hasText(providedName)) {
name = spelContext != null ? SpelUtils.evaluate(providedName, spelContext) : providedName;
useForceQuote = true;
} else {
name = defaultNameGenerator.apply(getNamingStrategy(forceQuote), source);
}
Assert.state(name != null, () -> String.format("Cannot determine default name for %s", source));
return createIdentifier(name, useForceQuote);
}
public void setNamingStrategy(@Nullable NamingStrategy namingStrategy) {
Assert.notNull(namingStrategy, "NamingStrategy must not be null");
this.namingStrategy = namingStrategy;
}
private NamingStrategy getNamingStrategy(boolean forceQuote) {
if (namingStrategy == null) {
if (forceQuote) {
return new NamingStrategy() {};
} else {
return NamingStrategy.INSTANCE;
}
}
return namingStrategy;
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2019-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.cassandra.core.mapping;
import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.internal.core.util.Strings;
/**
* Factory for {@link CqlIdentifier}.
*
* @author Mark Paluch
*/
class IdentifierFactory {
static CqlIdentifier create(String simpleName, boolean forceQuote) {
if (forceQuote) {
return CqlIdentifier.fromCql("\"" + simpleName + "\"");
}
if (Strings.needsDoubleQuotes(simpleName.toLowerCase())) {
return CqlIdentifier.fromCql("\"" + simpleName.toLowerCase() + "\"");
}
return CqlIdentifier.fromInternal(simpleName.toLowerCase());
}
}

View File

@@ -15,13 +15,15 @@
*/
package org.springframework.data.cassandra.core.mapping;
import java.util.Locale;
import java.util.function.UnaryOperator;
import org.springframework.util.Assert;
/**
* Interface and default implementation of a naming strategy. Defaults to table name based on {@link Class} and column
* name based on property names.
* name based on property names. Names are used as-is without quoting. Lower-case, non-keyword names are used without
* quoting. Upper-case, keyword or other names requiring quoting are used with quotes.
* <p>
* NOTE: Can also be used as an adapter. Create a lambda or an anonymous subclass and override any settings to implement
* a different strategy on the fly.
@@ -35,8 +37,15 @@ public interface NamingStrategy {
* Empty implementation of the interface utilizing only the default implementation.
* <p>
* Using this avoids creating essentially the same class over and over again.
*
* @since 3.3.6
*/
NamingStrategy INSTANCE = new NamingStrategy() {};
NamingStrategy CASE_SENSITIVE = new NamingStrategy() {};
/**
* Default implementation converting all names to {@link String#toLowerCase()}.
*/
NamingStrategy INSTANCE = CASE_SENSITIVE.transform(s -> s.toLowerCase(Locale.ROOT));
/**
* Naming strategy that renders CamelCase name parts to {@code snake_case}.
@@ -74,10 +83,8 @@ public interface NamingStrategy {
}
/**
* Apply a {@link UnaryOperator transformation function} to create a new {@link NamingStrategy}
* that applies the given transformation to each name component.
*
* Example:
* Apply a {@link UnaryOperator transformation function} to create a new {@link NamingStrategy} that applies the given
* transformation to each name component. Example:
* <p class="code">
* NamingStrategy lower = NamingStrategy.INSTANCE.transform(String::toLowerCase);
* </p>

View File

@@ -52,12 +52,12 @@ class BasicCassandraPersistentEntityUnitTests {
@Mock ApplicationContext context;
@Test
void subclassInheritsAtDocumentAnnotation() {
void subclassInheritsAtTableAnnotation() {
BasicCassandraPersistentEntity<Notification> entity = new BasicCassandraPersistentEntity<>(
ClassTypeInformation.from(Notification.class));
assertThat(entity.getTableName()).hasToString("messages");
assertThat(entity.getTableName().asCql(true)).isEqualTo("messages");
}
@Test

View File

@@ -21,6 +21,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.util.StringUtils;
import com.datastax.oss.driver.api.core.CqlIdentifier;
@@ -38,7 +40,48 @@ class NamingStrategyUnitTests {
context.setUserTypeResolver(typeName -> {
throw new IllegalStateException("");
});
context.setNamingStrategy(NamingStrategy.INSTANCE);
}
@Test
void getTableNameGeneratesTableName() {
BasicCassandraPersistentEntity<TableNameHolderThingy> entity = new BasicCassandraPersistentEntity<>(
ClassTypeInformation.from(TableNameHolderThingy.class));
assertThat(entity.getTableName().asCql(true)).isEqualTo("tablenameholderthingy");
}
@Test
void getTableNameGeneratesQuotedTableName() {
BasicCassandraPersistentEntity<TableNameHolderThingy> entity = new BasicCassandraPersistentEntity<>(
ClassTypeInformation.from(TableNameHolderThingy.class));
entity.setNamingStrategy(NamingStrategy.SNAKE_CASE.transform(it -> "\"" + StringUtils.capitalize(it) + "\""));
assertThat(entity.getTableName().asCql(true)).isEqualTo("\"Table_name_holder_thingy\"");
entity = new BasicCassandraPersistentEntity<>(ClassTypeInformation.from(TableNameHolderThingy.class));
entity.setNamingStrategy(NamingStrategy.SNAKE_CASE.transform(String::toUpperCase));
assertThat(entity.getTableName().asCql(true)).isEqualTo("\"TABLE_NAME_HOLDER_THINGY\"");
}
@Test
void atTableIsCaseSensitive() {
BasicCassandraPersistentEntity<ProvidedTableName> entity = new BasicCassandraPersistentEntity<>(
ClassTypeInformation.from(ProvidedTableName.class));
assertThat(entity.getTableName().asCql(true)).isEqualTo("\"iAmProvided\"");
}
@Test
void atTableWithQuotedNameShouldRetainQuotes() {
BasicCassandraPersistentEntity<QuotedTableName> entity = new BasicCassandraPersistentEntity<>(
ClassTypeInformation.from(QuotedTableName.class));
assertThat(entity.getTableName().asCql(true)).isEqualTo("\"IAmQuoted\"");
}
@Test // DATACASS-84
@@ -105,6 +148,17 @@ class NamingStrategyUnitTests {
@Id String firstName;
}
@Table("messages")
static class Message {}
static class TableNameHolderThingy {}
@Table("iAmProvided")
private static class ProvidedTableName {}
@Table("\"IAmQuoted\"")
private static class QuotedTableName {}
@UserDefinedType
private static class MyUserType {
@@ -122,4 +176,5 @@ class NamingStrategyUnitTests {
String firstName;
}
}

View File

@@ -138,8 +138,8 @@ The conventions are:
* The simple (short) Java class name is mapped to the table name by being changed to lower case.
For example, `com.bigbank.SavingsAccount` maps to a table named `savingsaccount`.
* The converter uses any registered Spring `Converter` instances to override the default mapping of object properties to tables fields.
* The properties of an object are used to convert to and from properties in the table.
* The converter uses any registered Spring `Converter` instances to override the default mapping of object properties to tables columns.
* The properties of an object are used to convert to and from columns in the table.
You can adjust conventions by configuring a `NamingStrategy` on `CassandraMappingContext`.
Naming strategy objects implement the convention by which a table, column or user-defined type is derived from an entity class and from an actual property.