We have moved away from having
any close contact with the database in the code.
Database access is handled by a separate, carefully handcoded, layer that handles the business logic and transaction handling. Access to that layer is by well-documented access metohds, that represent the problem space, not the database implementation. The programmer is thinking in terms of eg invoices, so define a method that hands back an invoice given its key and a method that updates/inserts an invoice, both representing the invoice in a way that is natural for the application. The complexities of storing the invoice in its interrelated set of 2, 3, or more tables is hidden. An application of the well-proven divide and conquer approach!
This gives us the ability to change the database structure without changing the myriad of code accessing it, and relieves the programmers of having to deal with the intricacies of coding against the database system. We find that often a query involves more than one table, and in surprisingly many cases it cannot be represented by a single SQL statement, but require several.
All this naturally leads you to implement the database access layer as a separate transaction server, which we have found is a nice productivity boost.
YMMV of course, but having coded in this way in the last approx. 7 years, after having done it in all kinds of ways since I got in touch with my first RDBMS in the late '80ies, I can only reccomend that you give it a try.
Of course SQL-generators can help the data access layer a bit, but in my experience not as much as to regain the effort to write them.