309 lines
12 KiB
Java
309 lines
12 KiB
Java
/*
|
|
Copyright (c) 2011-2022 Reliancy LLC
|
|
|
|
Licensed under the GNU LESSER GENERAL PUBLIC LICENSE Version 3.
|
|
You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html.
|
|
You may not use this file except in compliance with the License.
|
|
*/
|
|
package com.reliancy.dbo.sql;
|
|
|
|
import java.io.IOException;
|
|
import java.lang.reflect.Array;
|
|
import java.sql.Blob;
|
|
import java.sql.Clob;
|
|
import java.sql.Connection;
|
|
import java.sql.JDBCType;
|
|
import java.sql.SQLException;
|
|
import java.sql.Types;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
|
|
import com.reliancy.dbo.Action;
|
|
import com.reliancy.dbo.ActionHero;
|
|
import com.reliancy.dbo.Entity;
|
|
import com.reliancy.dbo.Terminal;
|
|
import com.reliancy.dbo.meta.MetaTerminal;
|
|
import com.reliancy.util.Path;
|
|
import com.zaxxer.hikari.HikariConfig;
|
|
import com.zaxxer.hikari.HikariDataSource;
|
|
|
|
/**
|
|
* JDBC/SQL implementation of {@link Terminal} with connection pooling.
|
|
*
|
|
* <p>This class provides database connectivity and CRUD operations for SQL databases
|
|
* using JDBC. It leverages HikariCP for high-performance connection pooling and
|
|
* supports multiple database vendors (PostgreSQL, MySQL, SQL Server, Oracle, H2).
|
|
*
|
|
* <h2>Connection URL Format:</h2>
|
|
* <pre>
|
|
* protocol://username:password@host:port/database
|
|
*
|
|
* Examples:
|
|
* postgres://user:pass@localhost:5432/mydb
|
|
* mysql://root:secret@192.168.1.100:3306/app
|
|
* sqlserver://sa:pass@server:1433/testdb
|
|
* h2://sa:@mem:testdb
|
|
* </pre>
|
|
*
|
|
* <h2>Features:</h2>
|
|
* <ul>
|
|
* <li><b>Connection Pooling</b>: HikariCP for efficient connection reuse</li>
|
|
* <li><b>Type Mapping</b>: Automatic Java ↔ SQL type conversion</li>
|
|
* <li><b>Vendor Support</b>: Database-specific SQL dialect handling</li>
|
|
* <li><b>Prepared Statements</b>: Caching enabled for performance (250 statements, 2KB max)</li>
|
|
* </ul>
|
|
*
|
|
* <h2>Usage Example:</h2>
|
|
* <pre>{@code
|
|
* SQLTerminal db = new SQLTerminal("postgres://user:pass@localhost:5432/mydb");
|
|
*
|
|
* // High-level operations
|
|
* PersonDBO person = db.load(PersonDBO.class, 42);
|
|
* person.set(PersonDBO.NAME, "Jane");
|
|
* db.save(person);
|
|
*
|
|
* // Low-level Action API
|
|
* try (Action query = db.begin()
|
|
* .load(PersonDBO.class)
|
|
* .filterBy(PersonDBO.AGE.gte(21))
|
|
* .execute()) {
|
|
* for (DBO record : query) {
|
|
* // Process records
|
|
* }
|
|
* }
|
|
* }</pre>
|
|
*
|
|
* <h2>Type Mapping:</h2>
|
|
* <p>The terminal maintains bidirectional type mappings:
|
|
* <ul>
|
|
* <li>{@link #getJava2SQL()} - Java Class → JDBC Types constant</li>
|
|
* <li>{@link #getSQL2Java()} - JDBC Types constant → Java Class</li>
|
|
* <li>{@link #getTypeName(Class, String)} - Java Class → SQL type name with parameters</li>
|
|
* </ul>
|
|
*
|
|
* <h3>Common Type Mappings:</h3>
|
|
* <table>
|
|
* <caption>Java to SQL type mappings</caption>
|
|
* <tr><th>Java Type</th><th>SQL Type</th></tr>
|
|
* <tr><td>Integer</td><td>INTEGER</td></tr>
|
|
* <tr><td>Long</td><td>BIGINT</td></tr>
|
|
* <tr><td>String</td><td>VARCHAR(n)</td></tr>
|
|
* <tr><td>BigDecimal</td><td>DECIMAL(p,s)</td></tr>
|
|
* <tr><td>java.sql.Date</td><td>DATE</td></tr>
|
|
* <tr><td>java.sql.Timestamp</td><td>TIMESTAMP/DATETIME</td></tr>
|
|
* <tr><td>Boolean</td><td>BOOLEAN/BIT (vendor-specific)</td></tr>
|
|
* </table>
|
|
*
|
|
* <h2>Database-Specific Handling:</h2>
|
|
* <p>The terminal adapts to different SQL dialects:
|
|
* <ul>
|
|
* <li><b>PostgreSQL</b>: Uses ILIKE for case-insensitive matching, BYTEA for binary, TEXT for large strings</li>
|
|
* <li><b>SQL Server</b>: BIT for boolean, DATETIME for timestamp, VARCHAR(MAX) for large text</li>
|
|
* <li><b>Oracle</b>: INTEGER for boolean, CLOB for large text</li>
|
|
* <li><b>MySQL</b>: TEXT for large strings</li>
|
|
* <li><b>H2</b>: In-memory database support</li>
|
|
* </ul>
|
|
*
|
|
* <h2>Identifier Quoting:</h2>
|
|
* <p>The terminal uses database-specific identifier quotes:
|
|
* <ul>
|
|
* <li>Standard: {@code "tablename"."columnname"}</li>
|
|
* <li>Access via {@link #getQuoteLeft()} and {@link #getQuoteRight()}</li>
|
|
* </ul>
|
|
*
|
|
* @see Terminal
|
|
* @see Action
|
|
* @see SQLReader
|
|
* @see SQLWriter
|
|
* @see SQLCleaner
|
|
*/
|
|
public class SQLTerminal implements Terminal{
|
|
HikariConfig config = new HikariConfig();
|
|
HikariDataSource ds;
|
|
Path url;
|
|
String quoteLeft="\""; // quotes could be subject to sql flavour
|
|
String quoteRight="\"";
|
|
|
|
public SQLTerminal(String url){
|
|
this.url=new Path(url);
|
|
String proto=this.url.getProtocol();
|
|
if(!proto.startsWith("jdbc:")) proto="jdbc:"+proto;
|
|
String u=proto+"://"+this.url.getHost()+":"+this.url.getPort()+"/"+this.url.getDatabase();
|
|
config.setJdbcUrl(u);
|
|
config.setUsername(this.url.getUserid());
|
|
config.setPassword(this.url.getPassword());
|
|
//config.setAutoCommit(false); -- do this in batch cases only
|
|
config.addDataSourceProperty( "cachePrepStmts" , "true" );
|
|
config.addDataSourceProperty( "prepStmtCacheSize" , "250" );
|
|
config.addDataSourceProperty( "prepStmtCacheSqlLimit" , "2048" );
|
|
ds = new HikariDataSource( config );
|
|
}
|
|
public Connection getConnection() throws SQLException{
|
|
return ds.getConnection();
|
|
}
|
|
@Override
|
|
public ActionHero getExecutor(Entity ent, Action.Trait trait) {
|
|
// Default executors for regular entities
|
|
if(trait instanceof Action.Load){
|
|
return new SQLReader(ent, this);
|
|
}else if(trait instanceof Action.Save){
|
|
return new SQLWriter(ent, this);
|
|
}else if(trait instanceof Action.Delete){
|
|
return new SQLCleaner(ent, this);
|
|
}else{
|
|
throw new UnsupportedOperationException("Trait not supported:"+trait);
|
|
}
|
|
}
|
|
|
|
public String getProtocol() {
|
|
return url.getProtocol();
|
|
}
|
|
public String getQuoteLeft(){
|
|
return this.quoteLeft;
|
|
}
|
|
public String getQuoteRight(){
|
|
return this.quoteRight;
|
|
}
|
|
final HashMap<Integer,Class<?>> sql2java=new HashMap<>();
|
|
final HashMap<Class<?>,Integer> java2sql=new HashMap<>();
|
|
public Map<Class<?>,Integer> getJava2SQL(){
|
|
if(!java2sql.isEmpty()) return java2sql;
|
|
String protocol=url.getProtocol();
|
|
java2sql.put(java.math.BigDecimal.class,Types.DECIMAL);
|
|
java2sql.put(java.math.BigInteger.class,Types.DECIMAL);
|
|
java2sql.put(Boolean.class,protocol.contains(":oracle")?Types.INTEGER:Types.BOOLEAN);
|
|
java2sql.put(Byte.class,Types.TINYINT);
|
|
java2sql.put(Short.class,Types.SMALLINT);
|
|
java2sql.put(Integer.class,Types.INTEGER);
|
|
java2sql.put(Long.class,Types.BIGINT);
|
|
java2sql.put(Float.class,Types.FLOAT);
|
|
java2sql.put(Double.class,Types.DOUBLE);
|
|
java2sql.put(byte[].class,Types.VARBINARY);
|
|
java2sql.put(Blob.class,Types.BLOB);
|
|
java2sql.put(char[].class,Types.VARCHAR);
|
|
java2sql.put(String.class,Types.VARCHAR);
|
|
java2sql.put(StringBuffer.class,Types.VARCHAR);
|
|
java2sql.put(Clob.class,Types.CLOB);
|
|
java2sql.put(java.sql.Date.class,Types.DATE);
|
|
java2sql.put(java.sql.Time.class,Types.TIME);
|
|
java2sql.put(java.sql.Timestamp.class,Types.TIMESTAMP);
|
|
java2sql.put(Array.class,Types.ARRAY);
|
|
return java2sql;
|
|
}
|
|
public Map<Integer,Class<?>> getSQL2Java(){
|
|
if(!sql2java.isEmpty()) return sql2java;
|
|
//String protocol=url.getProtocol();
|
|
sql2java.put(Types.NUMERIC,java.math.BigDecimal.class);
|
|
sql2java.put(Types.DECIMAL,java.math.BigDecimal.class);
|
|
sql2java.put(Types.BIT,Boolean.class);
|
|
sql2java.put(Types.BOOLEAN,Boolean.class);
|
|
sql2java.put(Types.TINYINT,Byte.class);
|
|
sql2java.put(Types.SMALLINT,Short.class);
|
|
sql2java.put(Types.INTEGER,Integer.class);
|
|
sql2java.put(Types.BIGINT,Long.class);
|
|
sql2java.put(Types.REAL,Float.class);
|
|
sql2java.put(Types.FLOAT,Float.class);
|
|
sql2java.put(Types.DOUBLE,Double.class);
|
|
sql2java.put(Types.BINARY,byte[].class);
|
|
sql2java.put(Types.VARBINARY,byte[].class);
|
|
sql2java.put(Types.LONGVARBINARY,byte[].class);
|
|
sql2java.put(Types.CHAR,String.class);
|
|
sql2java.put(Types.NCHAR,String.class);
|
|
sql2java.put(Types.VARCHAR,String.class);
|
|
sql2java.put(Types.NVARCHAR,String.class);
|
|
sql2java.put(Types.LONGVARCHAR,String.class);
|
|
sql2java.put(Types.LONGNVARCHAR,String.class);
|
|
sql2java.put(Types.DATE,java.sql.Date.class);
|
|
sql2java.put(Types.TIME,java.sql.Time.class);
|
|
sql2java.put(Types.TIMESTAMP,java.sql.Timestamp.class);
|
|
sql2java.put(Types.BLOB,byte[].class);
|
|
sql2java.put(Types.CLOB,char[].class);
|
|
sql2java.put(Types.ARRAY,java.sql.Array.class);
|
|
sql2java.put(Types.JAVA_OBJECT,Object.class);
|
|
return sql2java;
|
|
}
|
|
/**
|
|
* Returns back java class for given id and or name.
|
|
* The name is not used in default implementation.
|
|
* @param typeid sql type to map
|
|
* @return Class matching sql typeid.
|
|
*/
|
|
public Class<?> getJavaType(int typeid) {
|
|
Class<?> ret=getSQL2Java().get(typeid);
|
|
return ret;
|
|
}
|
|
/**
|
|
* This method will correct cases when sqltype is varchar (12) but type name is date or similar.
|
|
* @param sqltype
|
|
* @param type_name
|
|
* @return tries to promote sqltype given type name to something more specific.
|
|
*/
|
|
public int getTypeId(int sqltype,String type_name){
|
|
if(type_name==null) return sqltype;
|
|
type_name=type_name.toLowerCase();
|
|
if(sqltype==Types.VARCHAR || sqltype==Types.CHAR){
|
|
if(type_name.equals("date")) sqltype=Types.DATE;
|
|
if(type_name.equals("time")) sqltype=Types.TIME;
|
|
if(type_name.equals("datetime")) sqltype=Types.TIMESTAMP;
|
|
}
|
|
return sqltype;
|
|
}
|
|
|
|
/**
|
|
* @param cls
|
|
* @param createParams
|
|
* @return SQL type given java class and create params
|
|
*/
|
|
public int getTypeId(Class<?> cls,String createParams){
|
|
int ret=getJava2SQL().get(cls);
|
|
return ret;
|
|
}
|
|
public String getTypeName(Class<?> cls,String createParams){
|
|
int id=this.getTypeId(cls, createParams);
|
|
String ret = JDBCType.valueOf(id).getName();
|
|
if(ret==null) return null;
|
|
String protocol=url.getProtocol();
|
|
if(protocol.contains(":sqlserver")){
|
|
if("boolean".equalsIgnoreCase(ret)) ret="BIT";
|
|
if("timestamp".equalsIgnoreCase(ret)) ret="DATETIME";
|
|
if("double".equalsIgnoreCase(ret)) ret="float";
|
|
if("float".equalsIgnoreCase(ret)) ret="real";
|
|
}
|
|
if(protocol.contains(":postgre")){
|
|
if("varbinary".equalsIgnoreCase(ret)) ret="bytea";
|
|
if("double".equalsIgnoreCase(ret)) ret="double precision";
|
|
}
|
|
if("varchar".equalsIgnoreCase(ret) && (createParams!=null && !createParams.isEmpty())){
|
|
long size=Long.parseLong(createParams);
|
|
if(protocol.contains(":sqlserver")) ret=size>8000?ret.concat("(").concat("MAX").concat(")"):ret.concat("(").concat(String.valueOf(size)).concat(")");
|
|
else if(protocol.contains(":oracle")) ret=size>2000?"CLOB":ret.concat("(").concat(String.valueOf(size)).concat(")");
|
|
else if(protocol.contains(":mysql")) ret=size>Character.MAX_VALUE?"TEXT":ret.concat("(").concat(String.valueOf(size)).concat(")");
|
|
else if(protocol.contains(":h2")) ret=size>Integer.MAX_VALUE?"CLOB":ret.concat("(").concat(String.valueOf(size)).concat(")");
|
|
else if(protocol.contains(":postgre")) ret=size>Character.MAX_VALUE?"TEXT":ret.concat("(").concat(String.valueOf(size)).concat(")");
|
|
else ret=(size>Character.MAX_VALUE)?"CLOB":ret.concat("(").concat(String.valueOf(size)).concat(")");
|
|
}
|
|
String args=null;
|
|
if(ret.indexOf('(')==-1 && createParams!=null && !createParams.isEmpty()){
|
|
if("decimal".equalsIgnoreCase(ret)) args=createParams;
|
|
if("numeric".equalsIgnoreCase(ret)) args=createParams;
|
|
}
|
|
if(args!=null){
|
|
ret=ret.concat("(").concat(args).concat(")");
|
|
}
|
|
return ret;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns a MetaTerminal instance for metadata operations.
|
|
*
|
|
* @param ent the entity (unused, kept for interface compatibility)
|
|
* @return a new SQLMetaTerminal instance
|
|
*/
|
|
@Override
|
|
public com.reliancy.dbo.meta.MetaTerminal meta(Entity ent) {
|
|
return new SQLMetaTerminal(this);
|
|
}
|
|
}
|
|
|