Code that lists and inserts data to database: Make methods more reusable in superclass I'm trying to make some methods more reusable and have now three classes that work together: AbstractDao (the superclass), MemberDao (extends AbstractDao) and ProjectTaskDao (extendsAbstractDao). The method insert is very similar in both subclasses with some minor differences:
insert method in MemberDao:
public void insert(Member member) throws SQLException {
try (Connection connection = dataSource.getConnection()){
    try(PreparedStatement statement = connection.prepareStatement(
            &quot;INSERT INTO members (member_name, email) values (?, ?)&quot;,
            Statement.RETURN_GENERATED_KEYS
    )){
        statement.setString(1, member.getName());
        statement.setString(2, member.getEmail());
        statement.executeUpdate();

        try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
            generatedKeys.next();
            member.setId(generatedKeys.getLong(&quot;id&quot;));
        }
    }
}

}
insert method in ProjectTaskDao:
public void insert(ProjectTask task) throws SQLException {
try (Connection connection = dataSource.getConnection()){
    try(PreparedStatement statement = connection.prepareStatement(
            &quot;INSERT INTO project_tasks (task_name) values (?)&quot;,
            Statement.RETURN_GENERATED_KEYS
    )){
        statement.setString(1, task.getName());
        statement.executeUpdate();

        try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
            generatedKeys.next();
            task.setId(generatedKeys.getLong(&quot;id&quot;));
        }
    }
}

}
Thesee methods do the same with some minor differences with the &quot;statement.setSomething()&quot;. I`m not that advanced in Java yet so I'm kind of stuck on this. I would also like to pull these methods in as well:
list method in MemberDao:
public List&lt;Member&gt; list() throws SQLException {
List&lt;Member&gt;  members = new ArrayList&lt;&gt;();
try (Connection connection = dataSource.getConnection()) {
    try (PreparedStatement statement = connection.prepareStatement(&quot;SELECT * FROM members&quot;)) {
        try (ResultSet rs = statement.executeQuery()) {
            while (rs.next()) {
                Member member = new Member();
                member.setName(rs.getString(&quot;member_name&quot;));
                members.add(mapRow(rs));
            }
        }
    }
}
return members;

}
list method in ProjectTaskDao:
public List&lt;ProjectTask&gt; list() throws SQLException {
List&lt;ProjectTask&gt;  tasks = new ArrayList&lt;&gt;();
try (Connection connection = dataSource.getConnection()) {
    try (PreparedStatement statement = connection.prepareStatement(&quot;SELECT * FROM project_tasks&quot;)) {
        try (ResultSet rs = statement.executeQuery()) {
            while (rs.next()) {
                //ProjectTask projectTask = new ProjectTask();
                //projectTask.setName(rs.getString(&quot;&quot;));
                tasks.add(mapRow(rs));
            }
        }
    }
}
return tasks;

}
And here is the whole superclass as it stands currently:
public abstract class AbstractDao&lt;T&gt; {
protected final DataSource dataSource;

public AbstractDao(DataSource dataSource) {
    this.dataSource = dataSource;
}

protected T retrieve(Long id, String sql) throws SQLException {
    try (Connection connection = dataSource.getConnection()) {
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setLong(1, id);
            try (ResultSet rs = statement.executeQuery()) {
                if (rs.next()) {
                    return mapRow(rs);
                } else {
                    return null;
                }

            }
        }
    }
}

protected abstract T mapRow(ResultSet rs) throws SQLException;

}
Let me know if you need more information to help! Thanks :D
The first advise is to not over-architect the classes.

Keep alternatives in mind - JPA for instance eclipseLink. A bit of not reinventing the wheel as the database field has a long history of &quot;clever&quot; code.
This is an O/R mapping translating data base Relations into java Objects and vice versa. For a beginner I would first refrain from such magic, and continue as you did. (Your mapRow.)
The entity classes like Member in general are kept small without logic.
And for every entity class there is a repository class with data access (retrieve and such). They rely on a persistence manager singleton instance that takes care of the connection per transaction session (time doing some task).

A long ID and using generated keys is used often.
For the ID inheritance can be used. Too much inheritance can be painful however.


So keep id in its own base class, and do
public class Member extends IdEntity { ... }

public void insert(Member member) throws SQLException {
    try (Connection connection = dataSource.getConnection()) {
        try (PreparedStatement statement = connection.prepareStatement(
                &quot;INSERT INTO members (member_name, email) values (?, ?)&quot;,
                Statement.RETURN_GENERATED_KEYS)) {
            statement.setString(1, member.getName());
            statement.setString(2, member.getEmail());
            statement.executeUpdate();

            SQLUtils.fillGeneratedKeys(member, statement);
        }
    }
}

public abstract class SQLUtils {

    public static void fillGeneratedKeys(IdEntity entity, PreparedStatement statement) throws SQLException {
        try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
            generatedKeys.next();
            entity.setId(generatedKeys.getLong(&quot;id&quot;));
        }
    }

3a. Similar is the initialisation of a time stamp field. If you disregard the database server's clock and use java's clock, think of a locale independent time.

Consider a &quot;transaction&quot; level; grouping of data base access.
Several database operations, say some INSERTs and UPDATEs best function under the same connection. Possible as transaction, an error raising a rollback and otherwise the data is committed at the end. This cleans up the repetition of try (Connection ....


In every case consider a connection pool or at least getting the connection by your own utility function.

For query methods do not use null results but Optional or exceptions. It not only prevents NullPointerExceptions, but allows chaining:

Several usage like:
A.retrieve().map(A::getChildren()).map(List::size).orElse(0);

protected Optional&lt;T&gt; retrieve(long id, String sql) throws SQLException {
    try (Connection connection = dataSource.getConnection()) {
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setLong(1, id);
            try (ResultSet rs = statement.executeQuery()) {
                if (rs.next()) {
                    return Optional.of(mapRow(rs)=;
                }
                return Optional.empty();
            }
        }
    }
}

Here Long can be long (It is a long id field too). For retrieve-by-id an exception would probably be better.
Here it could be a painful use of inheritance as the child class provides logic; but it is okay.
protected abstract String retrieveByIdSQL();

public final T retrieveById(long id) throws SQLException {
    String sql = retrieveByIdSQL();
    try (Connection connection = dataSource.getConnection()) {
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setLong(1, id);
            try (ResultSet rs = statement.executeQuery()) {
                if (rs.next()) {
                    return mapRow(rs);
                }
                throw new SQLException(...);
            }
        }
    }


Consider translating an SQLException to your own child of a RuntimeException, by rethrowing catched exceptions. Checked exceptions like SQLException must be catched,
which nowadays with Stream usage can be cumbersome. But you could postpone this for the fare future.