/***************************************************************** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.cayenne.dba.mysql; import org.apache.cayenne.CayenneRuntimeException; import org.apache.cayenne.access.DataNode; import org.apache.cayenne.access.translator.ParameterBinding; import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory; import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory; import org.apache.cayenne.access.translator.select.QualifierTranslator; import org.apache.cayenne.access.translator.select.QueryAssembler; import org.apache.cayenne.access.translator.select.SelectTranslator; import org.apache.cayenne.access.types.ByteArrayType; import org.apache.cayenne.access.types.CharType; import org.apache.cayenne.access.types.ExtendedType; import org.apache.cayenne.access.types.ExtendedTypeFactory; import org.apache.cayenne.access.types.ExtendedTypeMap; import org.apache.cayenne.access.types.ValueObjectTypeRegistry; import org.apache.cayenne.configuration.Constants; import org.apache.cayenne.configuration.RuntimeProperties; import org.apache.cayenne.dba.DefaultQuotingStrategy; import org.apache.cayenne.dba.JdbcAdapter; import org.apache.cayenne.dba.PkGenerator; import org.apache.cayenne.dba.QuotingStrategy; import org.apache.cayenne.dba.TypesMapping; import org.apache.cayenne.di.Inject; import org.apache.cayenne.map.DbAttribute; import org.apache.cayenne.map.DbEntity; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.query.Query; import org.apache.cayenne.query.SQLAction; import org.apache.cayenne.query.SelectQuery; import org.apache.cayenne.resource.ResourceLocator; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; /** * DbAdapter implementation for <a href="http://www.mysql.com">MySQL RDBMS</a>. * <h3> * Foreign Key Constraint Handling</h3> * <p> * Foreign key constraints are supported by InnoDB engine and NOT supported by * MyISAM engine. This adapter by default assumes MyISAM, so * <code>supportsFkConstraints</code> will be false. Users can manually change * this by calling <em>setSupportsFkConstraints(true)</em> or better by using an * {@link org.apache.cayenne.dba.AutoAdapter}, i.e. not entering the adapter * name at all for the DataNode, letting Cayenne guess it in runtime. In the * later case Cayenne will check the <em>table_type</em> MySQL variable to * detect whether InnoDB is the default, and configure the adapter accordingly. * <h3>Sample Connection Settings</h3> * <ul> * <li>Adapter name: org.apache.cayenne.dba.mysql.MySQLAdapter</li> * <li>DB URL: jdbc:mysql://serverhostname/dbname</li> * <li>Driver Class: com.mysql.jdbc.Driver</li> * </ul> */ public class MySQLAdapter extends JdbcAdapter { static final String DEFAULT_STORAGE_ENGINE = "InnoDB"; static final String MYSQL_QUOTE_SQL_IDENTIFIERS_CHAR_START = "`"; static final String MYSQL_QUOTE_SQL_IDENTIFIERS_CHAR_END = "`"; protected String storageEngine; public MySQLAdapter(@Inject RuntimeProperties runtimeProperties, @Inject(Constants.SERVER_DEFAULT_TYPES_LIST) List<ExtendedType> defaultExtendedTypes, @Inject(Constants.SERVER_USER_TYPES_LIST) List<ExtendedType> userExtendedTypes, @Inject(Constants.SERVER_TYPE_FACTORIES_LIST) List<ExtendedTypeFactory> extendedTypeFactories, @Inject(Constants.SERVER_RESOURCE_LOCATOR) ResourceLocator resourceLocator, @Inject ValueObjectTypeRegistry valueObjectTypeRegistry) { super(runtimeProperties, defaultExtendedTypes, userExtendedTypes, extendedTypeFactories, resourceLocator, valueObjectTypeRegistry); // init defaults this.storageEngine = DEFAULT_STORAGE_ENGINE; setSupportsBatchUpdates(true); setSupportsUniqueConstraints(true); setSupportsGeneratedKeys(true); } @Override protected QuotingStrategy createQuotingStrategy() { return new DefaultQuotingStrategy("`", "`"); } @Override public SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver) { return new MySQLSelectTranslator(query, this, entityResolver); } @Override public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) { QualifierTranslator translator = new MySQLQualifierTranslator(queryAssembler); translator.setCaseInsensitive(caseInsensitiveCollations); return translator; } /** * Uses special action builder to create the right action. * * @since 1.2 */ @Override public SQLAction getAction(Query query, DataNode node) { return query.createSQLAction(new MySQLActionBuilder(node)); } /** * @since 3.0 */ @Override public Collection<String> dropTableStatements(DbEntity table) { // note that CASCADE is a noop as of MySQL 5.0, so we have to use FK // checks // statement StringBuilder buf = new StringBuilder(); QuotingStrategy context = getQuotingStrategy(); buf.append(context.quotedFullyQualifiedName(table)); return Arrays.asList("SET FOREIGN_KEY_CHECKS=0", "DROP TABLE IF EXISTS " + buf.toString() + " CASCADE", "SET FOREIGN_KEY_CHECKS=1"); } /** * Installs appropriate ExtendedTypes used as converters for passing values * between JDBC and Java layers. */ @Override protected void configureExtendedTypes(ExtendedTypeMap map) { super.configureExtendedTypes(map); // must handle CLOBs as strings, otherwise there // are problems with NULL clobs that are treated // as empty strings... somehow this doesn't happen // for BLOBs (ConnectorJ v. 3.0.9) map.registerType(new CharType(false, false)); map.registerType(new ByteArrayType(false, false)); } @Override public DbAttribute buildAttribute(String name, String typeName, int type, int size, int precision, boolean allowNulls) { if (typeName != null) { typeName = typeName.toLowerCase(); } // all LOB types are returned by the driver as OTHER... must remap them // manually // (at least on MySQL 3.23) if (type == Types.OTHER) { if ("longblob".equals(typeName)) { type = Types.BLOB; } else if ("mediumblob".equals(typeName)) { type = Types.BLOB; } else if ("blob".equals(typeName)) { type = Types.BLOB; } else if ("tinyblob".equals(typeName)) { type = Types.VARBINARY; } else if ("longtext".equals(typeName)) { type = Types.CLOB; } else if ("mediumtext".equals(typeName)) { type = Types.CLOB; } else if ("text".equals(typeName)) { type = Types.CLOB; } else if ("tinytext".equals(typeName)) { type = Types.VARCHAR; } } // types like "int unsigned" map to Long else if (typeName != null && typeName.endsWith(" unsigned")) { // per // http://dev.mysql.com/doc/refman/5.0/en/connector-j-reference-type-conversions.html if (typeName.equals("int unsigned") || typeName.equals("integer unsigned") || typeName.equals("mediumint unsigned")) { type = Types.BIGINT; } // BIGINT UNSIGNED maps to BigInteger according to MySQL docs, but // there is no // JDBC mapping for BigInteger } return super.buildAttribute(name, typeName, type, size, precision, allowNulls); } @Override public void bindParameter(PreparedStatement statement, ParameterBinding binding) throws SQLException, Exception { binding.setJdbcType(mapNTypes(binding.getJdbcType())); super.bindParameter(statement, binding); } private int mapNTypes(int sqlType) { switch (sqlType) { case Types.NCHAR: return Types.CHAR; case Types.NCLOB: return Types.CLOB; case Types.NVARCHAR: return Types.VARCHAR; case Types.LONGNVARCHAR: return Types.LONGVARCHAR; default: return sqlType; } } /** * Creates and returns a primary key generator. Overrides superclass * implementation to return an instance of MySQLPkGenerator that does the * correct table locking. */ @Override protected PkGenerator createPkGenerator() { return new MySQLPkGenerator(this); } /** * @since 3.0 */ @Override protected EJBQLTranslatorFactory createEJBQLTranslatorFactory() { JdbcEJBQLTranslatorFactory translatorFactory = new MySQLEJBQLTranslatorFactory(); translatorFactory.setCaseInsensitive(caseInsensitiveCollations); return translatorFactory; } /** * Overrides super implementation to explicitly set table engine to InnoDB * if FK constraints are supported by this adapter. */ @Override public String createTable(DbEntity entity) { String ddlSQL = super.createTable(entity); if (storageEngine != null) { ddlSQL += " ENGINE=" + storageEngine; } return ddlSQL; } /** * Customizes PK clause semantics to ensure that generated columns are in * the beginning of the PK definition, as this seems to be a requirement for * InnoDB tables. * * @since 1.2 */ // See CAY-358 for details of the InnoDB problem @Override protected void createTableAppendPKClause(StringBuffer sqlBuffer, DbEntity entity) { // must move generated to the front... List<DbAttribute> pkList = new ArrayList<>(entity.getPrimaryKeys()); Collections.sort(pkList, new PKComparator()); Iterator<DbAttribute> pkit = pkList.iterator(); if (pkit.hasNext()) { sqlBuffer.append(", PRIMARY KEY ("); boolean firstPk = true; while (pkit.hasNext()) { if (firstPk) firstPk = false; else sqlBuffer.append(", "); DbAttribute at = pkit.next(); sqlBuffer.append(quotingStrategy.quotedName(at)); } sqlBuffer.append(')'); } } /** * Appends AUTO_INCREMENT clause to the column definition for generated * columns. */ @Override public void createTableAppendColumn(StringBuffer sqlBuffer, DbAttribute column) { String[] types = externalTypesForJdbcType(column.getType()); if (types == null || types.length == 0) { String entityName = column.getEntity() != null ? column.getEntity().getFullyQualifiedName() : "<null>"; throw new CayenneRuntimeException("Undefined type for attribute '%s.%s': %s" , entityName, column.getName(), column.getType()); } String type = types[0]; sqlBuffer.append(quotingStrategy.quotedName(column)); sqlBuffer.append(' ').append(type); // append size and precision (if applicable)s if (typeSupportsLength(column.getType())) { int len = column.getMaxLength(); int scale = TypesMapping.isDecimal(column.getType()) ? column.getScale() : -1; // sanity check if (scale > len) { scale = -1; } if (len > 0) { sqlBuffer.append('(').append(len); if (scale >= 0) { sqlBuffer.append(", ").append(scale); } sqlBuffer.append(')'); } } sqlBuffer.append(column.isMandatory() ? " NOT NULL" : " NULL"); if (column.isGenerated()) { sqlBuffer.append(" AUTO_INCREMENT"); } } @Override public boolean typeSupportsLength(int type) { // As of MySQL 5.6.4 the "TIMESTAMP" and "TIME" types support length, // which is the number of decimal places for fractional seconds // http://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html switch (type) { case Types.TIMESTAMP: case Types.TIME: return true; default: return super.typeSupportsLength(type); } } final class PKComparator implements Comparator<DbAttribute> { public int compare(DbAttribute a1, DbAttribute a2) { if (a1.isGenerated() != a2.isGenerated()) { return a1.isGenerated() ? -1 : 1; } else { return a1.getName().compareTo(a2.getName()); } } } /** * @since 3.0 */ public String getStorageEngine() { return storageEngine; } /** * @since 3.0 */ public void setStorageEngine(String engine) { this.storageEngine = engine; } }