First of all, consider what your application needs to do. If your requirements include joining data from multiple sources, and this is not an optional feature, than regardless of the technology you select, you will need to implement such logic one way or another. In particular, this may mean that certain databases will not be well-suited for your your task, and there's not much you can do about it. If there was a single database which was best for all tasks, we would probably not have dozens of different databases on the market.
Once you know what kind of logic you need, create a data access API within your application. It should work on domain objects, which should be separate from the objects you will be storing in your database, even if they may look very similar. So your API might be, for example, a Java interface:
interface CustomerRepository { Customer findCustomer(String id); }
Now, the actual database code should be in a class implementing this interface, for example:
class MySqlCustomerRepository implements CustomerRepository { Customer findCustomer(String id) { //do stuff using MySql and some DAO objects you will use with it } }
If at a later time you decide to use another database, you will be able to change the implementation to e.g.:
class MongoDbCustomerRepository implements CustomerRepository { Customer findCustomer(String id) { //do stuff using MongoDB and some DAO objects you will use with it } }
The code inside will probably be completely different, as will be the DAO objects used with the particular databases. For example, you might decide to store such data as user's name and address in separate DB tables in a normalized manner and use joins between those tables to combine the data when using MySql, while you use denormalized data and store the address together with the name and other fields when using MongoDB. Correspondingly, the DAOs used within the repositories will be different. However, the domain object Customer you return in the end, will be the same, and this means that the application code using your CustomerRepository
need not be aware of the implementation you use.
This is the power of abstraction. It will make testing your application easier as well since for some tests, you will be able to mock the repository instead of using a real one.
Note however, that changing a database implementation may still not be so easy. It is possible, I've done such things myself, but it's a serious decision and depending on your access patterns, what kind of queries you use, performance requirements, etc., your choice may be quite limited (e.g. only between different SQL databases or between different KV-store DBs). I recommend the repository abstraction nonetheless, since it clearly separates concerns (app business logic and data storage) and makes testing easier, which may both be very helpful even if you end up never changing the DB implementation underneath.