Created
May 5, 2021 16:25
-
-
Save aeveltstra/a95472cafcfdd2f9a9e3e1f6512172a2 to your computer and use it in GitHub Desktop.
HalfDao, 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
Requires Java 10 or 11. | |
IDTO: | |
/** | |
* Half-DAO architecture to load read-only data structures from a data store, | |
* and map those to domain objects to be used inside a Java application. | |
* @author A.E.Veltstra | |
* @version 2.18.1130.1540 | |
*/ | |
package omegajunior.halfdao; | |
import java.lang.reflect.InvocationTargetException; | |
import java.sql.Connection; | |
import java.sql.PreparedStatement; | |
import java.sql.ResultSet; | |
import java.sql.ResultSetMetaData; | |
import java.sql.SQLException; | |
import java.util.Objects; | |
import javax.annotation.WillNotClose; | |
import javax.sql.DataSource; | |
import org.eclipse.collections.api.list.ImmutableList; | |
import org.eclipse.collections.api.list.MutableList; | |
import org.eclipse.collections.api.set.ImmutableSet; | |
import org.eclipse.collections.impl.factory.Lists; | |
import org.eclipse.collections.impl.factory.Sets; | |
import org.eclipse.collections.impl.list.Interval; | |
/** | |
* The IDTO interface allows implementing domain objects to retrieve all | |
* or just the first data record. Note that the methods are static: | |
* neither the domain object nor the DAO objects are supposed to override | |
* these. Instead, the DAOs implement a mapper-like behavior. | |
* An application typically has exactly 1 copy of this interface, | |
* and as many implementations as there are domain objects that need it. | |
*/ | |
public interface IDTO { | |
/** | |
* Mapping between the data store's result set to the desired domain object. | |
*/ | |
interface IDAO<T> { | |
void fillCurrent(@WillNotClose final ResultSet data) throws SQLException; | |
T generate(); | |
} | |
/** | |
* Creates as many IDAO instances as there are records in the data store that | |
* match the passed-in query. | |
* @return a list of IDAO instances. May be empty. The instances may be empty, | |
* too. Never null. | |
*/ | |
static <T extends IDAO<?>> ImmutableList<T> retrieveAll ( | |
final DataSource ds, final Query query, final Class<T> type | |
) throws InstantiationException, IllegalAccessException, IllegalArgumentException, | |
InvocationTargetException, NoSuchMethodException, SecurityException, SQLException { | |
final MutableList<T> generators = Lists.mutable.empty(); | |
try (var conn = Objects.requireNonNull(ds.getConnection()); | |
var sql = conn.prepareStatement(query.toString()); | |
var dataSet = sql.executeQuery()) { | |
if (!dataSet.isClosed()) { | |
while (dataSet.next()) { | |
final T dao = type.getDeclaredConstructor().newInstance(); | |
dao.fillCurrent(dataSet); | |
output.add(dao); | |
} | |
} | |
} | |
return output.asUnmodifiable().toImmutable(); | |
} | |
/** | |
* Creates a IDAO instance of the 1st record in the data store that | |
* matches the passed-in query. | |
* @return the generated IDAO instance. May be empty. Never null. | |
*/ | |
static <T extends IDAO<?>> T retrieveFirst ( | |
final DataSource ds, final Query query, final Class<T> type | |
) throws InstantiationException, IllegalAccessException, IllegalArgumentException, | |
InvocationTargetException, NoSuchMethodException, SecurityException, SQLException { | |
final T dao = type.getDeclaredConstructor().newInstance(); | |
try (var dbConn = Objects.requireNonNull(ds.getConnection()); | |
var sql = dbConn.prepareStatement(query.toString()); | |
var dataSet = sql.executeQuery()) { | |
if (!dataSet.isClosed()) { | |
dataSet.next(); | |
dao.fillCurrent(dataSet); | |
} | |
} | |
return dao; | |
} | |
/** | |
* Reads the passed-in resultset to find and return its column names. | |
*/ | |
static ImmutableSet<String> getColumnNames(final ResultSet data) throws SQLException { | |
var meta = data.getMetaData(); | |
var columnCount = meta.getColumnCount(); | |
var columnNames = Interval.oneTo(columnCount).collect(index -> { | |
try { | |
return meta.getColumnName(index.intValue()); | |
} catch (final SQLException ex) { | |
ex.printStackTrace(); | |
} | |
return ""; | |
}).reject(String::isEmpty).toSet().asUnmodifiable().toImmutable(); | |
return columnNames; | |
} | |
} | |
Interface for a domain object: | |
package omegajunior.halfdao; | |
/** | |
* The domain object interface tells its implementing classes how to | |
* get themselves instantiated and filled with data from the data store. | |
* Note that the methods are static: an implementing class is not supposed | |
* to override them. | |
* | |
* An application typically has multiple domain object interfaces. | |
* This is an example. It should not be copied into your application this way. | |
* Using the word "DomainObject" as the name of a domain object violates the | |
* concept of domain objects. | |
* | |
* @author A.E.Veltstra | |
* @version 2.18.1130.1558 | |
*/ | |
public interface IDomainObject { | |
/** | |
* Fetches a configuration from the data store which matches the | |
* identifier. Note: the interface knows how to generate the query | |
* to retrieve data records, but the implentation dictates how | |
* that query is applied. | |
* | |
* @return the matching portal configuration. May be empty. Never null. | |
*/ | |
static IDomainObject load(final DataSource ds, final Identifier id) { | |
final Query query = IDomainObject.makeQueryToRetrieveFirstById(id); | |
return DomainObject.retrieveFirst(ds, query); | |
} | |
/** | |
* Generates a retrieval SQL statement with which to fetch the 1st record | |
* that matches the passed-in {@code id}. | |
* | |
* @return the generated SQL statement. | |
*/ | |
static Query makeQueryToRetrieveFirstById(final Identifier id) { | |
/** specific SQL statement code is generated and returned. */ | |
} | |
} | |
Implementation of a domain object: | |
package omegajunior.halfdao; | |
import java.lang.reflect.InvocationTargetException; | |
import java.sql.ResultSet; | |
import java.sql.SQLException; | |
import java.util.Objects; | |
import javax.annotation.Nullable; | |
import javax.annotation.concurrent.Immutable; | |
import javax.sql.DataSource; | |
import org.eclipse.collections.api.list.ImmutableList; | |
import org.eclipse.collections.impl.factory.Lists; | |
/** | |
* The domain object represents a tangible thing in the knowledge domain | |
* in which the application operates. For instance, if the application is | |
* supposed to let people book hotel rooms, this domain object could be | |
* a room. | |
* | |
* An application typically implements multiple domain objects. | |
* This is an example. It should not be copied into your application this way. | |
* Using the word "DomainObject" as the name of a domain object violates the | |
* concept of domain objects. | |
* | |
* @author A.E.Veltstra | |
* @version 2.18.1130.1606 | |
*/ | |
@Immutable | |
@lombok.Builder(builderClassName = "Builder", toBuilder = true) | |
public class DomainObject implements IDomainObject, IDTO { | |
/** | |
* To {@link #generate()} {@link IDomainObject}s. This generator acts | |
* like a proxy to the data store. | |
* | |
* @author A.E.Veltstra | |
* @version 2.18.1130.1617 | |
*/ | |
static class Generator implements IDTO.IDAO<IDomainObject> { | |
/** Helps build instances of {@link DomainObject}. */ | |
private DomainObject.Builder builder = DomainObject.builder(); | |
@Override | |
public void fillCurrent(final ResultSet data) throws SQLException { | |
if (data.isClosed() | |
|| data.isBeforeFirst() | |
|| data.isAfterLast() | |
|| 0 == data.getRow()) { | |
return; | |
} | |
var columnNames = IDTO.getColumnNames(data); | |
if (columnNames.contains("some expected column name")) { | |
/** Map data to an application type and store it in the builder. */ | |
} else { | |
/** Gather missing column names to report problems. */ | |
} | |
/** Repeat the above as needed. */ | |
} | |
@Override | |
public IDomainObject generate() { | |
return this.builder.build(); | |
} | |
} | |
/** | |
* Fetches the record from the data store, that fits the passed-in query, | |
* and turns it into a domain object. | |
* | |
* @return either an empty domain object in case of errors, or one that | |
* contains as much information as possible. Never null. | |
* Note that the return type is an interface rather than | |
* the implementation. | |
*/ | |
public static IDomainObject retrieveFirst ( | |
final DataSource ds, final SqlDml query | |
) { | |
IDomainObject got = DomainObject.empty(); | |
try { | |
got = IDTO.retrieveFirst(ds, query, Generator.class).generate(); | |
} catch (final SQLException | InstantiationException | IllegalAccessException | |
| IllegalArgumentException | InvocationTargetException | NoSuchMethodException | |
| SecurityException e) { | |
e.printStackTrace(); | |
} | |
return got; | |
} | |
/** | |
* In case of relationships in the data store, a domain object can load its | |
* related children if that child implements the same architecture. | |
*/ | |
public Child loadChild(final DataSource ds) { | |
return Child.retrieveFirst(ds, Child.makeQueryToRetrieveFirst(), this.identity); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment