DATACASS-84 - Use NamingStrategy for table and column name derivation.

We now use a configurable NamingStrategy to configure how table, user-defined type and column names are derived if the name is not expicitly configured. The default naming strategy uses the type/property name. Naming strategies allow customization with a transformation function (all lower-case/upper case, prepending/appending and more) and strategies can be provided by a custom implementation.
This commit is contained in:
Mark Paluch
2020-02-19 10:09:00 +01:00
committed by John Blum
parent c9978f7a72
commit 290d271f4f
12 changed files with 559 additions and 9 deletions

View File

@@ -55,6 +55,8 @@ public class BasicCassandraPersistentEntity<T> extends BasicPersistentEntity<T,
private CqlIdentifier tableName;
private NamingStrategy namingStrategy = NamingStrategy.INSTANCE;
private @Nullable StandardEvaluationContext spelContext;
/**
@@ -106,13 +108,13 @@ public class BasicCassandraPersistentEntity<T> extends BasicPersistentEntity<T,
return determineName(annotation.value(), annotation.forceQuote());
}
return IdentifierFactory.create(getType().getSimpleName(), false);
return IdentifierFactory.create(namingStrategy.getTableName(this), false);
}
CqlIdentifier determineName(String value, boolean forceQuote) {
if (!StringUtils.hasText(value)) {
return IdentifierFactory.create(getType().getSimpleName(), forceQuote);
return IdentifierFactory.create(namingStrategy.getTableName(this), forceQuote);
}
String name = Optional.ofNullable(this.spelContext).map(it -> SpelUtils.evaluate(value, it)).orElse(value);
@@ -199,6 +201,23 @@ public class BasicCassandraPersistentEntity<T> extends BasicPersistentEntity<T,
this.tableName = tableName;
}
/**
* Set the {@link NamingStrategy} to use.
*
* @param namingStrategy must not be {@literal null}.
* @since 3.0
*/
public void setNamingStrategy(NamingStrategy namingStrategy) {
Assert.notNull(namingStrategy, "NamingStrategy must not be null");
this.namingStrategy = namingStrategy;
}
NamingStrategy getNamingStrategy() {
return namingStrategy;
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity#getTableName()
*/

View File

@@ -22,6 +22,8 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -62,6 +64,8 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP
private @Nullable CqlIdentifier columnName;
private NamingStrategy namingStrategy = NamingStrategy.INSTANCE;
private @Nullable StandardEvaluationContext spelContext;
/**
@@ -180,7 +184,7 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP
return null;
}
String defaultName = getName(); // TODO: replace with naming strategy class
Supplier<String> defaultName = () -> namingStrategy.getColumnName(this);
String overriddenName = null;
boolean forceQuote = false;
@@ -217,12 +221,15 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP
}
@Nullable
private CqlIdentifier createColumnName(String defaultName, @Nullable String overriddenName, boolean forceQuote) {
private CqlIdentifier createColumnName(Supplier<String> defaultName, @Nullable String overriddenName,
boolean forceQuote) {
String name = defaultName;
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;
@@ -239,6 +246,19 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP
this.columnName = columnName;
}
/**
* Set the {@link NamingStrategy} to use.
*
* @param namingStrategy must not be {@literal null}.
* @since 3.0
*/
public void setNamingStrategy(NamingStrategy namingStrategy) {
Assert.notNull(namingStrategy, "NamingStrategy must not be null");
this.namingStrategy = namingStrategy;
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty#setForceQuote(boolean)
*/

View File

@@ -72,6 +72,8 @@ public class CassandraMappingContext
private Mapping mapping = new Mapping();
private NamingStrategy namingStrategy = NamingStrategy.INSTANCE;
private @Deprecated @Nullable UserTypeResolver userTypeResolver;
private @Deprecated CodecRegistry codecRegistry = CodecRegistry.DEFAULT;
@@ -262,6 +264,19 @@ public class CassandraMappingContext
return this.codecRegistry;
}
/**
* Set the {@link NamingStrategy} to use.
*
* @param namingStrategy must not be {@literal null}.
* @since 3.0
*/
public void setNamingStrategy(NamingStrategy namingStrategy) {
Assert.notNull(namingStrategy, "NamingStrategy must not be null");
this.namingStrategy = namingStrategy;
}
/**
* Sets the {@link TupleTypeFactory}.
*
@@ -366,6 +381,7 @@ public class CassandraMappingContext
: isTuple(typeInformation) ? new BasicCassandraPersistentTupleEntity<>(typeInformation)
: new BasicCassandraPersistentEntity<>(typeInformation, getVerifier());
entity.setNamingStrategy(this.namingStrategy);
Optional.ofNullable(this.applicationContext).ifPresent(entity::setApplicationContext);
return entity;
@@ -390,6 +406,7 @@ public class CassandraMappingContext
? new BasicCassandraPersistentTupleProperty(property, owner, simpleTypeHolder)
: new BasicCassandraPersistentProperty(property, owner, simpleTypeHolder);
persistentProperty.setNamingStrategy(this.namingStrategy);
Optional.ofNullable(this.applicationContext).ifPresent(persistentProperty::setApplicationContext);
return persistentProperty;

View File

@@ -53,7 +53,7 @@ public class CassandraUserTypePersistentEntity<T> extends BasicCassandraPersiste
return determineName(annotation.value(), annotation.forceQuote());
}
return super.determineTableName();
return IdentifierFactory.create(getNamingStrategy().getUserDefinedTypeName(this), false);
}
/* (non-Javadoc)

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2017-2020 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.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.
* <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.
*
* @author Mark Paluch
* @since 3.0
*/
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.
*/
NamingStrategy INSTANCE = new NamingStrategy() {};
/**
* Naming strategy that renders CamelCase name parts to {@code snake_case}.
*/
NamingStrategy SNAKE_CASE = new SnakeCaseNamingStrategy();
/**
* Create a table name from the given {@link CassandraPersistentEntity}.
*/
default String getTableName(CassandraPersistentEntity<?> type) {
Assert.notNull(type, "CassandraPersistentEntity must not be null!");
return type.getType().getSimpleName();
}
/**
* Create a user-defined type name from the given {@link CassandraPersistentEntity}.
*/
default String getUserDefinedTypeName(CassandraPersistentEntity<?> type) {
Assert.notNull(type, "CassandraPersistentEntity must not be null!");
return type.getType().getSimpleName();
}
/**
* Create a column name from the given {@link CassandraPersistentProperty property}.
*/
default String getColumnName(CassandraPersistentProperty property) {
Assert.notNull(property, "CassandraPersistentProperty must not be null!");
return property.getName();
}
/**
* 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>
*
* @param mappingFunction must not be {@literal null}.
* @return the {@link NamingStrategy} that applies the given {@link UnaryOperator transformation function}.
*/
default NamingStrategy transform(UnaryOperator<String> mappingFunction) {
Assert.notNull(mappingFunction, "Mapping function must not be null!");
NamingStrategy previous = this;
return new TransformingNamingStrategy(previous, mappingFunction);
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2020 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 org.springframework.data.util.ParsingUtils;
import org.springframework.util.Assert;
/**
* Naming strategy that renders CamelCase name parts to {@code snake_case}.
*
* @author Mark Paluch
* @since 3.0
*/
public class SnakeCaseNamingStrategy implements NamingStrategy {
public SnakeCaseNamingStrategy() {}
/**
* Uses {@link Class#getSimpleName()} and separates camel case parts with '_'.
*/
public String getTableName(CassandraPersistentEntity<?> type) {
Assert.notNull(type, "CassandraPersistentEntity must not be null!");
return ParsingUtils.reconcatenateCamelCase(type.getType().getSimpleName(), "_");
}
/**
* Uses {@link Class#getSimpleName()} and separates camel case parts with '_'.
*/
public String getUserDefinedTypeName(CassandraPersistentEntity<?> type) {
Assert.notNull(type, "CassandraPersistentEntity must not be null!");
return ParsingUtils.reconcatenateCamelCase(type.getType().getSimpleName(), "_");
}
/**
* Uses {@link CassandraPersistentProperty#getName()} and separates camel case parts with '_'.
*/
public String getColumnName(CassandraPersistentProperty property) {
Assert.notNull(property, "CassandraPersistentProperty must not be null.");
return ParsingUtils.reconcatenateCamelCase(property.getName(), "_");
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2020 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.Function;
/**
* {@link NamingStrategy} that applies a transformation {@link Function} after invoking a delegate
* {@link NamingStrategy}.
*
* @author Mark Paluch
* @since 3.0
*/
public class TransformingNamingStrategy implements NamingStrategy {
private final NamingStrategy delegate;
private final Function<String, String> mappingFunction;
public TransformingNamingStrategy(NamingStrategy delegate, Function<String, String> mappingFunction) {
this.delegate = delegate;
this.mappingFunction = mappingFunction;
}
@Override
public String getTableName(CassandraPersistentEntity<?> type) {
return mappingFunction.apply(delegate.getTableName(type));
}
@Override
public String getUserDefinedTypeName(CassandraPersistentEntity<?> type) {
return mappingFunction.apply(delegate.getUserDefinedTypeName(type));
}
@Override
public String getColumnName(CassandraPersistentProperty property) {
return mappingFunction.apply(delegate.getColumnName(property));
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2020 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 static org.assertj.core.api.Assertions.*;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import com.datastax.oss.driver.api.core.CqlIdentifier;
/**
* Unit tests for {@link NamingStrategy}.
*
* @author Mark Paluch
*/
public class NamingStrategyUnitTests {
CassandraMappingContext context = new CassandraMappingContext();
@Before
public void before() {
context.setUserTypeResolver(typeName -> {
throw new IllegalStateException("");
});
context.setNamingStrategy(NamingStrategy.INSTANCE);
}
@Test // DATACASS-84
public void shouldDeriveTableName() {
BasicCassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(PersonTable.class);
assertThat(entity.getTableName()).isEqualTo(CqlIdentifier.fromCql("persontable"));
}
@Test // DATACASS-84
public void shouldDeriveUserDefinedTypeName() {
BasicCassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(MyUserType.class);
assertThat(entity.getTableName()).isEqualTo(CqlIdentifier.fromCql("myusertype"));
}
@Test // DATACASS-84
public void shouldDeriveColumnName() {
BasicCassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(PersonTable.class);
assertThat(entity.getRequiredIdProperty().getColumnName()).isEqualTo(CqlIdentifier.fromCql("firstname"));
}
@Test // DATACASS-84
public void shouldDeriveCaseSensitiveTableName() {
BasicCassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(QuotedPersonTable.class);
assertThat(entity.getTableName()).isEqualTo(CqlIdentifier.fromInternal("QuotedPersonTable"));
}
@Test // DATACASS-84
public void shouldDeriveCaseSensitiveUserDefinedTypeName() {
BasicCassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(QuotedMyUserType.class);
assertThat(entity.getTableName()).isEqualTo(CqlIdentifier.fromInternal("QuotedMyUserType"));
}
@Test // DATACASS-84
public void shouldDeriveCaseSensitiveColumnName() {
BasicCassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(QuotedPersonTable.class);
assertThat(entity.getRequiredIdProperty().getColumnName()).isEqualTo(CqlIdentifier.fromInternal("firstName"));
}
@Test // DATACASS-84
public void shouldApplyTransformedNamingStrategy() {
context.setNamingStrategy(NamingStrategy.INSTANCE.transform(String::toUpperCase));
BasicCassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(QuotedPersonTable.class);
assertThat(entity.getTableName()).isEqualTo(CqlIdentifier.fromInternal("QUOTEDPERSONTABLE"));
assertThat(entity.getRequiredIdProperty().getColumnName()).isEqualTo(CqlIdentifier.fromInternal("FIRSTNAME"));
}
static class PersonTable {
@Id String firstName;
}
@UserDefinedType
static class MyUserType {
String firstName;
}
@Table(forceQuote = true)
static class QuotedPersonTable {
@Id @PrimaryKey(forceQuote = true) String firstName;
}
@UserDefinedType(forceQuote = true)
static class QuotedMyUserType {
String firstName;
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2020 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 static org.assertj.core.api.Assertions.*;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import com.datastax.oss.driver.api.core.CqlIdentifier;
/**
* Unit tests for {@link SnakeCaseNamingStrategy}.
*
* @author Mark Paluch
*/
public class SnakeCaseNamingStrategyUnitTests {
CassandraMappingContext context = new CassandraMappingContext();
@Before
public void before() {
context.setUserTypeResolver(typeName -> {
throw new IllegalStateException("");
});
context.setNamingStrategy(NamingStrategy.SNAKE_CASE);
}
@Test // DATACASS-84
public void shouldDeriveTableName() {
BasicCassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(PersonTable.class);
assertThat(entity.getTableName()).isEqualTo(CqlIdentifier.fromCql("person_table"));
}
@Test // DATACASS-84
public void shouldDeriveUserDefinedTypeName() {
BasicCassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(MyUserType.class);
assertThat(entity.getTableName()).isEqualTo(CqlIdentifier.fromCql("my_user_type"));
}
@Test // DATACASS-84
public void shouldDeriveColumnName() {
BasicCassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(PersonTable.class);
assertThat(entity.getRequiredIdProperty().getColumnName()).isEqualTo(CqlIdentifier.fromCql("first_name"));
}
@Test // DATACASS-84
public void shouldDeriveCaseSensitiveTableName() {
BasicCassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(QuotedPersonTable.class);
assertThat(entity.getTableName()).isEqualTo(CqlIdentifier.fromInternal("quoted_person_table"));
}
@Test // DATACASS-84
public void shouldDeriveCaseSensitiveUserDefinedTypeName() {
BasicCassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(QuotedMyUserType.class);
assertThat(entity.getTableName()).isEqualTo(CqlIdentifier.fromInternal("quoted_my_user_type"));
}
@Test // DATACASS-84
public void shouldDeriveCaseSensitiveColumnName() {
BasicCassandraPersistentEntity<?> entity = context.getRequiredPersistentEntity(QuotedPersonTable.class);
assertThat(entity.getRequiredIdProperty().getColumnName()).isEqualTo(CqlIdentifier.fromInternal("first_name"));
}
static class PersonTable {
@Id String firstName;
}
@UserDefinedType
static class MyUserType {
String firstName;
}
@Table(forceQuote = true)
static class QuotedPersonTable {
@Id @PrimaryKey(forceQuote = true) String firstName;
}
@UserDefinedType(forceQuote = true)
static class QuotedMyUserType {
String firstName;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2020 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.example;
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
import org.springframework.data.cassandra.core.mapping.NamingStrategy;
public class NamingStrategyConfiguration {
public void configurationMethod() {
// tag::method[]
CassandraMappingContext context = new CassandraMappingContext();
// default naming strategy
context.setNamingStrategy(NamingStrategy.INSTANCE);
// snake_case converted to upper case (SNAKE_CASE)
context.setNamingStrategy(NamingStrategy.SNAKE_CASE.transform(String::toUpperCase));
// end::method[]
}
}

View File

@@ -5,11 +5,13 @@ This chapter summarizes changes and new features for each release.
[[new-features.3-0-0]]
== What's new in Spring Data for Apache Cassandra 3.0
* Upgrade to Cassandra Driver version 4. See the <<cassandra.migration.2.x-to-3.x,2.x to 3.x migration guide for details>>.
* Upgrade to Cassandra Driver version 4. See the <<cassandra.migration.2.x-to-3.x,2.x to 3.x migration guide for details>>.
* Support for `NamingStrategy`.
[[new-features.2-2-0]]
== What's new in Spring Data for Apache Cassandra 2.2
* Filter conditions for lightweight transaction update and delete (`UPDATE … IF <condition>`, `DELETE … IF <condition>`).
* Optimistic Locking support.
* Auditing via `@EnableCassandraAuditing`.
@@ -22,6 +24,7 @@ This chapter summarizes changes and new features for each release.
[[new-features.2-1-0]]
== What's new in Spring Data for Apache Cassandra 2.1
* New annotations for `@CountQuery` and `@ExistsQuery`.
* Template API extended with `count(…)` and `exists(…)` methods accepting `Query`.
* <<cassandra.template.query.fluent-template-api,Fluent API>> for CRUD operations.

View File

@@ -141,6 +141,19 @@ 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.
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.
The following example shows how to configure a `NamingStrategy`:
.Configuring `NamingStrategy` on `CassandraMappingContext`
====
[source,java]
----
include::../{example-root}/NamingStrategyConfiguration.java[tags=method]
----
====
[[mapping-configuration]]
=== Mapping Configuration
@@ -165,7 +178,6 @@ You can override it to tell the converter where to scan for classes annotated wi
You can add additional converters to the `MappingCassandraConverter` by overriding the `customConversions` method.
NOTE: `AbstractCassandraConfiguration` creates a `CassandraTemplate` instance and registers it with the container under the name of `cassandraTemplate`.
[[mapping.usage]]
== Metadata-based Mapping
@@ -432,7 +444,6 @@ include::../{example-root}/mapping/PersonWithIndexes.java[tags=class]
CAUTION: Index creation on session initialization may have a severe performance impact on application startup.
include::./converters.adoc[]
[[cassandra.mapping-usage.events]]
== Lifecycle Events