In any sufficiently complex application, something like a query builder that allows different parts of the application to work together to lazily compose queries (vs combining/filtering/sorting the data in memory) will be created at some point.
ORM is a tool that can make this easier, it's also a tool that can make it easier to shoot yourself in the foot (ie by making it easy to create N+1 queries without knowing). Like all tools, there are tradeoffs that need to be accounted for together with the actual use case to make a decision. Operating on SQL statement strings is not something I'd recommend in any case.
There are libraries that flip the concept of an ORM on its head. Instead of a library that allows lazy query composition, you write your queries and the library generates the code for that query at compile time. It’s a much better model in my opinion.
E.g. you could write a query like this:
getUser:
SELECT * FROM users WHERE id = ?
And the library would generate a class like: class GetUserQuery {
static getUser(id: String): GetUserQueryResult
}
Go: https://github.com/kyleconroy/sqlc
Rust: https://github.com/cornucopia-rs/cornucopia
This is my preferred method of interacting with databases now, if available.
Very flexible.