-
Notifications
You must be signed in to change notification settings - Fork 165
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add optional MySQLExpression which makes use of mysql's binary keyword
MySQL, by default, uses case insensitive comparisons. So "foo like 'bar'" is the same as "foo like 'BaR'". WO's case-insensitive like uses the UPPER function which bypasses mysql indexing making it a slow operation. The solution is to use "foo like 'bar'" for case insensitive and "binary foo like 'Bar'" for case-sensitive. Similarly for sort orderings, I believe, the binary keyword can be used for case sensitive sort orderings. Enable with property com.webobjects.jdbcadaptor.MySQLExpression.enable=true
- Loading branch information
Showing
2 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
341 changes: 341 additions & 0 deletions
341
Frameworks/PlugIns/MySQLPlugIn/Sources/com/webobjects/jdbcadaptor/MySQLExpression.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,341 @@ | ||
/** | ||
* | ||
*/ | ||
package com.webobjects.jdbcadaptor; | ||
|
||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
import com.webobjects.eoaccess.EOAttribute; | ||
import com.webobjects.eoaccess.EOEntity; | ||
import com.webobjects.eocontrol.EOKeyComparisonQualifier; | ||
import com.webobjects.eocontrol.EOKeyValueQualifier; | ||
import com.webobjects.eocontrol.EOQualifier; | ||
import com.webobjects.eocontrol.EOQualifierVariable; | ||
import com.webobjects.eocontrol.EOSortOrdering; | ||
import com.webobjects.foundation.NSSelector; | ||
import com.webobjects.foundation._NSStringUtilities; | ||
|
||
/** | ||
* <p> | ||
* Overrides the default EOF MySQLExpression to adjust the sql generation of | ||
* string comparisons. | ||
* </p> | ||
* <p> | ||
* MySQL's default behaviour, as per usual, is to ignore the standards. Thus an | ||
* sql <code>like</code> performs a case insensitive comparison rather than case | ||
* sensitive which is vexing. | ||
* </p> | ||
* <p> | ||
* To enforce standard behaviour you can tell mysql by using | ||
* <code>like binary</code> for a case sensitive comparison. Thus a case | ||
* insensitive comparison simply requires using a <code>like</code> in order to | ||
* obtain the results desired. | ||
* </p> | ||
* <p> | ||
* <b>Note</b>: This assumes that you're not using binary columns. | ||
* </p> | ||
* <p> | ||
* The alternative is to define all of your columns as binary columns and | ||
* specifically specify the collation to use for case insensitive comparisons. | ||
* But that's a more complex approach in the author's view. | ||
* </p> | ||
* <p> | ||
* Another approach is to only every use qualifiers and sort orderings that are | ||
* case sensitive (semantically) and choose in your model what external type to | ||
* map to in order to control the behaviour. In my view this is bad practice | ||
* because you're separating the logic of queries and returning results that are | ||
* not intended according to the code. | ||
* </p> | ||
* <p> | ||
* To have this class enabled as the runtime MySQLExpression define the | ||
* following property. | ||
* </p> | ||
* <p> | ||
* com.webobjects.jdbcadaptor.MySQLExpression.enable=true | ||
* </p> | ||
* <p> | ||
* To summarise: | ||
* </p> | ||
* <ul> | ||
* <li>LIKE UPPER(foo) becomes LIKE foo | ||
* <li>LIKE foo becomes LIKE BINARY foo | ||
* </ul> | ||
* | ||
* @author ldeck | ||
*/ | ||
public class MySQLExpression | ||
extends | ||
com.webobjects.jdbcadaptor.MySQLPlugIn.MySQLExpression | ||
{ | ||
|
||
private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger( MySQLExpression.class ); | ||
private static final List< NSSelector > SORT_ORDERING_ASC_SELECTORS = | ||
Arrays.asList( EOSortOrdering.CompareAscending, EOSortOrdering.CompareCaseInsensitiveAscending ); | ||
private static final List< NSSelector > SORT_ORDERING_BIN_SELECTORS = Arrays.asList( EOSortOrdering.CompareAscending, EOSortOrdering.CompareDescending ); | ||
private static final List< NSSelector > SORT_ORDERING_DESC_SELECTORS = | ||
Arrays.asList( EOSortOrdering.CompareCaseInsensitiveDescending, EOSortOrdering.CompareDescending ); | ||
|
||
private final Pattern likeOperatorRegex; | ||
private final Pattern upperFunctionNameRegex; | ||
|
||
/** | ||
* @param entity | ||
*/ | ||
public MySQLExpression( EOEntity entity ) | ||
{ | ||
super( entity ); | ||
this.upperFunctionNameRegex = Pattern.compile( "\\Q" + this._upperFunctionName + "\\E\\(([^\\)]+)\\)" ); | ||
this.likeOperatorRegex = Pattern.compile( "([Ll][Ii][Kk][Ee])" ); | ||
} | ||
|
||
/** | ||
* @see com.webobjects.eoaccess.EOSQLExpression#addOrderByAttributeOrdering(com.webobjects.eocontrol.EOSortOrdering) | ||
*/ | ||
@Override | ||
public void addOrderByAttributeOrdering( EOSortOrdering sortOrdering ) | ||
{ | ||
NSSelector< ? > selector = sortOrdering.selector(); | ||
String attPath = sortOrdering.key(); | ||
String sqlString1 = sqlStringForAttributeNamed( attPath ); | ||
if ( sqlString1 == null ) | ||
{ | ||
throw new IllegalStateException( new StringBuilder() | ||
.append( "addOrderByAttributeOrdering: attempt to generate SQL for " ) | ||
.append( sortOrdering.getClass().getName() ) | ||
.append( " " ) | ||
.append( sortOrdering ) | ||
.append( " failed because attribute identified by key '" ) | ||
.append( sortOrdering.key() ) | ||
.append( "' was not reachable from from entity '" ) | ||
.append( this._entity.name() ) | ||
.append( "'" ) | ||
.toString() ); | ||
} | ||
String format; | ||
boolean isAscending = false; | ||
if ( ( isAscending = SORT_ORDERING_ASC_SELECTORS.contains( selector ) ) || SORT_ORDERING_DESC_SELECTORS.contains( selector ) ) | ||
{ | ||
String orderType = isAscending ? "ASC" : "DESC"; | ||
String binaryOperator = | ||
SORT_ORDERING_BIN_SELECTORS.contains( selector ) | ||
&& entity()._attributeForPath( attPath ).adaptorValueType() == EOAttribute.AdaptorCharactersType ? "BINARY " : ""; | ||
|
||
format = _NSStringUtilities.concat( binaryOperator, " ", sqlString1, " ", orderType ); | ||
} | ||
else | ||
{ | ||
format = _NSStringUtilities.concat( "(", sqlString1, ")" ); | ||
} | ||
appendItemToListString( format, _orderByString() ); | ||
} | ||
|
||
/** | ||
* @see com.webobjects.jdbcadaptor.JDBCExpression#appendItemToOrderByString(java.lang.String) | ||
*/ | ||
@Override | ||
protected void appendItemToOrderByString( String sqlString ) | ||
{ | ||
super.appendItemToOrderByString( replaceStringForCaseInsensitiveLike( sqlString ) ); | ||
} | ||
|
||
protected Pattern likeOperatorRegex() | ||
{ | ||
return this.likeOperatorRegex; | ||
} | ||
|
||
protected String replaceStringForCaseInsensitiveLike( String string ) | ||
{ | ||
StringBuffer result = new StringBuffer(); | ||
|
||
Matcher matcher = upperFunctionNameRegex().matcher( string ); | ||
while ( matcher.find() ) | ||
{ | ||
matcher.appendReplacement( result, Matcher.quoteReplacement( matcher.group( 1 ) ) ); | ||
} | ||
matcher.appendTail( result ); | ||
|
||
return result.toString(); | ||
} | ||
|
||
protected String replaceStringForCaseSensitiveLike( String string ) | ||
{ | ||
StringBuffer result = new StringBuffer(); | ||
|
||
Matcher matcher = likeOperatorRegex().matcher( string ); | ||
while ( matcher.find() ) | ||
{ | ||
matcher.appendReplacement( result, Matcher.quoteReplacement( matcher.group( 1 ) + " BINARY" ) ); | ||
} | ||
matcher.appendTail( result ); | ||
|
||
return result.toString(); | ||
} | ||
|
||
/** | ||
* @see com.webobjects.eoaccess.EOSQLExpression#sqlStringForCaseInsensitiveLike(java.lang.String, | ||
* java.lang.String) | ||
*/ | ||
@Override | ||
public String sqlStringForCaseInsensitiveLike( String valueString, String keyString ) | ||
{ | ||
return replaceStringForCaseInsensitiveLike( super.sqlStringForCaseInsensitiveLike( valueString, keyString ) ); | ||
} | ||
|
||
/** | ||
* @see com.webobjects.eoaccess.EOSQLExpression#sqlStringForKeyComparisonQualifier(com.webobjects.eocontrol.EOKeyComparisonQualifier) | ||
*/ | ||
@Override | ||
public String sqlStringForKeyComparisonQualifier( EOKeyComparisonQualifier qualifier ) | ||
{ | ||
String leftKey = qualifier.leftKey(); | ||
String rightKey = qualifier.rightKey(); | ||
if ( leftKey != null && leftKey.equals( rightKey ) ) | ||
{ | ||
return "(1=1)"; | ||
} | ||
EOAttribute att = this._entity._attributeForPath( leftKey ); | ||
String leftKeyString = sqlStringForAttributeNamed( leftKey ); | ||
if ( leftKeyString == null ) | ||
{ | ||
throw new IllegalStateException( new StringBuilder() | ||
.append( "sqlStringForKeyComparisonQualifier: attempt to generate SQL for " ) | ||
.append( qualifier.getClass().getName() ) | ||
.append( " " ) | ||
.append( qualifier ) | ||
.append( " failed because attribute identified by key '" ) | ||
.append( leftKey ) | ||
.append( "' was not reachable from from entity '" ) | ||
.append( this._entity.name() ) | ||
.append( "'" ) | ||
.toString() ); | ||
} | ||
leftKeyString = formatSQLString( leftKeyString, att.readFormat() ); | ||
att = this._entity._attributeForPath( rightKey ); | ||
String rightKeyString = sqlStringForAttributeNamed( rightKey ); | ||
if ( rightKeyString == null ) | ||
{ | ||
throw new IllegalStateException( new StringBuilder() | ||
.append( "sqlStringForKeyComparisonQualifier: attempt to generate SQL for " ) | ||
.append( qualifier.getClass().getName() ) | ||
.append( " " ) | ||
.append( qualifier ) | ||
.append( " failed because attribute identified by key '" ) | ||
.append( rightKey ) | ||
.append( "' was not reachable from from entity '" ) | ||
.append( this._entity.name() ) | ||
.append( "'" ) | ||
.toString() ); | ||
} | ||
else | ||
{ | ||
rightKeyString = formatSQLString( rightKeyString, att.readFormat() ); | ||
String operatorString = sqlStringForSelector( qualifier.selector(), null ); | ||
|
||
EOAttribute leftAttribute = this._entity._attributeForPath( leftKey ); | ||
EOAttribute rightAttribute = this._entity._attributeForPath( rightKey ); | ||
|
||
NSSelector qualifierSelector = qualifier.selector(); | ||
boolean isLike = | ||
qualifierSelector.equals( EOQualifier.QualifierOperatorLike ) || qualifierSelector.equals( EOQualifier.QualifierOperatorCaseInsensitiveLike ); | ||
boolean isStringComparison = | ||
isLike | ||
|| EOAttribute.AdaptorCharactersType == leftAttribute.adaptorValueType() | ||
|| EOAttribute.AdaptorCharactersType == rightAttribute.adaptorValueType(); | ||
|
||
String binaryOperator = isStringComparison ? "BINARY " : ""; | ||
|
||
return _NSStringUtilities.concat( binaryOperator, leftKeyString, " ", operatorString, " ", rightKeyString ); | ||
} | ||
} | ||
|
||
/** | ||
* @see com.webobjects.eoaccess.EOSQLExpression#sqlStringForKeyValueQualifier(com.webobjects.eocontrol.EOKeyValueQualifier) | ||
*/ | ||
@Override | ||
public String sqlStringForKeyValueQualifier( EOKeyValueQualifier qualifier ) | ||
{ | ||
String key = qualifier.key(); | ||
String keyString = sqlStringForAttributeNamed( key ); | ||
if ( keyString == null ) | ||
{ | ||
throw new IllegalStateException( new StringBuilder() | ||
.append( "sqlStringForKeyValueQualifier: attempt to generate SQL for " ) | ||
.append( qualifier.getClass().getName() ) | ||
.append( " " ) | ||
.append( qualifier ) | ||
.append( " failed because attribute identified by key '" ) | ||
.append( key ) | ||
.append( "' was not reachable from from entity '" ) | ||
.append( this._entity.name() ) | ||
.append( "'" ) | ||
.toString() ); | ||
} | ||
Object qualifierValue = qualifier.value(); | ||
if ( qualifierValue instanceof EOQualifierVariable ) | ||
{ | ||
throw new IllegalStateException( new StringBuilder() | ||
.append( "sqlStringForKeyValueQualifier: attempt to generate SQL for " ) | ||
.append( qualifier.getClass().getName() ) | ||
.append( " " ) | ||
.append( qualifier ) | ||
.append( " failed because the qualifier variable '$" ) | ||
.append( ( ( EOQualifierVariable )qualifierValue ).key() ) | ||
.append( "' is unbound." ) | ||
.toString() ); | ||
} | ||
|
||
EOAttribute attribute = this._entity._attributeForPath( key ); | ||
|
||
keyString = formatSQLString( keyString, attribute.readFormat() ); | ||
NSSelector qualifierSelector = qualifier.selector(); | ||
boolean isLike = | ||
qualifierSelector.equals( EOQualifier.QualifierOperatorLike ) || qualifierSelector.equals( EOQualifier.QualifierOperatorCaseInsensitiveLike ); | ||
boolean isStringComparison = isLike || EOAttribute.AdaptorCharactersType == attribute.adaptorValueType(); | ||
|
||
Object value; | ||
if ( isLike ) | ||
{ | ||
value = sqlPatternFromShellPattern( ( String )qualifierValue ); | ||
} | ||
else | ||
{ | ||
value = qualifierValue; | ||
} | ||
String string; | ||
if ( qualifierSelector.equals( EOQualifier.QualifierOperatorCaseInsensitiveLike ) ) | ||
{ | ||
String valueString = sqlStringForValue( value, key ); | ||
String operatorString = sqlStringForSelector( qualifierSelector, value ); | ||
string = sqlStringForCaseInsensitiveLike( valueString, keyString ); | ||
} | ||
else if ( EOQualifier.QualifierOperatorLike.equals( qualifierSelector ) || isStringComparison ) | ||
{ | ||
String valueString = sqlStringForValue( value, key ); | ||
String operatorString = sqlStringForSelector( qualifierSelector, value ); | ||
string = _NSStringUtilities.concat( "BINARY ", keyString, " ", operatorString, " ", valueString ); | ||
} | ||
else | ||
{ | ||
String valueString = sqlStringForValue( value, key ); | ||
String operatorString = sqlStringForSelector( qualifierSelector, value ); | ||
string = _NSStringUtilities.concat( keyString, " ", operatorString, " ", valueString ); | ||
} | ||
if ( isLike ) | ||
{ | ||
char escapeChar = sqlEscapeChar(); | ||
if ( escapeChar != 0 ) | ||
{ | ||
string = _NSStringUtilities.concat( string, new StringBuilder().append( " ESCAPE '" ).append( escapeChar ).append( "'" ).toString() ); | ||
} | ||
} | ||
return string; | ||
} | ||
|
||
protected Pattern upperFunctionNameRegex() | ||
{ | ||
return this.upperFunctionNameRegex; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters