Back to basics: Writing an application using Go and PostgreSQL
henvic.dev
henvic.dev
I wrote an insanely long rant about this with thorough examples using the structure of the article, but it was too long for HN. So I put it on my blog instead: https://jrock.us/posts/go-interfaces/
The only reason why I defined inventory.DB (https://github.com/henvic/pgxtutorial/blob/main/internal/inv...) is to be able to test. Otherwise, I'd have skipped it.
I haven't read your article yet, but will do so later and get back to this subject and tell my opinion by the end of the day (on vacation, and need to hurry to catch the train!).
My blog post covers what unit testing is like with your interface, with a consumer-defined interface, and just putting some test data fields in the DBImpl struct. The latter is my preferred approach. It took me a long time to get there. I was working at Google writing some filesystem access code and wanted to test it. I designed it as interfaces so I could have a real implementation and a test implementation, and a member of the Go team reviewed the code and told me it was absolutely inappropriate to use interfaces there. I disagreed at the time, but I've slowly come to terms with this style. Separating out production and testing is "clean", but that cleanliness comes at the cost of a mental abstraction burden (every time you read the code you're confused about where the meat lives) and unbounded future maintenance costs (every time you want to add a new feature, you have to edit two files).
I don't think having a mock framework do codegen to make generic mocks is that useful. Just keep the exact behavior close to the test. A working test harness for a particular unit is something small enough to write out in an HN comment, and keeping the test code close to the test (and not wrapped up in some complicated framework) makes it much easier to enhance and debug the tests. (I've had Mockery forced upon me before, and I'm miserable every time I have to interact with it. Have to get the right version. Have to update an interface after writing the real implementation. Have to regenerate the mocks. Then I have to refer to the docs to do even the simplest tests, and those tests end up not doing what I want (instead producing something useless like "assert that function was called with the right args"). It's a chore that isn't worth the cost.)
I think the underlying psychology behind the mega-interfaces is probably that we come up with an idea for what the interface should look like, and want to write it down somewhere. Then we can disengage the brain and start filling out the method bodies. That is not a good enough reason to add a layer of abstraction to the code; you can always type out 'func RealImplementation() { panic("write this") }' if that's the real reason.
My view shifted when I was trying to use the OpenTelemetry library (circa v0.6.0) to emit Prometheus metrics. I wanted to set the histogram buckets on a per-metric basis. I chased 100 layers of indirection (every function took and returned an interface, so you'd need to have like 5 code windows open to debug the flow of data between the interface level and the implementation level), and never figured out how to do it. I think I determined it wasn't possible. It was at that moment that I decided "wait, I think everyone is using interfaces wrong", and then found CodeReviewComments telling me "yup, it's wrong".
For one thing, it's easy to work around the issue of "pasting every single function signature into this test file, and making it panic("not implemented") or something". You can simply embed the DB interface in your test struct, and only override the method(s) you need. For example, let's say the DB interface defines 10 methods, but our function under test only need one, GetThings(). We do this:
type testDB struct {
DB // embedded field named DB, so testDB implements DB
things []Thing
}
// but override GetThings method
func (d *testDB) GetThings() ([]Thing, error) {
return d.things, nil
}
The embedded DB field, which is a field named DB of type DB and is nil, pulls in all the methods of that type so testDB implements the DB interface. When you construct a testDB you just leave the DB field nil -- if any of the not-explicitly-defined methods are called, they'll panic with a nil pointer, but that's okay, they would explicitly panic in your test implementation anyway.The other thing is simply pragmatism: at some point it gets tedious to define all these tiny interfaces. Do you do it at the handler level, where each db sub-interface only has 1 or 2 methods? This blows out the number of interfaces. Or do you do it at the server level, where the db interface has all the methods (what you're opposed to)? Or something in between: have server "sections" grouped by logical feature, such as authDB, projectDB, and so on. Each might have 5-10 methods, and to test each sub-server you'd either do what I've suggested above with an embedded interface field, or just implement a single in-memory test db for each db type.
I do like what you said about interfaces obfuscating the code (it becomes more abstract and hard to follow and navigate), and that testing on the real db if you can is a good idea. Creating a new PostgreSQL db for every test seems like it'd be very slow and unnecessary, though. Why not just one db for each test run?
If it's for testing, I'd just skip mocking that out and run the queries against the real database, like you do in the article. Production is going to connect to Postgres and run `select * from foobars`. Might as well run `select * from foobars` in the tests, so I'm not surprised when it doesn't work in production.
(I haven't done this, but it does seem very achievable for this use case. See https://disaev.me/p/writing-useful-go-analysis-linter/ and https://golangci-lint.run/contributing/new-linters/)
I didn't look closely, but I think that just wrapping the native struct in an interface without the method someone wants to call will just lead them to `wrapper.(*pgxpool.Pool)` and calling the unsafe method anyway.
Yeah I don’t get ORMs.
Most ORMs advertise “type safety” and not having to learn SQL. But actually you’re just writing SQL code in a non-SQL language. Usually the type-safety is weak, and a good IDE will provide type-checking and even data-source checking to raw SQL anyways.
Converting complex object operations into relational operations is non-trivial. ORM objects auto-query and auto-update from the database, but these queries/updates are often unoptimal or happen at invalid times. It’s much easier to write and execute the SQL queries manually, and then auto-convert the results to/from regular objects.
In any meaningul sized codebase, converting to/from objects will be used all over the place. Then someone adds an abstraction on top, and congratulations - there is now an inhouse orm.
In my previous company, I was forced to use an outdated forked version of gorm. I'd just do my best to do anything else other than leading with databases there. Running the test suite took minutes, and to run way fewer test cases. It was a nightmare.
I think you're doing it wrong if you end up doing really complex queries in your ORM. It's kind of a judgement call, but at a certain point, you need to switch over to SQL. Good ORM's let you do that without too much hassle.
Every other ORM in other languages I've tried seems to have WTF moments. It could be that Ruby is a dynamic enough language that it allows you to work around these things easily, that stricter languages do not.
The real question to me is, why can't we drop the relational database? In many small projects (where you would use an ORM) it is overkill. Why can't I create a list of plain-old-data objects in my language, then add indicies on that, and do fast queries directly in the language? Most languages already have a data structure with an index on one key (a dict, hashmap or similar). With C#'s LINQ or Pythons list comprehensions we already have half of the solution.
I've never in my life heard of this, which IDE are you talking about? I'd love to use something like that! I'm in C# land where I can use Management Studio to write queries which works well enough, but going between that and Visual Studio does have some friction.
Even though we use Entity Framework here, I mostly use it to manage migrations than to work with data. Most of the time unless a query is really trivial, I'll just write some SQL and use Dapper for object mapping, I find it a lot easier once the query gets to be over three or four lines or so.
Sadly this functionality, which I think is not much more complex than a JSON parser, is absent from many toolkits / libraries, and an immediate option people see is an ORM. Libraries that just do serialization / deserialization from SQL are much less popular than something like Hibernate, which could be a reason.
That's...not writing SQL code, just as advertised. In the same way that writing code in a higher-level language isn't writing native machine code.
e.g. userPosts = Posts.InnerJoin(Users).SelectAll().Where(user => user.id == userID)
It’s not always exactly the same, but the point remains that you have to understand SQL anyways and the code usually isn’t any clearer.
Maybe i’m confusing this with query builders. ORMs also let you form queries by interacting with objects directly, e.g. ‘user = Users.Find(userID); userPosts = user.Posts’. But that leads to the other issue with inefficient and unpredictable queries.
Is there any truth to this?
Django's ORM doesn't stop you from using database-specific features (JSON fields, full text search, PostgreSQL's geo extensions, etc.) It also makes it very straightforward to bypass parts of the ORM and write your own SQL either just as the WHERE part of a query or as the whole SQL statement. If you build a large enough app that deals with enough data, you'll eventually find some hot spots where you need to do that for performance reasons. When you do that, you obviously can lose portability.
If you avoid those things though, you pretty much can switch the database. In practice, I've never really found a need to do that for a production system, but it still has some major advantages. First, I regularly run PostgreSQL in production, but I can use SQLite as an in-memory database for unit tests, which makes those much faster (and simpler to run unit tests without having to also spin up a full DBMS).
The other huge advantage is that it enables Django's whole ecosystem of reusable applications. Eg, the built-in admin interface, popular third party applications for authentication, tagging, CMS-type stuff, etc. are generally written using the ORM and as a result can be added to any Django project with a couple lines of config and will work no matter what database you use. That large ecosystem of fairly polished components has been a major reason that Django has remained so popular for such a long time. If it had to be split up by supported database ("MySQL admin" vs "PostgreSQL admin", etc), I don't think it would've been as successful.
Most of the databases I access are from external providers. We changed our main provider 2 years ago, so their DB went from Oracle to SQL Server.
So in 12 years that would have been one database change. Except all the schema has changed and they don't allow SQL queries on it but provide good old SOAP web services...
So even if I had used a full ORM (I use Dapper, you write the SQL, it "just" converts the result of the query to objects) I still would have to change my code to adapt to the new provider.
However going from SQL to webservices really convinced me SQL is a superpower. With it you get exactly the data you need, no need to re-process it in code.
Before this change I never had to really look at my apps performances, it was good enough. But since this provider webservices are so inefficient (for example I can't "query" a single customer, I have to get all of them) I had to optimize my app in every possible way to mitigate these inefficiencies.
I assume it would be a bit easier because the query and ORM syntax is uniform across databases. And it also probably wont support many database-specific operations.
But it would still be work. Some databases don’t support basic operations (e.g. UPDATE with LIMIT in postgres) which you can still do with the query syntax. And different databases usually have different preferred (usually faster) ways of doing indexes, joins etc. Plus you still have to migrate your data.
ORMs are ideally supposed to be a complete abstraction where you can pretend the database doesn’t exist, so if you switch it shouldn’t matter. The problem in, in practice you can’t pretend the database doesn’t exist. So you either end up writing code for your specific dialect anyways, or you write inefficient code.
It’s also like, you can just write plain SQL and ignore your dialect’s extra features. And that would make migration much easier, but it would also be less efficient and much harder to write.
-- name: ListAuthors :many
SELECT * FROM authors
ORDER BY name;
But what if I want to get a list of authors by date? Shall I do: -- name: ListAuthors :many
SELECT * FROM authors
ORDER BY name;
-- name: ListAuthorsByDate :many
...
And what if later I need to get a list of authors by date and name? Should I write yet another query? I'm used to write one generic (composable) query that is very handy in situations where one need to retrieve rows by many different filtering criteria. Is that possible in sqlc?- Use multiple queries that share the same output type.
- Push the predicate into the query directly.
However, you can't pass an expression for an order by column since the order by clause takes a name, not an expression. Postgres doesn't allow using names as arguments to a prepared query so that leaves either adding an annotation like sqlc.order_by that's dynamically added to a query string, or by getting more creative with the structure of the query:
SELECT * FROM AUTHORS WHERE sqlc.arg('by_date') ORDER BY DATE
UNION ALL
SELECT * FROM AUTHORS WHERE NOT sqlc.arg('by_date')pggen occupies the same design space as sqlc but the implementations are quite different. Sqlc figures out the query types using type inference in Go which is nice because you don’t need Postgres at build time. Pggen asks Postgres what the query types are which is nice because it works with any extensions and arbitrarily complex queries.
Check out https://github.com/launchbadge/sqlx#sqlx-is-not-an-orm for an alternative approach.
struct Country { country: String, count: i64 }
let countries = sqlx::query_as!(Country, "SELECT country, count FROM countries WHERE organization = ?", organization)
.fetch_all(&db_pool)
.await?;SQLite is not an equivalent to a standard networked database, as a developer you're supposed to see it as an enhanced data structure, like an array++. You wouldn't share an array between multiple processes (unless you know exactly what you're doing) so you wouldn't do the same for SQLite. If you really need multiple processes writing the same kind of data, maybe it should be partitioned into multiple independent files ?
Or for working with data in memory.
There's a whole world of software outside of saas crud
Litestream watches your SQLite database and then streams changes to a cloud storage provider (e.g., S3, Backblaze). You get the performance and simplicity of writing SQLite to the local filesystem, but it's syncing to the cloud. And the cool part is that you don't have to change any of your application code to do it - as far as your app is concerned, it's writing to a local SQLite file.
I wrote a little log uploading utility for my business that uses Litestream, and it's been fantastic.[1] It essentially carries around its data with it, so I can deploy my app to Heroku, blow away the instance and then launch it on fly.io, and it pops up with the exact same data.[2]
I'm currently in the process of rewriting an open-source AppEngine app to use SQLite + Litestream instead of Google Firestore.[2] It's such a relief to get away from all the complexity of GCP and Firestore and get back to simple SQLite.
[1] https://mtlynch.io/litestream/
[2] https://asciinema.org/a/I2HcYheYayeh7aHj23QSY9Vyf/embed?size...
Look, it comes down to what you, the developer are comfortable with. If you know and are comfortable with running something like postgres in the cloud, then go for it.
Some of us prefer to not. Similarly, not all of us nuke the entire filesystem on every deploy :)
I've had a positive experience with the project.
https://github.com/jackc/pgx/issues/863
pgx doesn't seem to handle reconnects the same way lib/pg does, but I haven't tested this recently: https://github.com/jackc/pgx/issues/672
I wonder why, though?
I'd rather manage the database separately, and just have it configured for my tests to use them. This is why I kind of pushed towards a solution that didn't involve Docker.
Someone might think: "oh, but what [web] developer doesn't have Docker installed?" Well, I dod, but I always have problems with it, so I barely use it. I also prefer to run my database on a homelab server (https://henvic.dev/posts/homelab/) instead of on my laptop, especially because I also need OpenSearch or Elasticsearch running, and it's better if I just offload the performance|battery hit.
Funny is that when I joined the company back in February I was asked what computer I'd like to receive, and I really wanted a 13" MacBook with the new M1 processor, so I asked for it, but in the end I decided to go with Intel as I imagined using Docker would be very important.
It turns out that the M1 would work just as fine for the type of work I've been doing since then, with some minor exceptions.