Reputation: 57
For educational purposes, I'm developing a simple desktop/console Java program to persist data into MySQL database using JDBC. I'd like to know if the following architecure/layers are ok (despite the computational/maintenance cost, interpretability etc):
Class Product
public class Product {
private int id;
private String name;
//getters and setters and constructors
}
ConnectionFactory:
public abstract class ConnectionFactory {
private static Connection conn = null;
public static Connection getConnection() throws SQLException{
if (conn == null)
conn = DriverManager.getConnection("jdbc:mysql://localhost/database_name","user","password");
return conn;
}
}
Interface DAO
public interface DAO <T>{
void save(T type);
List<T> findAll();
}
Interface ProductDAO
public class ProductDAO implements DAO<Product>{
@Override
public void save(Product p) {
String sql = "INSERT INTO product values (?,?)";
try(Connection con = ConnectionFactory.getConnection();
PreparedStatement stmt = con.prepareStatement(sql)) {
stmt.setInt(1, p.getId());
stmt.setString(2, p.getName());
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public List<Product> buscarTodos() {
String sql = "SELECT * FROM product";
List<Product> listProd =new ArrayList<>();
try(Connection con = ConnectionFactory.criaConexao();
PreparedStatement stmt = con.prepareStatement(sql)) {
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
Product p = new Product(rs.getInt("id"), rs.getString("name"));
listProd.add(p);
}
} catch (SQLException e) {
e.printStackTrace();
}
return listProd;
}
}
Interface ProductService:
public interface Service <T>{
void save(T type);
List<T> findAll();
}
Class ProductService:
public class ProductService implements Service<Product>{
ProductDAO productDAO ;
public ProductService() {
productDAO= new ProductDAO();
}
@Override
public void save(Product p) {
productDAO.save(p);
}
@Override
public List<Product> findAll() {
return productDAO.findAll();
}
}
Class Main
public class Main {
public static void main(String[] args) {
Product p1 = new Product(1,"Product A");
ProductService productService = new ProductService();
productService.save(p1);
}
}
I'd be grateful for comments and suggestions.
Upvotes: 2
Views: 142
Reputation: 9479
Going to go ahead and try and provide some counterpoint to rzwitserloot's answer (which is very good, and has a lot of really salient points).
I've absolutely written large-scale applications that look 90% like the suggested pattern. And they have scaled remarkably well. They have incredibly boring, verbose code, and likely can be updated 20 years later with very little learning-curve. The big advantage in building an application like that is that you get absolute control over your low-level execution of your database commands, which can provide a huge amount of performance and control. Furthermore, without dependencies on third-party libraries, they never have out-of-date or incompatible versions, and generally will keep running for decades.
The downside is, of course, writing a VERY large amount of code. That code is boring, and in many cases irrelevant, and something like JOOQ can be a huge accelerator. It's a tradeoff between control and efficiency (as ALL abstractions are). If it is important to you to exactly control how you are talking to the database, how you are managing your transactions, and exactly optimizing performance, the above patterns will give you that control, while still isolating your data-access to a separate layer and keeping business-logic separate / testable.
I do not necessarily agree with the option to embed SQL / connection logic directly into business logic, it makes unit-testing a nightmare.
What you need to ask yourself is, is it important that I control these interactions? Or is it ok if a framework takes over this because I don't need that level of control (or if you are not qualified to deal with it). It's the same question we ask any time we are building software, what do I need control over, and what can I pass over to an abstraction to handle? You ARE going to program to some abstraction, SQL itself is an abstraction layer over the database operations, the question is, what's the right abstraction layer for your particular needs? Figuring this out regularly is one of the more important bit of software architecture and design.
As for reading/writing to a file, it's once again, just another abstraction layer (albeit pretty far down compared to some of the other persistence mechanisms described here). You could easily take your DAO pattern, and back-end it with a file system. Once again, keeping that out of your business logic, because writing a unit test around file I/O is a nightmare, and watch out for concurrency/locking issues. A relational database is literally just an abstraction over files that provides some nice automatic-handling of things like locking, concurrent updates, etc. I've definitely used it for certain use-cases, usually heavy read and very light write-operations where I need complete control.
I'm sorry to say, like many answers in this forum, "it depends" is the correct answer to your question. Figuring out what it depends on is a larger discussion than what you will get here.
Upvotes: 1
Reputation: 103453
This is… useless. It works, sure. And the program you will produce will do what it is designed to do and it will be easy enough to understand. But it is useless in the sense that this lesson will not easily translate to anything meaningful (meaningful in the sense of: You have now learned a technique that will let you write more maintainable software which is much more difficult without these learned techniques). This approach gets you the worst of SQL and the worst of Java - if this DB grows to anything more than a handful of rows, you really really do not want to just SELECT * FROM foo
and then sort out the rest in java. The whole point of a relation DB engine is that you can do relational queries on it, and this system makes that at worst impossible and at best gratingly complicated.
The point of doing all this effort to write a DAO and all that is primarily to separate out the java side from the SQL side. In other words, insulating the main code from having to even know what SQL is, is sort of the point. However, ditching SQL means, pretty much, ditching the ability to write queries efficiently once it gets 'interesting' (lots of data, complicated queries). Hence, if you do this right, you add escape hatches where you can tweak how a query is actually done for performance reasons. Setting the system up so that it is somewhat easy to do this, is complicated.
What you've done is badly reinvent Jakarta Persistence (JPA; formerly Java Persistence API) (Hibernate is the most commonly used implementation of JPA, perhaps you've heard of that). Which gets you all the downsides of this system (namely, that it is not possible to actually make the program work properly once things get interesting without serious SQL knowledge in which case... why did you go through all this trouble?), but some upsides such as nominally at least DB engine independence.
Doing transactions right is much trickier than you think. The easiest way is to use SERIALIZABLE
transaction isolation which is by far the most 'fool proof' (and note that errors in how transactions interact results in serious data corruption that is extremely hard to test for. 'fool proof' is very useful here!), however, your setup is not compatible with SERIALIZABLE.
Trying to interleave your setup with transactions requires pushing the transaction to a higher level (you can't start a transaction in the method that INSERTs some data and commit it in the same method - what if you want to insert some data, query something else, insert some more, and that whole job is 1 single transaction?), and transactions in SERIALIZABLE require retries. Most DBs implement that isolation level by checking, when you commit, if all the queries you did still provide the same results. If not, they say: Oh, whoops - none of what you tried is going to happen, please start again. This is also called optimistic locking and is the standard answer for how to get the best of both worlds and both [A] have a database that isn't hopelessly global-lock-based slow, and [B] have consistent transactions where data queried can be relied upon to actually be consistent throughout a transaction.
But, because of optimistic locking, you must therefore catch that RetryException
(which is complicated, because JDBC makes it complicated to figure out if an SQLException
is a retry), and rerun the code, which requires writing lambdas or using a Spring-style annotation-on-a-method so that a framework can choose to rerun that method if a retry comes up.
Useful DB abstractions take care of this, and other such db interaction concerns - concerns about managing how the DB interaction itself works, not concerns about how to send the right SQL. JDBI and JOOQ are the libraries you're looking for - they do this stuff properly.
Well, why involve a relational DB system at all then? Just write hashmaps and arraylists to disk! If queries by reading it all in and then searching through what you get all in memory is perfectly fine, then… that's a lot simpler and won't require hosting an entire DB on the target machine!
Pick a side.
Either you treat the fact that you host this data on a relational DB as a mostly irrelevant detail, in which case you want a JPA implementation which gets you all this stuff for free, extra bonuses such as DB engine independence, and far more thought-through methods to run queries and the like, and you at least stand a chance if performance becomes an issue (but note that JPA is complicated, and SQL is complicated, and if the task gets interesting you will have to be an expert at both, so, this is far more complex than the next option!).
Or, embrace SQL and have your primary business code write SQL directly. Stop thinking about tables as things that have an equivalent Java class. Because SQL isn't just 'SELECT * FROM table'. It's:
SELECT a.foo, DATEDIFF(YEAR, b.birthDate, NOW()) >= 18 AS adult FROM a INNER JOIN b ON stuff WHERE clauses
UNION SELECT more stuff
And that doesn't translate to 'java class'. Not to mention how DB structure has various DB migration tools available to it (software flows - you get change requests and address them. This tends to result in changes to the DB structure. If 'just toss all data in the bin and start over completely' is not an option, and that really shouldn't be, you need a more complicated solution for migrating structure!).
So don't bother. Don't have classes that match the tables. JDBI and JOOQ have excellent tooling for making classes on the fly (matching your query, not your tables).
Or conclude that the data needs are tiny, ditch the concept of 'database', and just use simplistic files instead, where your app reads it all, works entirely in memory, and any saving of anything means writing the entire DB to disk, every time. Hey, if the data is small (a few thousand products max), this would be pretty much instant.
The primary lesson you may learn from continuing with this code base is to get experience. Experience in how not to do things. The problem with complicated experience-based lessons such as this answer is that it's difficult and against human nature to internalize it. It's going to stick much better in your head if you actually write an application based on how you understand things should be, and then suffer for a year or two with an endless parade of code that seems far more complex than it should be, and a product that is riddled with bugs, hard to update, and where seemingly trivial requests for changes require weeks of coding to deliver.
But, the only way that's actually going to happen is if this educational app will actually be used by real users having realistic expectations for a longer period of time (years, preferably). If that is not in the cards, this product is mostly a waste of time.
Better ideas:
Upvotes: 3