Browse Source
Created NamingStrategy and a default implementation that replicates the current solution. Several unit tests illustrates how to override the default and plugin a custom solution including a ThreadLocal, contextual one that could be user-based if, for example, Spring Security's SecurityContextHolder was used.pull/13/head
17 changed files with 595 additions and 29 deletions
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
/* |
||||
* Copyright 2017 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 |
||||
* |
||||
* http://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.jdbc.mapping.model; |
||||
|
||||
/** |
||||
* Basic implementation of {@link NamingStrategy} with no schema, table based on {@link Class} and |
||||
* column name based on {@link JdbcPersistentProperty}. |
||||
* |
||||
* NOTE: Can also be used as an adapter. Create an anonymous subclass and override any settings to implement |
||||
* a different strategy on the fly. |
||||
* |
||||
* @author Greg Turnquist |
||||
*/ |
||||
public class DefaultNamingStrategy implements NamingStrategy { |
||||
|
||||
/** |
||||
* No schema at all! |
||||
*/ |
||||
@Override |
||||
public String getSchema() { |
||||
return ""; |
||||
} |
||||
|
||||
/** |
||||
* Look up the {@link Class}'s simple name. |
||||
*/ |
||||
@Override |
||||
public String getTableName(Class<?> type) { |
||||
return type.getSimpleName(); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Look up the {@link JdbcPersistentProperty}'s name. |
||||
*/ |
||||
@Override |
||||
public String getColumnName(JdbcPersistentProperty property) { |
||||
return property.getName(); |
||||
} |
||||
} |
||||
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
/* |
||||
* Copyright 2017 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 |
||||
* |
||||
* http://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.jdbc.mapping.model; |
||||
|
||||
/** |
||||
* @author Greg Turnquist |
||||
*/ |
||||
public interface NamingStrategy { |
||||
|
||||
String getSchema(); |
||||
|
||||
String getTableName(Class<?> type); |
||||
|
||||
String getColumnName(JdbcPersistentProperty property); |
||||
|
||||
default String getQualifiedTableName(Class<?> type) { |
||||
return this.getSchema() + (this.getSchema().equals("") ? "" : ".") + this.getTableName(type); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,214 @@
@@ -0,0 +1,214 @@
|
||||
/* |
||||
* Copyright 2017 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 |
||||
* |
||||
* http://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.jdbc.core; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import java.util.concurrent.CountDownLatch; |
||||
import java.util.concurrent.TimeUnit; |
||||
import java.util.function.Consumer; |
||||
|
||||
import org.assertj.core.api.SoftAssertions; |
||||
import org.junit.Test; |
||||
import org.springframework.data.annotation.Id; |
||||
import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy; |
||||
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; |
||||
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; |
||||
import org.springframework.data.jdbc.mapping.model.NamingStrategy; |
||||
import org.springframework.data.mapping.PropertyPath; |
||||
|
||||
/** |
||||
* Unit tests to verify a contextual {@link NamingStrategy} implementation that customizes using a user-centric {@link ThreadLocal}. |
||||
* |
||||
* NOTE: Due to the need to verify SQL generation and {@link SqlGenerator}'s package-private status suggests |
||||
* this unit test exist in this package, not {@literal org.springframework.data.jdbc.mappings.model}. |
||||
* |
||||
* @author Greg Turnquist |
||||
*/ |
||||
public class SqlGeneratorContextBasedNamingStrategyUnitTests { |
||||
|
||||
private final ThreadLocal<String> userHandler = new ThreadLocal<>(); |
||||
|
||||
/** |
||||
* Use a {@link DefaultNamingStrategy}, but override the schema with a {@link ThreadLocal}-based setting. |
||||
*/ |
||||
private final NamingStrategy contextualNamingStrategy = new DefaultNamingStrategy() { |
||||
@Override |
||||
public String getSchema() { |
||||
return userHandler.get(); |
||||
} |
||||
}; |
||||
|
||||
@Test // DATAJDBC-107
|
||||
public void findOne() { |
||||
|
||||
testAgainstMultipleUsers(user -> { |
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); |
||||
|
||||
String sql = sqlGenerator.getFindOne(); |
||||
|
||||
SoftAssertions softAssertions = new SoftAssertions(); |
||||
softAssertions.assertThat(sql) //
|
||||
.startsWith("SELECT") //
|
||||
.contains(user + ".DummyEntity.id AS id,") //
|
||||
.contains(user + ".DummyEntity.name AS name,") //
|
||||
.contains("ref.l1id AS ref_l1id") //
|
||||
.contains("ref.content AS ref_content") //
|
||||
.contains("FROM " + user + ".DummyEntity"); |
||||
softAssertions.assertAll(); |
||||
}); |
||||
} |
||||
|
||||
@Test // DATAJDBC-107
|
||||
public void cascadingDeleteFirstLevel() { |
||||
|
||||
testAgainstMultipleUsers(user -> { |
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); |
||||
|
||||
String sql = sqlGenerator.createDeleteByPath(PropertyPath.from("ref", DummyEntity.class)); |
||||
|
||||
assertThat(sql).isEqualTo( |
||||
"DELETE FROM " + user + ".ReferencedEntity WHERE " + user + ".DummyEntity = :rootId"); |
||||
}); |
||||
} |
||||
|
||||
@Test // DATAJDBC-107
|
||||
public void cascadingDeleteAllSecondLevel() { |
||||
|
||||
testAgainstMultipleUsers(user -> { |
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); |
||||
|
||||
String sql = sqlGenerator.createDeleteByPath(PropertyPath.from("ref.further", DummyEntity.class)); |
||||
|
||||
assertThat(sql).isEqualTo( |
||||
"DELETE FROM " + user + ".SecondLevelReferencedEntity " + |
||||
"WHERE " + user + ".ReferencedEntity IN " + |
||||
"(SELECT l1id FROM " + user + ".ReferencedEntity " + |
||||
"WHERE " + user + ".DummyEntity = :rootId)"); |
||||
}); |
||||
} |
||||
|
||||
@Test // DATAJDBC-107
|
||||
public void deleteAll() { |
||||
|
||||
testAgainstMultipleUsers(user -> { |
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); |
||||
|
||||
String sql = sqlGenerator.createDeleteAllSql(null); |
||||
|
||||
assertThat(sql).isEqualTo("DELETE FROM " + user + ".DummyEntity"); |
||||
}); |
||||
} |
||||
|
||||
@Test // DATAJDBC-107
|
||||
public void cascadingDeleteAllFirstLevel() { |
||||
|
||||
testAgainstMultipleUsers(user -> { |
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); |
||||
|
||||
String sql = sqlGenerator.createDeleteAllSql(PropertyPath.from("ref", DummyEntity.class)); |
||||
|
||||
assertThat(sql).isEqualTo( |
||||
"DELETE FROM " + user + ".ReferencedEntity WHERE " + user + ".DummyEntity IS NOT NULL"); |
||||
}); |
||||
} |
||||
|
||||
@Test // DATAJDBC-107
|
||||
public void cascadingDeleteSecondLevel() { |
||||
|
||||
testAgainstMultipleUsers(user -> { |
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(contextualNamingStrategy); |
||||
|
||||
String sql = sqlGenerator.createDeleteAllSql(PropertyPath.from("ref.further", DummyEntity.class)); |
||||
|
||||
assertThat(sql).isEqualTo( |
||||
"DELETE FROM " + user + ".SecondLevelReferencedEntity " + |
||||
"WHERE " + user + ".ReferencedEntity IN " + |
||||
"(SELECT l1id FROM " + user + ".ReferencedEntity " + |
||||
"WHERE " + user + ".DummyEntity IS NOT NULL)"); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Take a set of user-based assertions and run them against multiple users, in different threads. |
||||
*/ |
||||
private void testAgainstMultipleUsers(Consumer<String> testAssertions) { |
||||
|
||||
CountDownLatch latch = new CountDownLatch(2); |
||||
|
||||
threadedTest("User1", latch, testAssertions); |
||||
threadedTest("User2", latch, testAssertions); |
||||
|
||||
try { |
||||
latch.await(10L, TimeUnit.SECONDS); |
||||
} catch (InterruptedException e) { |
||||
throw new RuntimeException(e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Inside a {@link Runnable}, fetch the {@link ThreadLocal}-based username and execute the provided |
||||
* set of assertions. Then signal through the provided {@link CountDownLatch}. |
||||
*/ |
||||
private void threadedTest(String user, CountDownLatch latch, Consumer<String> testAssertions) { |
||||
|
||||
new Thread(() -> { |
||||
userHandler.set(user); |
||||
|
||||
testAssertions.accept(user); |
||||
|
||||
latch.countDown(); |
||||
}).start(); |
||||
} |
||||
|
||||
/** |
||||
* Plug in a custom {@link NamingStrategy} for this test case. |
||||
*/ |
||||
private SqlGenerator configureSqlGenerator(NamingStrategy namingStrategy) { |
||||
|
||||
JdbcMappingContext context = new JdbcMappingContext(namingStrategy); |
||||
JdbcPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(DummyEntity.class); |
||||
|
||||
return new SqlGenerator(context, persistentEntity, new SqlGeneratorSource(context)); |
||||
} |
||||
|
||||
static class DummyEntity { |
||||
|
||||
@Id Long id; |
||||
String name; |
||||
ReferencedEntity ref; |
||||
} |
||||
|
||||
static class ReferencedEntity { |
||||
|
||||
@Id Long l1id; |
||||
String content; |
||||
SecondLevelReferencedEntity further; |
||||
} |
||||
|
||||
static class SecondLevelReferencedEntity { |
||||
|
||||
@Id Long l2id; |
||||
String something; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,201 @@
@@ -0,0 +1,201 @@
|
||||
/* |
||||
* Copyright 2017 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 |
||||
* |
||||
* http://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.jdbc.core; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import org.assertj.core.api.SoftAssertions; |
||||
import org.junit.Test; |
||||
import org.springframework.data.annotation.Id; |
||||
import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy; |
||||
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; |
||||
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; |
||||
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; |
||||
import org.springframework.data.jdbc.mapping.model.NamingStrategy; |
||||
import org.springframework.data.mapping.PropertyPath; |
||||
|
||||
/** |
||||
* Unit tests to a fixed {@link NamingStrategy} implementation containing a hard wired schema, table, and property prefix. |
||||
* |
||||
* NOTE: Due to the need to verify SQL generation and {@link SqlGenerator}'s package-private status suggests |
||||
* this unit test exist in this package, not {@literal org.springframework.data.jdbc.mappings.model}. |
||||
* |
||||
* @author Greg Turnquist |
||||
*/ |
||||
public class SqlGeneratorFixedNamingStrategyUnitTests { |
||||
|
||||
final NamingStrategy fixedCustomTablePrefixStrategy = new DefaultNamingStrategy() { |
||||
|
||||
@Override |
||||
public String getSchema() { |
||||
return "FixedCustomSchema"; |
||||
} |
||||
|
||||
@Override |
||||
public String getTableName(Class<?> type) { |
||||
return "FixedCustomTablePrefix_" + type.getSimpleName(); |
||||
} |
||||
|
||||
@Override |
||||
public String getColumnName(JdbcPersistentProperty property) { |
||||
return "FixedCustomPropertyPrefix_" + property.getName(); |
||||
} |
||||
}; |
||||
|
||||
final NamingStrategy upperCaseLowerCaseStrategy = new DefaultNamingStrategy() { |
||||
|
||||
@Override |
||||
public String getTableName(Class<?> type) { |
||||
return type.getSimpleName().toUpperCase(); |
||||
} |
||||
|
||||
@Override |
||||
public String getColumnName(JdbcPersistentProperty property) { |
||||
return property.getName().toLowerCase(); |
||||
} |
||||
}; |
||||
|
||||
@Test // DATAJDBC-107
|
||||
public void findOneWithOverriddenFixedTableName() { |
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); |
||||
|
||||
String sql = sqlGenerator.getFindOne(); |
||||
|
||||
SoftAssertions softAssertions = new SoftAssertions(); |
||||
softAssertions.assertThat(sql) //
|
||||
.startsWith("SELECT") //
|
||||
.contains("FixedCustomSchema.FixedCustomTablePrefix_DummyEntity.FixedCustomPropertyPrefix_id AS FixedCustomPropertyPrefix_id,") //
|
||||
.contains("FixedCustomSchema.FixedCustomTablePrefix_DummyEntity.FixedCustomPropertyPrefix_name AS FixedCustomPropertyPrefix_name,") //
|
||||
.contains("ref.FixedCustomPropertyPrefix_l1id AS ref_FixedCustomPropertyPrefix_l1id") //
|
||||
.contains("ref.FixedCustomPropertyPrefix_content AS ref_FixedCustomPropertyPrefix_content") //
|
||||
.contains("FROM FixedCustomSchema.FixedCustomTablePrefix_DummyEntity"); |
||||
softAssertions.assertAll(); |
||||
} |
||||
|
||||
@Test // DATAJDBC-107
|
||||
public void findOneWithUppercasedTablesAndLowercasedColumns() { |
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(upperCaseLowerCaseStrategy); |
||||
|
||||
String sql = sqlGenerator.getFindOne(); |
||||
|
||||
SoftAssertions softAssertions = new SoftAssertions(); |
||||
softAssertions.assertThat(sql) //
|
||||
.startsWith("SELECT") //
|
||||
.contains("DUMMYENTITY.id AS id,") //
|
||||
.contains("DUMMYENTITY.name AS name,") //
|
||||
.contains("ref.l1id AS ref_l1id") //
|
||||
.contains("ref.content AS ref_content") //
|
||||
.contains("FROM DUMMYENTITY"); |
||||
softAssertions.assertAll(); |
||||
} |
||||
|
||||
@Test // DATAJDBC-107
|
||||
public void cascadingDeleteFirstLevel() { |
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); |
||||
|
||||
String sql = sqlGenerator.createDeleteByPath(PropertyPath.from("ref", DummyEntity.class)); |
||||
|
||||
assertThat(sql).isEqualTo("DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " + |
||||
"WHERE FixedCustomSchema.FixedCustomTablePrefix_DummyEntity = :rootId"); |
||||
} |
||||
|
||||
@Test // DATAJDBC-107
|
||||
public void cascadingDeleteAllSecondLevel() { |
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); |
||||
|
||||
String sql = sqlGenerator.createDeleteByPath(PropertyPath.from("ref.further", DummyEntity.class)); |
||||
|
||||
assertThat(sql).isEqualTo( |
||||
"DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_SecondLevelReferencedEntity " + |
||||
"WHERE FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity IN " + |
||||
"(SELECT FixedCustomPropertyPrefix_l1id " + |
||||
"FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " + |
||||
"WHERE FixedCustomSchema.FixedCustomTablePrefix_DummyEntity = :rootId)"); |
||||
} |
||||
|
||||
@Test // DATAJDBC-107
|
||||
public void deleteAll() { |
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); |
||||
|
||||
String sql = sqlGenerator.createDeleteAllSql(null); |
||||
|
||||
assertThat(sql).isEqualTo("DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_DummyEntity"); |
||||
} |
||||
|
||||
@Test // DATAJDBC-107
|
||||
public void cascadingDeleteAllFirstLevel() { |
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); |
||||
|
||||
String sql = sqlGenerator.createDeleteAllSql(PropertyPath.from("ref", DummyEntity.class)); |
||||
|
||||
assertThat(sql).isEqualTo("DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " + |
||||
"WHERE FixedCustomSchema.FixedCustomTablePrefix_DummyEntity IS NOT NULL"); |
||||
} |
||||
|
||||
@Test // DATAJDBC-107
|
||||
public void cascadingDeleteSecondLevel() { |
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy); |
||||
|
||||
String sql = sqlGenerator.createDeleteAllSql(PropertyPath.from("ref.further", DummyEntity.class)); |
||||
|
||||
assertThat(sql).isEqualTo( |
||||
"DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_SecondLevelReferencedEntity " + |
||||
"WHERE FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity IN " + |
||||
"(SELECT FixedCustomPropertyPrefix_l1id " + |
||||
"FROM FixedCustomSchema.FixedCustomTablePrefix_ReferencedEntity " + |
||||
"WHERE FixedCustomSchema.FixedCustomTablePrefix_DummyEntity IS NOT NULL)"); |
||||
} |
||||
|
||||
/** |
||||
* Plug in a custom {@link NamingStrategy} for this test case. |
||||
* |
||||
* @param namingStrategy |
||||
*/ |
||||
private SqlGenerator configureSqlGenerator(NamingStrategy namingStrategy) { |
||||
|
||||
JdbcMappingContext context = new JdbcMappingContext(namingStrategy); |
||||
JdbcPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(DummyEntity.class); |
||||
return new SqlGenerator(context, persistentEntity, new SqlGeneratorSource(context)); |
||||
} |
||||
|
||||
static class DummyEntity { |
||||
|
||||
@Id Long id; |
||||
String name; |
||||
ReferencedEntity ref; |
||||
} |
||||
|
||||
static class ReferencedEntity { |
||||
|
||||
@Id Long l1id; |
||||
String content; |
||||
SecondLevelReferencedEntity further; |
||||
} |
||||
|
||||
static class SecondLevelReferencedEntity { |
||||
|
||||
@Id Long l2id; |
||||
String something; |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue