Skip to content

Instantly share code, notes, and snippets.

@aeveltstra
Created May 5, 2021 16:25
Show Gist options
  • Save aeveltstra/a95472cafcfdd2f9a9e3e1f6512172a2 to your computer and use it in GitHub Desktop.
Save aeveltstra/a95472cafcfdd2f9a9e3e1f6512172a2 to your computer and use it in GitHub Desktop.
HalfDao, Java
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