Skip to content

Prevent Non-Rendering Where Clauses by Default #515

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
226ac5e
Update license header file for new parent
jeffgbutler Aug 14, 2022
d3427d1
Initial Work on Statement Configuration
jeffgbutler Aug 15, 2022
11aa4c8
Move configuration classes into a new package
jeffgbutler Aug 15, 2022
1d88f38
Merge branch 'master' into statement-configuration
jeffgbutler Aug 15, 2022
6e537cf
Better docs
jeffgbutler Aug 16, 2022
d656b15
Checkstyle
jeffgbutler Aug 16, 2022
ebfa8b2
Merge branch 'master' into statement-configuration
jeffgbutler Aug 16, 2022
90cd2cc
Use late initialization for where builders
jeffgbutler Aug 16, 2022
03e11b8
Polishing
jeffgbutler Aug 16, 2022
a03ab7f
Rewrite for code coverage
jeffgbutler Aug 24, 2022
8e3e194
Improve tests per Sonar recs
jeffgbutler Aug 24, 2022
bc814e7
Improve code per Sonar recs
jeffgbutler Aug 24, 2022
2b68c93
Better Property Name
jeffgbutler Aug 24, 2022
93c91df
Add helpful message
jeffgbutler Aug 24, 2022
81153f4
IDEA warning removal
jeffgbutler Aug 24, 2022
d2437d4
Make the configuration more testable
jeffgbutler Aug 24, 2022
7c85dda
Make the configuration more testable
jeffgbutler Aug 24, 2022
93af255
Checkstyle
jeffgbutler Aug 24, 2022
cd009d6
Documentation
jeffgbutler Aug 24, 2022
e31f68f
Deprecate the "empty list" methods
jeffgbutler Aug 24, 2022
5a68a75
Kotlin DSL statement configuration should not call where()
jeffgbutler Aug 25, 2022
e8876ad
Update docs
jeffgbutler Aug 25, 2022
fda6dd6
Update docs
jeffgbutler Aug 25, 2022
f67bc5d
Deprecate the callback interface in favor of configuration functions
jeffgbutler Aug 25, 2022
21e45e7
Add PR Number to Changelog
jeffgbutler Aug 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ This log will detail notable changes to MyBatis Dynamic SQL. Full details are av

GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.4.1+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.4.1+)

### Potentially Breaking Change

In this release we have changed the default behavior of the library in one key area. If a where clause is coded,
but fails to render because all the optional conditionals drop out of the where clause, then the library will now
throw a `NonRenderingWhereClauseException`. We have made this change out of an abundance of caution. The prior
behavior would allow generation of statements that inadvertently affected all rows in a table.

We have also deprecated the "empty callback" functions in the "in" conditions in favor of this new configuration
strategy. The "empty callback" methods were effective for "in" conditions that failed to render, but they offered
no help for other conditions that failed to render, or if all conditions fail to render - which is arguably a more
dangerous outcome. If you were using any of these methods, you should remove the calls to those methods and catch the
new `NonRenderingWhereClauseException`.

If you desire the prior behavior where non rendering where clauses are allowed, you can change the global configuration
of the library or - even better - change the configuration of individual statements where this behavior should be allowed.

For examples of global and statement configuration, see the "Configuration of the Library" page.

### Other Changes

1. Added support for criteria groups without an initial criteria. This makes it possible to create an independent list
of pre-created criteria and then add the list to a where clause. See the tests in the related pull request for
usage examples. ([#462](https://github.com/mybatis/mybatis-dynamic-sql/pull/462))
Expand All @@ -15,6 +35,9 @@ GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=miles
3. Updated the Kotlin DSL to use Kotlin 1.7's new "definitely non-null" types where appropriate. This helps us to more
accurately represent the nullable/non-nullable expectations for API method calls.
([#496](https://github.com/mybatis/mybatis-dynamic-sql/pull/496))
4. Added the ability to configure the library and change some default behaviors. Currently, this is limited to changing
the behavior of the library in regard to where clauses that will not render. See the "Configuration of the Library"
page for details. ([#515](https://github.com/mybatis/mybatis-dynamic-sql/pull/515))

## Release 1.4.0 - March 3, 2022

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2021 the original author or authors.
* Copyright 2016-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.
Expand All @@ -26,12 +26,23 @@

public abstract class AbstractListValueCondition<T> implements VisitableCondition<T> {
protected final Collection<T> values;

/**
* @deprecated in favor of the statement configuration functions
*/
@Deprecated
protected final Callback emptyCallback;

protected AbstractListValueCondition(Collection<T> values) {
this(values, () -> { });
}

/**
* @deprecated in favor of the statement configuration functions
* @param values values
* @param emptyCallback empty callback
*/
@Deprecated
protected AbstractListValueCondition(Collection<T> values, Callback emptyCallback) {
this.values = Objects.requireNonNull(values);
this.emptyCallback = Objects.requireNonNull(emptyCallback);
Expand Down Expand Up @@ -96,6 +107,14 @@ protected <R, S extends AbstractListValueCondition<R>> S mapSupport(Function<? s
*/
public abstract AbstractListValueCondition<T> filter(Predicate<? super T> predicate);

/**
* Specifies a callback function to be called if the value list is empty when rendered.
*
* @deprecated in favor of the statement configuration functions
* @param callback a callback function - typically throws an exception to block the statement from executing
* @return this condition
*/
@Deprecated
public abstract AbstractListValueCondition<T> withListEmptyCallback(Callback callback);

public abstract String renderCondition(String columnName, Stream<String> placeholders);
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/org/mybatis/dynamic/sql/Callback.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2020 the original author or authors.
* Copyright 2016-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.
Expand All @@ -17,6 +17,10 @@

import java.util.function.Function;

/**
* @deprecated in favor of the new statement configuration functionality
*/
@Deprecated
@FunctionalInterface
public interface Callback {
void call();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2016-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
*
* 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.mybatis.dynamic.sql.configuration;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

public class GlobalConfiguration {
public static final String CONFIGURATION_FILE_PROPERTY = "mybatis-dynamic-sql.configurationFile"; //$NON-NLS-1$
private static final String DEFAULT_PROPERTY_FILE = "mybatis-dynamic-sql.properties"; //$NON-NLS-1$
private boolean isNonRenderingWhereClauseAllowed = false;
private final Properties properties = new Properties();

public GlobalConfiguration() {
initialize();
}

private void initialize() {
initializeProperties();
initializeNonRenderingWhereClauseAllowed();
}

private void initializeProperties() {
String configFileName = getConfigurationFileName();
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(configFileName);
if (inputStream != null) {
loadProperties(inputStream, configFileName);
}
}

private String getConfigurationFileName() {
String property = System.getProperty(CONFIGURATION_FILE_PROPERTY);
if (property == null) {
return DEFAULT_PROPERTY_FILE;
} else {
return property;
}
}

void loadProperties(InputStream inputStream, String propertyFile) {
try {
properties.load(inputStream);
} catch (IOException e) {
Logger logger = Logger.getLogger(GlobalConfiguration.class.getName());
logger.log(Level.SEVERE, e, () -> "IOException reading property file \"" + propertyFile + "\"");
}
}

private void initializeNonRenderingWhereClauseAllowed() {
String value = properties.getProperty("nonRenderingWhereClauseAllowed", "false"); //$NON-NLS-1$ //$NON-NLS-2$
isNonRenderingWhereClauseAllowed = Boolean.parseBoolean(value);
}

public boolean isIsNonRenderingWhereClauseAllowed() {
return isNonRenderingWhereClauseAllowed;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2016-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
*
* 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.mybatis.dynamic.sql.configuration;

public class GlobalContext {

private static final GlobalContext instance = new GlobalContext();

private final GlobalConfiguration globalConfiguration = new GlobalConfiguration();

private GlobalContext() {}

public static GlobalConfiguration getConfiguration() {
return instance.globalConfiguration;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2016-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
*
* 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.mybatis.dynamic.sql.configuration;

import org.mybatis.dynamic.sql.exception.NonRenderingWhereClauseException;

/**
* This class can be used to change some behaviors of the framework. Every configurable statement
* contains a unique instance of this class, so changes here will only impact a single statement.
* If you intend to change the behavior for all statements, use the {@link GlobalConfiguration}.
* Initial values for this class in each statement are set from the {@link GlobalConfiguration}.
* Configurable behaviors are detailed below:
*
* <dl>
* <dt>nonRenderingWhereClauseAllowed</dt>
* <dd>If false (default), the framework will throw a {@link NonRenderingWhereClauseException}
* if a where clause is specified in the statement, but it fails to render because all
* optional conditions do not render. For example, if an "in" condition specifies an
* empty list of values. If no criteria are specified in a where clause, the framework
* assumes that no where clause was intended and will not throw an exception.
* </dd>
* </dl>
*
* @see GlobalConfiguration
* @since 1.4.1
* @author Jeff Butler
*/
public class StatementConfiguration {
private boolean isNonRenderingWhereClauseAllowed =
GlobalContext.getConfiguration().isIsNonRenderingWhereClauseAllowed();

public boolean isNonRenderingWhereClauseAllowed() {
return isNonRenderingWhereClauseAllowed;
}

public void setNonRenderingWhereClauseAllowed(boolean nonRenderingWhereClauseAllowed) {
this.isNonRenderingWhereClauseAllowed = nonRenderingWhereClauseAllowed;
}
}
33 changes: 25 additions & 8 deletions src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,25 @@
package org.mybatis.dynamic.sql.delete;

import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;

import org.jetbrains.annotations.NotNull;
import org.mybatis.dynamic.sql.SqlTable;
import org.mybatis.dynamic.sql.configuration.StatementConfiguration;
import org.mybatis.dynamic.sql.util.Buildable;
import org.mybatis.dynamic.sql.where.AbstractWhereDSL;
import org.mybatis.dynamic.sql.where.AbstractWhereSupport;
import org.mybatis.dynamic.sql.where.WhereModel;

public class DeleteDSL<R> extends AbstractWhereSupport<DeleteDSL<R>.DeleteWhereBuilder> implements Buildable<R> {
public class DeleteDSL<R> extends AbstractWhereSupport<DeleteDSL<R>.DeleteWhereBuilder, DeleteDSL<R>>
implements Buildable<R> {

private final Function<DeleteModel, R> adapterFunction;
private final SqlTable table;
private final String tableAlias;
private final DeleteWhereBuilder whereBuilder = new DeleteWhereBuilder();
private DeleteWhereBuilder whereBuilder;
private final StatementConfiguration statementConfiguration = new StatementConfiguration();

private DeleteDSL(SqlTable table, String tableAlias, Function<DeleteModel, R> adapterFunction) {
this.table = Objects.requireNonNull(table);
Expand All @@ -40,6 +44,9 @@ private DeleteDSL(SqlTable table, String tableAlias, Function<DeleteModel, R> ad

@Override
public DeleteWhereBuilder where() {
if (whereBuilder == null) {
whereBuilder = new DeleteWhereBuilder();
}
return whereBuilder;
}

Expand All @@ -52,11 +59,19 @@ public DeleteWhereBuilder where() {
@NotNull
@Override
public R build() {
DeleteModel deleteModel = DeleteModel.withTable(table)
.withTableAlias(tableAlias)
.withWhereModel(whereBuilder.buildWhereModel())
.build();
return adapterFunction.apply(deleteModel);
DeleteModel.Builder deleteModelBuilder = DeleteModel.withTable(table)
.withTableAlias(tableAlias);
if (whereBuilder != null) {
deleteModelBuilder.withWhereModel(whereBuilder.buildWhereModel());
}

return adapterFunction.apply(deleteModelBuilder.build());
}

@Override
public DeleteDSL<R> configureStatement(Consumer<StatementConfiguration> consumer) {
consumer.accept(statementConfiguration);
return this;
}

public static <R> DeleteDSL<R> deleteFrom(Function<DeleteModel, R> adapterFunction, SqlTable table,
Expand All @@ -74,7 +89,9 @@ public static DeleteDSL<DeleteModel> deleteFrom(SqlTable table, String tableAlia

public class DeleteWhereBuilder extends AbstractWhereDSL<DeleteWhereBuilder> implements Buildable<R> {

private DeleteWhereBuilder() {}
private DeleteWhereBuilder() {
super(statementConfiguration);
}

@NotNull
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2016-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
*
* 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.mybatis.dynamic.sql.exception;

import org.mybatis.dynamic.sql.configuration.GlobalConfiguration;
import org.mybatis.dynamic.sql.configuration.StatementConfiguration;

/**
* This exception is thrown when the where clause in a statement will not render.
* This can happen if all the optional conditions in a where clause fail to
* render - for example, if an "in" condition specifies an empty list.
*
* <p>By default, the framework will throw this exception if a where clause
* fails to render. A where clause that fails to render can be very dangerous in that
* it could cause all rows in a table to be affected by a statement - for example,
* all rows could be deleted.
*
* <p>If you intend to allow a where clause to not render, then configure the
* statement to allow it, or change the global configuration.
*
* @see GlobalConfiguration
* @see StatementConfiguration
* @since 1.4.1
* @author Jeff Butler
*/
public class NonRenderingWhereClauseException extends RuntimeException {
public NonRenderingWhereClauseException() {
super("A where clause was specified, but failed to render."); //$NON-NLS-1$
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

public abstract class AbstractQueryExpressionDSL<W extends AbstractWhereDSL<?>,
T extends AbstractQueryExpressionDSL<W, T>>
extends AbstractWhereSupport<W> {
extends AbstractWhereSupport<W, T> {

private final List<JoinSpecification.Builder> joinSpecificationBuilders = new ArrayList<>();
private final Map<SqlTable, String> tableAliases = new HashMap<>();
Expand Down
Loading